diff --git a/.github/workflows/e2e.frontend.yaml b/.github/workflows/e2e.frontend.yaml index 968e52e8e0..434b1ac0c2 100644 --- a/.github/workflows/e2e.frontend.yaml +++ b/.github/workflows/e2e.frontend.yaml @@ -10,6 +10,9 @@ jobs: - feature/feature.spec.ts - groups/groups.spec.ts - projects/access.spec.ts + - projects/overview.spec.ts + # - projects/settings.spec.ts + - projects/notifications.spec.ts - segments/segments.spec.ts - import/import.spec.ts steps: diff --git a/frontend/cypress/integration/projects/notifications.spec.ts b/frontend/cypress/integration/projects/notifications.spec.ts index 64ca745c09..125c9b861b 100644 --- a/frontend/cypress/integration/projects/notifications.spec.ts +++ b/frontend/cypress/integration/projects/notifications.spec.ts @@ -113,7 +113,6 @@ describe('notifications', () => { //then cy.get("[data-testid='NOTIFICATIONS_MODAL']").should('exist'); - cy.get("[data-testid='UNREAD_NOTIFICATIONS']").should('not.exist'); const credentials = userCredentials[0]; diff --git a/frontend/cypress/integration/projects/overview.spec.ts b/frontend/cypress/integration/projects/overview.spec.ts new file mode 100644 index 0000000000..4d02dbafa0 --- /dev/null +++ b/frontend/cypress/integration/projects/overview.spec.ts @@ -0,0 +1,171 @@ +/// +import { + BATCH_ACTIONS_BAR, + BATCH_SELECT, + BATCH_SELECTED_COUNT, + MORE_BATCH_ACTIONS, + SEARCH_INPUT, +} from '../../../src/utils/testIds'; + +const randomId = String(Math.random()).split('.')[1]; +const featureTogglePrefix = 'unleash-e2e-project-overview'; +const featureToggleName = `${featureTogglePrefix}-${randomId}`; +const baseUrl = Cypress.config().baseUrl; +const selectAll = '[title="Toggle All Rows Selected"] input[type="checkbox"]'; + +// Disable the prod guard modal by marking it as seen. +const disableFeatureStrategiesProdGuard = () => { + localStorage.setItem( + 'useFeatureStrategyProdGuardSettings:v2', + JSON.stringify({ hide: true }) + ); +}; + +// Disable all active splash pages by visiting them. +const disableActiveSplashScreens = () => { + cy.visit(`/splash/operators`); +}; + +describe('project overview', () => { + before(() => { + disableFeatureStrategiesProdGuard(); + disableActiveSplashScreens(); + cy.login(); + cy.request({ + url: '/api/admin/projects/default/features', + method: 'POST', + body: { + name: `${featureToggleName}-A`, + description: 'hello-world', + type: 'release', + impressionData: false, + }, + }); + cy.request({ + url: '/api/admin/projects/default/features', + method: 'POST', + body: { + name: `${featureToggleName}-B`, + description: 'hello-world', + type: 'release', + impressionData: false, + }, + }); + }); + + beforeEach(() => { + cy.login(); + if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { + cy.get("[data-testid='CLOSE_SPLASH']").click(); + } + }); + + after(() => { + cy.request({ + method: 'DELETE', + url: `${baseUrl}/api/admin/features/${featureToggleName}-A`, + failOnStatusCode: false, + }); + cy.request({ + method: 'DELETE', + url: `${baseUrl}/api/admin/features/${featureToggleName}-B`, + failOnStatusCode: false, + }); + cy.request({ + method: 'DELETE', + url: `${baseUrl}/api/admin/archive/${featureToggleName}-A`, + }); + cy.request({ + method: 'DELETE', + url: `${baseUrl}/api/admin/archive/${featureToggleName}-B`, + }); + }); + + it('loads the table', () => { + cy.visit('/projects/default'); + + // Use search to filter feature toggles and check that the feature toggle is listed in the table. + cy.get("[data-testid='SEARCH_INPUT']").click().type(featureToggleName); + cy.get('table').contains('td', `${featureToggleName}-A`); + cy.get('table tbody tr').should('have.length', 2); + }); + + it('can select and deselect feature toggles', () => { + cy.visit('/projects/default'); + cy.viewport(1920, 1080); + cy.get("[data-testid='SEARCH_INPUT']").click().type(featureToggleName); + cy.get('table tbody tr').should('have.length', 2); + const counter = `[data-testid="${BATCH_SELECTED_COUNT}"]`; + + cy.get(counter).should('not.exist'); + cy.get(selectAll).click(); + cy.get(counter).contains('2'); + cy.get(selectAll).click(); + cy.get(counter).should('not.exist'); + + cy.get('table td') + .contains(`${featureToggleName}-A`) + .closest('tr') + .find(`[data-testid="${BATCH_SELECT}"] input[type="checkbox"]`) + .click(); + cy.get(counter).contains('1'); + + cy.get('table td') + .contains(`${featureToggleName}-A`) + .closest('tr') + .find(`[data-testid="${BATCH_SELECT}"] input[type="checkbox"]`) + .click(); + cy.get(counter).should('not.exist'); + cy.get('table td') + .contains(`${featureToggleName}-B`) + .closest('tr') + .find(`[data-testid="${BATCH_SELECT}"] input[type="checkbox"]`) + .click(); + cy.get(counter).contains('1'); + + cy.get('table td') + .contains(`${featureToggleName}-A`) + .closest('tr') + .find(`[data-testid="${BATCH_SELECT}"] input[type="checkbox"]`) + .click(); + cy.get(counter).contains('2'); + cy.get('table td') + .contains(`${featureToggleName}-B`) + .closest('tr') + .find(`[data-testid="${BATCH_SELECT}"] input[type="checkbox"]`) + .click(); + cy.get(counter).contains('1'); + }); + + it('can mark selected togggles as stale', () => { + cy.visit('/projects/default'); + cy.viewport(1920, 1080); + cy.get(`[data-testid='${SEARCH_INPUT}']`).click().type(featureToggleName); + cy.get('table tbody tr').should('have.length', 2); + cy.get(selectAll).click(); + + cy.get(`[data-testid="${MORE_BATCH_ACTIONS}"]`).click(); + + cy.get('[role="menuitem"]').contains('Mark as stale').click(); + + cy.visit(`/projects/default/features/${featureToggleName}-A`); + cy.get('[title="Feature toggle is deprecated."]').should('exist'); + }); + + it('can archive selected togggles', () => { + cy.visit('/projects/default'); + cy.viewport(1920, 1080); + cy.get(`[data-testid='${SEARCH_INPUT}']`).click().type(featureToggleName); + cy.get('table tbody tr').should('have.length', 2); + cy.get(selectAll).click(); + + cy.get(`[data-testid=${BATCH_ACTIONS_BAR}] button`) + .contains('Archive') + .click(); + cy.get('p') + .contains('Are you sure you want to archive 2 feature toggles?') + .should('exist'); + cy.get('button').contains('Archive toggles').click(); + cy.get('table tbody tr').should('have.length', 0); + }); +}); diff --git a/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx b/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx index 7fc0c7cca1..2fec1c34c3 100644 --- a/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx +++ b/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx @@ -1,5 +1,6 @@ import { FC } from 'react'; import { Box, Paper, styled, Typography } from '@mui/material'; +import { BATCH_ACTIONS_BAR, BATCH_SELECTED_COUNT } from 'utils/testIds'; interface IBatchSelectionActionsBarProps { count: number; @@ -56,11 +57,13 @@ export const BatchSelectionActionsBar: FC = ({ } return ( - + - {count} + + {count} +  selected {children} diff --git a/frontend/src/component/common/Search/Search.tsx b/frontend/src/component/common/Search/Search.tsx index 92bf3823c3..8e8a2fd38d 100644 --- a/frontend/src/component/common/Search/Search.tsx +++ b/frontend/src/component/common/Search/Search.tsx @@ -6,6 +6,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import { SearchSuggestions } from './SearchSuggestions/SearchSuggestions'; import { IGetSearchContextOutput } from 'hooks/useSearch'; import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut'; +import { SEARCH_INPUT } from 'utils/testIds'; interface ISearchProps { initialValue?: string; @@ -108,7 +109,10 @@ export const Search = ({ onSearchChange(e.target.value)} onFocus={() => setShowSuggestions(true)} diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/MoreActions.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/MoreActions.tsx index 8a1fbc445b..117202dc05 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/MoreActions.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/MoreActions.tsx @@ -19,6 +19,7 @@ import useProject from 'hooks/api/getters/useProject/useProject'; import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; +import { MORE_BATCH_ACTIONS } from 'utils/testIds'; interface IMoreActionsProps { projectId: string; @@ -89,7 +90,7 @@ export const MoreActions: VFC = ({ projectId, data }) => { return ( <> - + = ({ projectId, data }) => { aria-expanded={open ? 'true' : undefined} onClick={handleClick} type="button" + data-testid={MORE_BATCH_ACTIONS} > diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell.tsx index eaac3ba5ff..fa77f40c73 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell.tsx @@ -1,5 +1,6 @@ import { Box, Checkbox, styled } from '@mui/material'; import { FC } from 'react'; +import { BATCH_SELECT } from 'utils/testIds'; interface IRowSelectCellProps { onChange: () => void; @@ -18,7 +19,7 @@ export const RowSelectCell: FC = ({ checked, title, }) => ( - + ); diff --git a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap index 527faf5644..19c6820f8c 100644 --- a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap +++ b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap @@ -55,6 +55,7 @@ exports[`renders an empty list correctly 1`] = `