From 4263564f35b3b0b27a149d7fb69b485a208d0bbc Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:21:50 -0700 Subject: [PATCH 1/3] ACM-36696 Preserve newlines in CSV export to match console rendering Backport of #6081 to release-2.14. Fixes returnCSVSafeString to preserve newlines instead of replacing them with spaces, so the exported CSV matches the UI rendering of policy descriptions. Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 --- frontend/src/resources/utils/utils.ts | 11 ++++++++--- frontend/src/ui-components/AcmTable/AcmTable.test.tsx | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/resources/utils/utils.ts b/frontend/src/resources/utils/utils.ts index d7e15df8cf0..9079206b334 100644 --- a/frontend/src/resources/utils/utils.ts +++ b/frontend/src/resources/utils/utils.ts @@ -53,9 +53,14 @@ export function exportObjectString(object: Record) { return keyValueMap.toString() } -export function returnCSVSafeString(exportValue: string | ReactNode) { - // extract newlines - return `"${typeof exportValue === 'string' ? exportValue.split('\n').join(' ').replace(/"/g, '""') : exportValue}"` +export function returnCSVSafeString(exportValue: string | number | ReactNode) { + if (typeof exportValue === 'number') { + return `"${exportValue}"` + } + if (typeof exportValue !== 'string') { + return '"-"' + } + return `"${exportValue.replaceAll('"', '""')}"` } export const getISOStringTimestamp = (timestamp: string) => { diff --git a/frontend/src/ui-components/AcmTable/AcmTable.test.tsx b/frontend/src/ui-components/AcmTable/AcmTable.test.tsx index db828f6e544..f77b0596539 100644 --- a/frontend/src/ui-components/AcmTable/AcmTable.test.tsx +++ b/frontend/src/ui-components/AcmTable/AcmTable.test.tsx @@ -1150,10 +1150,10 @@ describe('AcmTable', () => { }) describe('returnCSVSafeString', () => { - test('returns a csv safe string replacing new line with space', () => { + test('returns a csv safe string preserving newlines', () => { const content = 'testing for multiline\ndescription' const exportContent = returnCSVSafeString(content) - expect(exportContent).toEqual('"testing for multiline description"') + expect(exportContent).toEqual('"testing for multiline\ndescription"') }) test('returns a csv safe string escaping double quotes', () => { From 6669c3a387d0a1463e663ddfd720f782fdf852be Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:10:20 -0700 Subject: [PATCH 2/3] Backport ACM-32500 unit test for CSV export whitespace matching Add test case validating that preserveWhitespace textContent matches CSV export format for multiline descriptions, ensuring UI and export consistency for policy descriptions. Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Haiku 4.5 --- .../routes/Governance/common/util.test.tsx | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/frontend/src/routes/Governance/common/util.test.tsx b/frontend/src/routes/Governance/common/util.test.tsx index 709cb8ab87a..07a3d365077 100644 --- a/frontend/src/routes/Governance/common/util.test.tsx +++ b/frontend/src/routes/Governance/common/util.test.tsx @@ -1,10 +1,11 @@ /* Copyright Contributors to the Open Cluster Management project */ 'use strict' -import { hasInformOnlyPolicies, getPolicyRemediation, resolveExternalStatus, parseStringMap } from './util' +import { hasInformOnlyPolicies, getPolicyRemediation, resolveExternalStatus, parseStringMap, preserveWhitespace, returnCSVSafeString } from './util' import { PolicyTableItem } from '../policies/Policies' import { Policy, PolicyTemplate, REMEDIATION_ACTION } from '../../../resources' import { cloneDeep } from 'lodash' +import { render } from '@testing-library/react' describe('Test resolveExternalStatus', () => { const mockPolicyWithManagers = (managers: string[]): Policy => { @@ -901,3 +902,24 @@ describe('Test parseStringValue', () => { ) }) }) + +describe('ACM-32500: UI description text should match CSV export for multiline descriptions', () => { + const descriptionFromApi = + 'Policy is placed on hub or managed clusters with label acm-virt-config=acm-dr-virt-config-file-name.\nCreates a velero Schedule for all virtualmachines.kubevirt.io resources with a cluster.open-cluster-management.io/backup-vm label.' + + test('preserveWhitespace textContent should equal CSV export value', () => { + const formatted = preserveWhitespace(descriptionFromApi) + const { container } = render(<>{formatted}) + const uiText = container.textContent + + const csvRaw = returnCSVSafeString(descriptionFromApi) + const csvText = csvRaw.slice(1, -1).replace(/""/g, '"') + + expect(uiText).toEqual(csvText) + }) + + test('preserveWhitespace returns undefined for empty input', () => { + expect(preserveWhitespace(undefined)).toBeUndefined() + expect(preserveWhitespace('')).toBeUndefined() + }) +}) From 49789e441400469e84a694496c37e48a8485779a Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:02:55 -0700 Subject: [PATCH 3/3] test(ACM-32500): Fix CSV export tests to validate actual 2.14 code Replace misleading preserveWhitespace tests that depended on code not present on release-2.14. Replace with focused tests validating the actual returnCSVSafeString fix behavior: newline preservation, quote escaping, number handling, and ReactNode fallback. Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Haiku 4.5 --- .../routes/Governance/common/util.test.tsx | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/frontend/src/routes/Governance/common/util.test.tsx b/frontend/src/routes/Governance/common/util.test.tsx index 07a3d365077..ff290bb47f3 100644 --- a/frontend/src/routes/Governance/common/util.test.tsx +++ b/frontend/src/routes/Governance/common/util.test.tsx @@ -1,11 +1,11 @@ /* Copyright Contributors to the Open Cluster Management project */ 'use strict' -import { hasInformOnlyPolicies, getPolicyRemediation, resolveExternalStatus, parseStringMap, preserveWhitespace, returnCSVSafeString } from './util' +import { hasInformOnlyPolicies, getPolicyRemediation, resolveExternalStatus, parseStringMap } from './util' +import { returnCSVSafeString } from '../../../resources/utils' import { PolicyTableItem } from '../policies/Policies' import { Policy, PolicyTemplate, REMEDIATION_ACTION } from '../../../resources' import { cloneDeep } from 'lodash' -import { render } from '@testing-library/react' describe('Test resolveExternalStatus', () => { const mockPolicyWithManagers = (managers: string[]): Policy => { @@ -903,23 +903,31 @@ describe('Test parseStringValue', () => { }) }) -describe('ACM-32500: UI description text should match CSV export for multiline descriptions', () => { +describe('ACM-32500: returnCSVSafeString preserves newlines in CSV export', () => { const descriptionFromApi = 'Policy is placed on hub or managed clusters with label acm-virt-config=acm-dr-virt-config-file-name.\nCreates a velero Schedule for all virtualmachines.kubevirt.io resources with a cluster.open-cluster-management.io/backup-vm label.' - test('preserveWhitespace textContent should equal CSV export value', () => { - const formatted = preserveWhitespace(descriptionFromApi) - const { container } = render(<>{formatted}) - const uiText = container.textContent - + test('returnCSVSafeString preserves newlines', () => { const csvRaw = returnCSVSafeString(descriptionFromApi) - const csvText = csvRaw.slice(1, -1).replace(/""/g, '"') + const unquoted = csvRaw.slice(1, -1).replace(/""/g, '"') + + expect(unquoted).toContain('\n') + expect(unquoted).toEqual(descriptionFromApi) + }) + + test('returnCSVSafeString escapes double quotes', () => { + const stringWithQuotes = 'Description with "quoted" text' + const result = returnCSVSafeString(stringWithQuotes) + + expect(result).toBe('"Description with ""quoted"" text"') + }) - expect(uiText).toEqual(csvText) + test('returnCSVSafeString handles number input', () => { + expect(returnCSVSafeString(42)).toBe('"42"') }) - test('preserveWhitespace returns undefined for empty input', () => { - expect(preserveWhitespace(undefined)).toBeUndefined() - expect(preserveWhitespace('')).toBeUndefined() + test('returnCSVSafeString handles non-string/non-number ReactNode', () => { + expect(returnCSVSafeString(null)).toBe('"-"') + expect(returnCSVSafeString(undefined)).toBe('"-"') }) })