diff --git a/.github/workflows/e2e.frontend.yaml b/.github/workflows/e2e.frontend.yaml index c7eda419f3..968e52e8e0 100644 --- a/.github/workflows/e2e.frontend.yaml +++ b/.github/workflows/e2e.frontend.yaml @@ -11,6 +11,7 @@ jobs: - groups/groups.spec.ts - projects/access.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 new file mode 100644 index 0000000000..a74ccd2965 --- /dev/null +++ b/frontend/cypress/integration/import/import.spec.ts @@ -0,0 +1,125 @@ +/// + +const baseUrl = Cypress.config().baseUrl; +const randomSeed = String(Math.random()).split('.')[1]; +const randomFeatureName = `cypress-features${randomSeed}`; +const userIds: any[] = []; + +// Disable all active splash pages by visiting them. +const disableActiveSplashScreens = () => { + cy.visit(`/splash/operators`); +}; + +describe('imports', () => { + before(() => { + disableActiveSplashScreens(); + cy.login(); + 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(); + 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(); + + 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'); + + // 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.contains('Import completed'); + + cy.visit(`/projects/default/features/${randomFeatureName}`); + cy.contains('enabled in development'); + cy.contains('50%'); + }); +}); diff --git a/frontend/src/component/project/Project/Import/ImportModal.tsx b/frontend/src/component/project/Project/Import/ImportModal.tsx index cef316970b..582c1d9959 100644 --- a/frontend/src/component/project/Project/Import/ImportModal.tsx +++ b/frontend/src/component/project/Project/Import/ImportModal.tsx @@ -14,6 +14,11 @@ import { import { ValidationStage } from './validate/ValidationStage'; import { ImportStage } from './import/ImportStage'; import { ImportOptions } from './configure/ImportOptions'; +import { + IMPORT_BUTTON, + IMPORT_CONFIGURATION_BUTTON, + VALIDATE_BUTTON, +} from '../../../../utils/testIds'; const ModalContentContainer = styled('div')(({ theme }) => ({ minHeight: '100vh', diff --git a/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx b/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx index f8638a6ec0..f0f2cff1c6 100644 --- a/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx +++ b/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx @@ -16,6 +16,11 @@ import React, { FC, ReactNode, useState } from 'react'; import useToast from 'hooks/useToast'; import { ImportLayoutContainer } from '../ImportLayoutContainer'; import { ActionsContainer } from '../ActionsContainer'; +import { + CODE_EDITOR_TAB, + CODE_TEXT_FIELD, + VALIDATE_BUTTON, +} from 'utils/testIds'; const StyledTextField = styled(TextField)(({ theme }) => ({ width: '100%', @@ -58,6 +63,7 @@ export const ConfigurationTabs: FC<{ setActiveTab('code')} /> @@ -119,6 +125,7 @@ export const ImportArea: FC<{ variant="outlined" onChange={event => setImportPayload(event.target.value)} value={importPayload} + data-testid={CODE_TEXT_FIELD} multiline minRows={13} maxRows={13} @@ -139,6 +146,7 @@ export const Actions: FC<{ variant="contained" type="submit" onClick={onSubmit} + data-testid={VALIDATE_BUTTON} disabled={disabled} > Validate diff --git a/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx b/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx index afe7952213..0a73810a4f 100644 --- a/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx +++ b/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx @@ -3,6 +3,7 @@ import { KeyboardArrowDownOutlined } from '@mui/icons-material'; import React, { FC, useEffect } from 'react'; import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments'; import { Box, styled, Typography } from '@mui/material'; +import { IMPORT_ENVIRONMENT } from 'utils/testIds'; const ImportOptionsContainer = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.secondaryContainer, @@ -57,6 +58,7 @@ export const ImportOptions: FC = ({ onChange={onChange} label={'Environment'} value={environment} + data-testid={IMPORT_ENVIRONMENT} IconComponent={KeyboardArrowDownOutlined} fullWidth /> diff --git a/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx b/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx index 68468efc02..355f1a314c 100644 --- a/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx +++ b/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx @@ -9,6 +9,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; import { ActionsContainer } from '../ActionsContainer'; +import { IMPORT_CONFIGURATION_BUTTON } from 'utils/testIds'; const ImportInfoContainer = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.secondaryContainer, @@ -191,6 +192,7 @@ export const ValidationStage: FC<{ variant="contained" type="submit" onClick={onSubmit} + data-testid={IMPORT_CONFIGURATION_BUTTON} disabled={validationResult.errors.length > 0 || !validJSON} > Import configuration diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx index 027a449ec5..c8e4439480 100644 --- a/frontend/src/component/project/Project/Project.tsx +++ b/frontend/src/component/project/Project/Project.tsx @@ -41,6 +41,7 @@ import { ProjectChangeRequests } from '../../changeRequest/ProjectChangeRequests import { ProjectSettings } from './ProjectSettings/ProjectSettings'; import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi'; import { ImportModal } from './Import/ImportModal'; +import { IMPORT_BUTTON } from 'utils/testIds'; export const Project = () => { const projectId = useRequiredPathParam('projectId'); @@ -150,6 +151,7 @@ export const Project = () => { }} onClick={() => setModalOpen(true)} tooltipProps={{ title: 'Import' }} + data-testid={IMPORT_BUTTON} data-loading > diff --git a/frontend/src/utils/testIds.ts b/frontend/src/utils/testIds.ts index 39bfd2467d..c1f3133683 100644 --- a/frontend/src/utils/testIds.ts +++ b/frontend/src/utils/testIds.ts @@ -75,3 +75,12 @@ export const ANNOUNCER_ELEMENT_TEST_ID = 'ANNOUNCER_ELEMENT_TEST_ID'; export const INSTANCE_STATUS_BAR_ID = 'INSTANCE_STATUS_BAR_ID'; export const TOAST_TEXT = 'TOAST_TEXT'; export const LARGE_NUMBER_PRETTIFIED = 'LARGE_NUMBER_PRETTIFIED'; + +/* EXPORT/IMPORT FEATURES */ + +export const IMPORT_BUTTON = 'IMPORT_BUTTON'; +export const CODE_EDITOR_TAB = 'CODE_EDITOR_TAB'; +export const IMPORT_ENVIRONMENT = 'IMPORT_ENVIRONMENT'; +export const CODE_TEXT_FIELD = 'CODE_TEXT_FIELD'; +export const VALIDATE_BUTTON = 'VALIDATE_BUTTON'; +export const IMPORT_CONFIGURATION_BUTTON = 'IMPORT_CONFIGURATION_BUTTON';