Skip to content

Commit 57ef572

Browse files
authoredFeb 11, 2025
Refactor exporter unit test to use Jest spyOn (stolostron#4232)
* refactor exporter unit test to use Jest spy Signed-off-by: Randy Bruno Piverger <rbrunopi@redhat.com> * leverage getISOStringTimestamp() util to format export timestamps Signed-off-by: Randy Bruno Piverger <rbrunopi@redhat.com> --------- Signed-off-by: Randy Bruno Piverger <rbrunopi@redhat.com>
1 parent b40dd92 commit 57ef572

26 files changed

+321
-265
lines changed
 

‎frontend/src/resources/utils/utils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export function returnCSVSafeString(exportValue: string | ReactNode) {
5959
return `"${typeof exportValue === 'string' ? exportValue.split('\n').join(' ').replace(/"/g, '""') : exportValue}"`
6060
}
6161

62+
export const getISOStringTimestamp = (timestamp: string) => {
63+
return new Date(timestamp).toISOString()
64+
}
65+
6266
export function parseLabel(label?: string | null) {
6367
let prefix, oper, suffix
6468
if (label && label.includes('=')) {

‎frontend/src/routes/Applications/AdvancedConfiguration.test.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -417,9 +417,9 @@ describe('Export from application tables', () => {
417417
expect(blobConstructorSpy).toHaveBeenCalledWith(
418418
[
419419
'Name,Namespace,Channel,Applications,Clusters,Time window,Created\n' +
420-
'"helloworld-simple-subscription-1","helloworld-simple-ns","ggithubcom-app-samples",-,"None",-,"Thu Jul 30 2026 03:18:48 GMT+0000"\n' +
421-
'"helloworld-simple-subscription-2","helloworld-simple-ns","ggithubcom-app-samples",-,"None",-,"Thu Jul 30 2026 03:18:48 GMT+0000"\n' +
422-
'"helloworld-simple-subscription-3","helloworld-simple-ns","ggithubcom-app-samples","1","1 Remote, 1 Local","Active","Thu Jul 30 2026 03:18:48 GMT+0000"',
420+
'"helloworld-simple-subscription-1","helloworld-simple-ns","ggithubcom-app-samples",-,"None",-,"2026-07-30T03:18:48.000Z"\n' +
421+
'"helloworld-simple-subscription-2","helloworld-simple-ns","ggithubcom-app-samples",-,"None",-,"2026-07-30T03:18:48.000Z"\n' +
422+
'"helloworld-simple-subscription-3","helloworld-simple-ns","ggithubcom-app-samples","1","1 Remote, 1 Local","Active","2026-07-30T03:18:48.000Z"',
423423
],
424424
{ type: 'text/csv' }
425425
)
@@ -439,7 +439,7 @@ describe('Export from application tables', () => {
439439
expect(blobConstructorSpy).toHaveBeenCalledWith(
440440
[
441441
'Name,Namespace,Type,Subscriptions,Clusters,Created\n' +
442-
'"ggithubcom-app-samples-ns/ggithubcom-app-samples","default","Git",-,"None","Fri Jun 28 2024 03:18:48 GMT+0000"',
442+
'"ggithubcom-app-samples-ns/ggithubcom-app-samples","default","Git",-,"None","2024-06-28T03:18:48.000Z"',
443443
],
444444
{ type: 'text/csv' }
445445
)
@@ -459,7 +459,7 @@ describe('Export from application tables', () => {
459459
expect(blobConstructorSpy).toHaveBeenCalledWith(
460460
[
461461
'Name,Namespace,Clusters,Created\n' +
462-
'"helloworld-simple-placement-3","helloworld-simple-placement-3","1 Remote, 1 Local","Fri Jun 28 2024 03:18:48 GMT+0000"',
462+
'"helloworld-simple-placement-3","helloworld-simple-placement-3","1 Remote, 1 Local","2024-06-28T03:18:48.000Z"',
463463
],
464464
{ type: 'text/csv' }
465465
)
@@ -478,7 +478,7 @@ describe('Export from application tables', () => {
478478

479479
expect(blobConstructorSpy).toHaveBeenCalledWith(
480480
[
481-
'Name,Namespace,Clusters,Replicas,Created\n"test-placementRule","default","Local","1","Fri Jun 28 2024 03:18:48 GMT+0000"',
481+
'Name,Namespace,Clusters,Replicas,Created\n"test-placementRule","default","Local","1","2024-06-28T03:18:48.000Z"',
482482
],
483483
{ type: 'text/csv' }
484484
)

‎frontend/src/routes/Applications/AdvancedConfiguration.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ import {
1313
TextVariants,
1414
} from '@patternfly/react-core'
1515
import { cellWidth } from '@patternfly/react-table'
16-
import { AcmExpandableCard, IAcmRowAction, IAcmTableColumn } from '../../ui-components'
1716
import _ from 'lodash'
1817
import { useCallback, useEffect, useMemo, useState } from 'react'
1918
import { Link, useNavigate } from 'react-router-dom-v5-compat'
20-
import { useRecoilValue, useSharedAtoms } from '../../shared-recoil'
2119
import { useTranslation } from '../../lib/acm-i18next'
2220
import { DOC_LINKS, ViewDocumentationLink } from '../../lib/doc-util'
2321
import { canUser } from '../../lib/rbac-util'
@@ -37,12 +35,14 @@ import {
3735
SubscriptionDefinition,
3836
SubscriptionKind,
3937
} from '../../resources'
38+
import { getISOStringTimestamp } from '../../resources/utils'
39+
import { useRecoilValue, useSharedAtoms } from '../../shared-recoil'
40+
import { AcmExpandableCard, IAcmRowAction, IAcmTableColumn } from '../../ui-components'
4041
import { IDeleteResourceModalProps } from './components/DeleteResourceModal'
4142
import ResourceLabels from './components/ResourceLabels'
4243
import { ApplicationToggleOptions, ToggleSelector } from './components/ToggleSelector'
4344
import { ClusterCount, getAge, getClusterCountString, getEditLink, getSearchLink } from './helpers/resource-helper'
4445
import { useHubCluster } from './helpers/useHubCluster'
45-
import { getMoment } from '../../resources/utils'
4646

4747
export interface AdvancedConfigurationPageProps {
4848
readonly defaultToggleOption?: ApplicationToggleOptions
@@ -531,7 +531,7 @@ export default function AdvancedConfiguration(props: AdvancedConfigurationPagePr
531531
},
532532
exportContent: (resource) => {
533533
if (resource.metadata?.creationTimestamp) {
534-
return getMoment(resource.metadata?.creationTimestamp).toString()
534+
return getISOStringTimestamp(resource.metadata?.creationTimestamp)
535535
}
536536
},
537537
sort: 'metadata.creationTimestamp',
@@ -645,7 +645,7 @@ export default function AdvancedConfiguration(props: AdvancedConfigurationPagePr
645645
sort: 'metadata.creationTimestamp',
646646
exportContent: (resource) => {
647647
if (resource.metadata?.creationTimestamp) {
648-
return getMoment(resource.metadata?.creationTimestamp).toString()
648+
return getISOStringTimestamp(resource.metadata?.creationTimestamp)
649649
}
650650
},
651651
},
@@ -697,7 +697,7 @@ export default function AdvancedConfiguration(props: AdvancedConfigurationPagePr
697697
sort: 'metadata.creationTimestamp',
698698
exportContent: (resource) => {
699699
if (resource.metadata?.creationTimestamp) {
700-
return getMoment(resource.metadata?.creationTimestamp).toString()
700+
return getISOStringTimestamp(resource.metadata?.creationTimestamp)
701701
}
702702
},
703703
},
@@ -761,7 +761,7 @@ export default function AdvancedConfiguration(props: AdvancedConfigurationPagePr
761761
sort: 'metadata.creationTimestamp',
762762
exportContent: (resource) => {
763763
if (resource.metadata?.creationTimestamp) {
764-
return getMoment(resource.metadata?.creationTimestamp).toString()
764+
return getISOStringTimestamp(resource.metadata?.creationTimestamp)
765765
}
766766
},
767767
},

‎frontend/src/routes/Applications/Overview.test.tsx

+43-31
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
/* Copyright Contributors to the Open Cluster Management project */
22

3-
import { render, screen } from '@testing-library/react'
3+
import { render, screen, waitFor } from '@testing-library/react'
44
import userEvent from '@testing-library/user-event'
55
import { MemoryRouter, Route, Routes } from 'react-router-dom-v5-compat'
66
import { RecoilRoot } from 'recoil'
77
import { managedClustersState, placementDecisionsState, subscriptionsState } from '../../atoms'
88
import {
9+
nockAggegateRequest,
910
nockIgnoreApiPaths,
1011
nockIgnoreRBAC,
1112
nockPostRequest,
1213
nockSearch,
13-
nockAggegateRequest,
1414
} from '../../lib/nock-util'
1515
import { defaultPlugin, PluginContext } from '../../lib/PluginContext'
16-
import { waitForText } from '../../lib/test-util'
16+
import { getCSVDownloadLink, getCSVExportSpies, waitForText } from '../../lib/test-util'
1717
import {
1818
ApplicationKind,
1919
ApplicationSetKind,
@@ -22,6 +22,8 @@ import {
2222
ManagedClusterKind,
2323
SubscriptionKind,
2424
} from '../../resources'
25+
import { getISOStringTimestamp } from '../../resources/utils'
26+
import { AcmToastGroup, AcmToastProvider } from '../../ui-components'
2527
import {
2628
acmExtension,
2729
mockApplication0,
@@ -112,26 +114,28 @@ describe('Applications Page', () => {
112114
snapshot.set(managedClustersState, mockClusters)
113115
}}
114116
>
115-
<MemoryRouter>
116-
<PluginContext.Provider
117-
value={{
118-
...defaultPlugin,
119-
acmExtensions: acmExtension,
120-
}}
121-
>
122-
<Routes>
123-
<Route path="*" element={<Overview />} />
124-
</Routes>
125-
</PluginContext.Provider>
126-
</MemoryRouter>
117+
<AcmToastProvider>
118+
<AcmToastGroup />
119+
<MemoryRouter>
120+
<PluginContext.Provider
121+
value={{
122+
...defaultPlugin,
123+
acmExtensions: acmExtension,
124+
}}
125+
>
126+
<Routes>
127+
<Route path="*" element={<Overview />} />
128+
</Routes>
129+
</PluginContext.Provider>
130+
</MemoryRouter>
131+
</AcmToastProvider>
127132
</RecoilRoot>
128133
)
129134
})
130135

131136
test('should display info', async () => {
132137
// wait for page to load
133138
await waitForText('feng-remote-argo8')
134-
135139
expect(screen.getByText(SubscriptionKind)).toBeTruthy()
136140
expect(screen.getByText(mockApplication0.metadata.namespace!)).toBeTruthy()
137141
expect(screen.getAllByText('Local')).toBeTruthy()
@@ -233,29 +237,37 @@ describe('Applications Page', () => {
233237
userEvent.click(screen.getByRole('button', { name: /close openshift/i }))
234238
})
235239

236-
test.skip('export button should produce a file for download', async () => {
237-
nockAggegateRequest('applications', fetchAggregate.req, applicationAggregate.res)
240+
test('export button should produce a file for download', async () => {
241+
nockAggegateRequest('applications', fetchAggregate.req, fetchAggregate.res)
238242

239243
await waitForText('feng-remote-argo8')
240244

241245
window.URL.createObjectURL = jest.fn()
242246
window.URL.revokeObjectURL = jest.fn()
243-
const documentBody = document.body.appendChild
244-
const documentCreate = document.createElement('a').dispatchEvent
245247

246-
const anchorMocked = { href: '', click: jest.fn(), download: 'table-values', style: { display: '' } } as any
247-
const createElementSpyOn = jest.spyOn(document, 'createElement').mockReturnValueOnce(anchorMocked)
248-
document.body.appendChild = jest.fn()
249-
document.createElement('a').dispatchEvent = jest.fn()
248+
const { blobConstructorSpy, createElementSpy } = getCSVExportSpies()
250249

251-
userEvent.click(screen.getByLabelText('export-search-result'))
250+
userEvent.click(screen.getByTestId('export-search-result'))
252251
userEvent.click(screen.getByText('Export all to CSV'))
253252

254-
expect(createElementSpyOn).toHaveBeenCalledWith('a')
255-
expect(anchorMocked.download).toContain('table-values')
256-
257-
document.body.appendChild = documentBody
258-
document.createElement('a').dispatchEvent = documentCreate
259-
await new Promise((resolve) => setTimeout(resolve, 1500))
253+
await waitFor(() => {
254+
const toastElement = screen.getByText(/Export successful/i)
255+
expect(toastElement).toBeInTheDocument()
256+
})
257+
258+
expect(blobConstructorSpy).toHaveBeenCalledWith(
259+
[
260+
'Name,Type,Namespace,Clusters,Repository,Health Status,Sync Status,Time window,Created\n' +
261+
`"application-0","Subscription","namespace-0","Local",-,-,-,-,"${getISOStringTimestamp(applicationAggregate.res.items[0].metadata?.creationTimestamp || '')}"\n` +
262+
'"applicationset-0","Application set","openshift-gitops","None","git",-,-,-,-\n' +
263+
'"applicationset-1","Application set","openshift-gitops","None","git",-,-,-,-\n' +
264+
'"argoapplication-1","Argo CD","argoapplication-1-ns","unknown","git",-,-,-,-\n' +
265+
'"feng-remote-argo8","Argo CD","argoapplication-1-ns","unknown","git",-,-,-,-\n' +
266+
'"authentication-operator","OpenShift","authentication-operator-ns","None",-,-,-,-,-\n' +
267+
'"authentication-operatorf","Flux","authentication-operator-ns","None",-,-,-,-,-',
268+
],
269+
{ type: 'text/csv' }
270+
)
271+
expect(getCSVDownloadLink(createElementSpy)?.value.download).toMatch(/^applicationsoverview-[\d]+\.csv$/)
260272
})
261273
})

‎frontend/src/routes/Applications/Overview.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ import {
7373
isResourceTypeOf,
7474
} from './helpers/resource-helper'
7575
import { isLocalSubscription } from './helpers/subscriptions'
76-
import { getMoment } from '../../resources/utils'
76+
import { getISOStringTimestamp } from '../../resources/utils'
7777

7878
const gitBranchAnnotationStr = 'apps.open-cluster-management.io/git-branch'
7979
const gitPathAnnotationStr = 'apps.open-cluster-management.io/git-path'
@@ -634,7 +634,7 @@ export default function ApplicationsOverview() {
634634
search: 'transformed.createdText',
635635
exportContent: (resource) => {
636636
if (resource.metadata?.creationTimestamp) {
637-
return getMoment(resource.metadata?.creationTimestamp).toString()
637+
return getISOStringTimestamp(resource.metadata?.creationTimestamp)
638638
}
639639
},
640640
},

‎frontend/src/routes/Credentials/CredentialsPage.test.tsx

+14-13
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
clickBulkAction,
1818
clickByLabel,
1919
clickByText,
20+
getCSVDownloadLink,
21+
getCSVExportSpies,
2022
selectTableRow,
2123
waitForNock,
2224
waitForNocks,
@@ -267,28 +269,27 @@ describe('provider connections page RBAC', () => {
267269
})
268270

269271
describe('Export from clusterpool table', () => {
270-
test.skip('export button should produce a file for download', async () => {
272+
test('export button should produce a file for download', async () => {
271273
nockGet(getSecrets1.req, getSecrets1.res) // get 'secrets' in 'provider-connection-namespace' namespace
272274
render(
273275
<TestProviderConnectionsPage providerConnections={[...mockProviderConnections, cloudRedHatProviderConnection]} />
274276
)
275277
window.URL.createObjectURL = jest.fn()
276278
window.URL.revokeObjectURL = jest.fn()
277-
const documentBody = document.body.appendChild
278-
const documentCreate = document.createElement('a').dispatchEvent
279-
280-
const anchorMocked = { href: '', click: jest.fn(), download: 'table-values', style: { display: '' } } as any
281-
const createElementSpyOn = jest.spyOn(document, 'createElement').mockReturnValueOnce(anchorMocked)
282-
document.body.appendChild = jest.fn()
283-
document.createElement('a').dispatchEvent = jest.fn()
279+
const { blobConstructorSpy, createElementSpy } = getCSVExportSpies()
284280

285281
await clickByLabel('export-search-result')
286282
await clickByText('Export all to CSV')
287283

288-
expect(createElementSpyOn).toHaveBeenCalledWith('a')
289-
expect(anchorMocked.download).toContain('table-values')
290-
291-
document.body.appendChild = documentBody
292-
document.createElement('a').dispatchEvent = documentCreate
284+
expect(blobConstructorSpy).toHaveBeenCalledWith(
285+
[
286+
'Name,Credential type,Namespace,Additional actions,Created\n' +
287+
'"provider-connection-1","Red Hat Ansible Automation Platform","provider-connection-namespace","-",-\n' +
288+
'"provider-connection-2","unknown","provider-connection-namespace","-","2024-06-28T03:06:13.000Z"\n' +
289+
'"ocm-api-token","Red Hat OpenShift Cluster Manager","ocm","Create cluster discovery",-',
290+
],
291+
{ type: 'text/csv' }
292+
)
293+
expect(getCSVDownloadLink(createElementSpy)?.value.download).toMatch(/^credentials-[\d]+\.csv$/)
293294
})
294295
})

‎frontend/src/routes/Credentials/CredentialsPage.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ import {
2929
SecretDefinition,
3030
unpackProviderConnection,
3131
} from '../../resources'
32-
import { deleteResource } from '../../resources/utils'
32+
import { deleteResource, getISOStringTimestamp } from '../../resources/utils'
3333
import AcmTimestamp from '../../lib/AcmTimestamp'
34-
import moment from 'moment'
3534

3635
export default function CredentialsPage() {
3736
const { secretsState, discoveryConfigState } = useSharedAtoms()
@@ -241,7 +240,7 @@ export function CredentialsTable(props: {
241240
),
242241
exportContent: (item: Secret) => {
243242
if (item.metadata.creationTimestamp) {
244-
return moment(new Date(item.metadata.creationTimestamp)).fromNow()
243+
return getISOStringTimestamp(item.metadata.creationTimestamp)
245244
}
246245
},
247246
},

‎frontend/src/routes/Governance/discovered/DiscoveredPolicies.test.tsx

+24-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as useFetchPolicies from './useFetchPolicies'
33
import DiscoveredPolicies from './DiscoveredPolicies'
44
import { getSourceFilterOptions } from './ByCluster/common'
55
import { fireEvent, render, screen, within } from '@testing-library/react'
6-
import { waitForText, waitForNotText } from '../../../lib/test-util'
6+
import { waitForText, waitForNotText, getCSVExportSpies, getCSVDownloadLink } from '../../../lib/test-util'
77
import { MemoryRouter } from 'react-router-dom-v5-compat'
88
import { ApolloError } from '@apollo/client'
99

@@ -667,4 +667,27 @@ describe('useFetchPolicies custom hook', () => {
667667
screen.getByRole('checkbox', { name: 'Kyverno Policy 1' }).click()
668668
screen.getByRole('checkbox', { name: 'Kyverno ClusterPolicy 1' }).click()
669669
})
670+
test('export button should produce a file for download', () => {
671+
render(
672+
<MemoryRouter>
673+
<DiscoveredPolicies />
674+
</MemoryRouter>
675+
)
676+
window.URL.createObjectURL = jest.fn()
677+
window.URL.revokeObjectURL = jest.fn()
678+
const { blobConstructorSpy, createElementSpy } = getCSVExportSpies()
679+
680+
screen.getByTestId('export-search-result').click()
681+
screen.getByText('Export all to CSV').click()
682+
683+
expect(blobConstructorSpy).toHaveBeenCalledWith(
684+
[
685+
'Name,Engine,Kind,Labels,Response action,Severity,Cluster violations,Source\n' +
686+
'"require-owner-labels","Kyverno","ClusterPolicy",-,"Audit","medium","no violations: 0 clusters, violations: 1 cluster, pending: 0 clusters, unknown: 0 clusters","Local"\n' +
687+
'"require-team-label","Kyverno","Policy",-,"Audit","critical","no violations: 1 cluster, violations: 1 cluster, pending: 0 clusters, unknown: 0 clusters","Local"',
688+
],
689+
{ type: 'text/csv' }
690+
)
691+
expect(getCSVDownloadLink(createElementSpy)?.value.download).toMatch(/^discoveredPolicies-[\d]+\.csv$/)
692+
})
670693
})

0 commit comments

Comments
 (0)
Failed to load comments.