From d01100fbf0ab29ac5dffde09d52c79ff6a22b5d1 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Mon, 6 May 2024 14:40:47 +0200 Subject: [PATCH] test: move import test from cypress to RTL to make it less flaky (#6982) --- .github/workflows/e2e.frontend.yaml | 1 - .../cypress/integration/import/import.spec.ts | 136 ---------------- .../project/Project/Import/Import.test.tsx | 151 ++++++++++++++++++ .../Project/Import/configure/FileDropZone.tsx | 2 +- package.json | 7 +- 5 files changed, 158 insertions(+), 139 deletions(-) delete mode 100644 frontend/cypress/integration/import/import.spec.ts create mode 100644 frontend/src/component/project/Project/Import/Import.test.tsx diff --git a/.github/workflows/e2e.frontend.yaml b/.github/workflows/e2e.frontend.yaml index 3d1649ddc8..be5cb725d3 100644 --- a/.github/workflows/e2e.frontend.yaml +++ b/.github/workflows/e2e.frontend.yaml @@ -12,7 +12,6 @@ jobs: - projects/access.spec.ts - projects/overview.spec.ts - segments/segments.spec.ts - - import/import.spec.ts steps: - name: Dump GitHub context env: diff --git a/frontend/cypress/integration/import/import.spec.ts b/frontend/cypress/integration/import/import.spec.ts deleted file mode 100644 index 8f0f774772..0000000000 --- a/frontend/cypress/integration/import/import.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -/// - -describe('imports', () => { - const baseUrl = Cypress.config().baseUrl; - const randomSeed = String(Math.random()).split('.')[1]; - const randomFeatureName = `cypress-features${randomSeed}`; - const userIds: any[] = []; - - before(() => { - cy.runBefore(); - cy.login_UI(); - for (let i = 1; i <= 2; i++) { - cy.request('POST', `${baseUrl}/api/admin/user-admin`, { - name: `unleash-e2e-user${i}-${randomFeatureName}`, - email: `unleash-e2e-user${i}-${randomFeatureName}@test.com`, - sendEmail: false, - rootRole: 3, - }).then((response) => userIds.push(response.body.id)); - } - }); - - after(() => { - userIds.forEach((id) => - cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`), - ); - }); - - beforeEach(() => { - cy.login_UI(); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } - }); - - it('can import data', () => { - cy.visit('/projects/default'); - cy.get("[data-testid='IMPORT_BUTTON']").click({ force: true }); - - const exportText = { - features: [ - { - name: randomFeatureName, - description: '', - type: 'release', - project: 'default', - stale: false, - impressionData: false, - archived: false, - }, - ], - featureStrategies: [ - { - name: 'flexibleRollout', - id: '14a0d9dd-2b5d-4a21-98fd-ede72bda0328', - featureName: randomFeatureName, - parameters: { - groupId: randomFeatureName, - rollout: '50', - stickiness: 'default', - }, - constraints: [], - segments: [], - }, - ], - featureEnvironments: [ - { - enabled: true, - featureName: randomFeatureName, - environment: 'test', - variants: [], - name: randomFeatureName, - }, - ], - contextFields: [], - featureTags: [ - { - featureName: randomFeatureName, - tagType: 'simple', - tagValue: 'best-tag', - }, - { - featureName: randomFeatureName, - tagType: 'simple', - tagValue: 'rserw', - }, - { - featureName: randomFeatureName, - tagType: 'simple', - tagValue: 'FARO', - }, - ], - segments: [], - tagTypes: [ - { - name: 'simple', - description: 'Used to simplify filtering of features', - icon: '#', - }, - ], - }; - - cy.get("[data-testid='VALIDATE_BUTTON']").should('be.disabled'); - - cy.intercept('POST', '/api/admin/features-batch/import').as( - 'featureImported', - ); - - // cypress can only work with input@file that is visible - cy.get('input[type=file]') - .invoke('attr', 'style', 'display: block') - .selectFile({ - contents: Cypress.Buffer.from(JSON.stringify(exportText)), - fileName: 'upload.json', - lastModified: Date.now(), - }); - cy.get("[data-testid='VALIDATE_BUTTON']").click(); - cy.get("[data-testid='IMPORT_CONFIGURATION_BUTTON']").click(); - - cy.wait('@featureImported'); - cy.contains('Import completed'); - - cy.request({ - url: `/api/admin/projects/default/features/${randomFeatureName}`, - headers: { 'Content-Type': 'application/json' }, - }).then((response) => { - expect(response.body.name).to.equal(randomFeatureName); - const devEnv = response.body.environments.find( - (env: any) => env.name === 'development', - ); - - expect(devEnv.name).to.equal('development'); - expect(devEnv.strategies[0].parameters.rollout).to.equal('50'); - expect(devEnv.enabled).to.equal(true); - }); - }); -}); diff --git a/frontend/src/component/project/Project/Import/Import.test.tsx b/frontend/src/component/project/Project/Import/Import.test.tsx new file mode 100644 index 0000000000..0cf050b77b --- /dev/null +++ b/frontend/src/component/project/Project/Import/Import.test.tsx @@ -0,0 +1,151 @@ +import { render } from 'utils/testRenderer'; +import { screen, waitFor } from '@testing-library/react'; +import { ImportModal } from './ImportModal'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; +import userEvent from '@testing-library/user-event'; +import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions'; + +const server = testServerSetup(); + +const setupApi = () => { + testServerRoute(server, '/api/admin/ui-config', { + versionInfo: { + current: { enterprise: 'present' }, + }, + }); + testServerRoute(server, '/api/admin/projects/default', { + environments: [ + { environment: 'development' }, + { environment: 'production' }, + ], + }); + testServerRoute( + server, + '/api/admin/features-batch/validate', + { errors: [], permissions: [], warnings: [] }, + 'post', + ); + testServerRoute(server, '/api/admin/features-batch/import', {}, 'post'); +}; + +const importFile = async (content: string) => { + const selectFileInput = screen.getByTestId('import-file'); + const importFile = new File([content], 'import.json', { + type: 'application/json', + }); + userEvent.upload(selectFileInput, importFile); +}; + +test('Import happy path', async () => { + setupApi(); + let closed = false; + const setOpen = (open: boolean) => { + closed = !open; + }; + render(, { + permissions: [{ permission: CREATE_FEATURE }], + }); + + // configure stage + screen.getByText('Import options'); + screen.getByText('Drop your file here'); + const validateButton = screen.getByText('Validate'); + expect(validateButton).toBeDisabled(); + + await importFile('{}'); + await waitFor(() => { + expect(screen.getByText('Validate')).toBeEnabled(); + }); + + const codeEditorLabel = screen.getByText('Code editor'); + codeEditorLabel.click(); + const editor = screen.getByLabelText('Exported toggles'); + expect(editor.textContent).toBe('{}'); + + screen.getByText('Validate').click(); + + // validate stage + screen.getByText('You are importing this configuration in:'); + screen.getByText('development'); + screen.getByText('default'); + const importButton = screen.getByText('Import configuration'); + expect(importButton).toBeEnabled(); + importButton.click(); + + // import stage + await screen.findByText('Importing...'); + await screen.findByText('Import completed'); + + expect(closed).toBe(false); + const closeButton = screen.getByText('Close'); + closeButton.click(); + expect(closed).toBe(true); +}); + +test('Block when importing non json content', async () => { + setupApi(); + const setOpen = () => {}; + render(, { + permissions: [{ permission: CREATE_FEATURE }], + }); + + const codeEditorLabel = screen.getByText('Code editor'); + codeEditorLabel.click(); + const editor = screen.getByLabelText('Exported toggles'); + userEvent.type(editor, 'invalid non json'); + + const validateButton = screen.getByText('Validate'); + expect(validateButton).toBeDisabled(); +}); + +test('Show validation errors', async () => { + setupApi(); + testServerRoute( + server, + '/api/admin/features-batch/validate', + { + errors: [ + { message: 'error message', affectedItems: ['itemC', 'itemD'] }, + ], + permissions: [ + { + message: 'permission message', + affectedItems: ['itemE', 'itemF'], + }, + ], + warnings: [ + { + message: 'warning message', + affectedItems: ['itemA', 'itemB'], + }, + ], + }, + 'post', + ); + const setOpen = () => {}; + render(, { + permissions: [{ permission: CREATE_FEATURE }], + }); + + await importFile('{}'); + await waitFor(() => { + expect(screen.getByText('Validate')).toBeEnabled(); + }); + + screen.getByText('Validate').click(); + + await screen.findByText('warning message'); + await screen.findByText('itemA'); + await screen.findByText('itemB'); + + await screen.findByText('error message'); + await screen.findByText('itemC'); + await screen.findByText('itemD'); + + await screen.findByText('permission message'); + await screen.findByText('itemE'); + await screen.findByText('itemF'); + + const importButton = screen.getByText('Import configuration'); + expect(importButton).toBeDisabled(); +}); diff --git a/frontend/src/component/project/Project/Import/configure/FileDropZone.tsx b/frontend/src/component/project/Project/Import/configure/FileDropZone.tsx index d2ac9200bd..8964c6ffed 100644 --- a/frontend/src/component/project/Project/Import/configure/FileDropZone.tsx +++ b/frontend/src/component/project/Project/Import/configure/FileDropZone.tsx @@ -52,7 +52,7 @@ export const FileDropZone: FC = ({ return ( - + {children} ); diff --git a/package.json b/package.json index 6225f08071..940ce52246 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,12 @@ "^.+\\.tsx?$": ["@swc/jest"] }, "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", - "testPathIgnorePatterns": ["/dist/", "/node_modules/", "/frontend/"], + "testPathIgnorePatterns": [ + "/dist/", + "/node_modules/", + "/frontend/", + "/website/" + ], "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"], "coveragePathIgnorePatterns": [ "/node_modules/",