diff --git a/.github/workflows/e2e.frontend.yaml b/.github/workflows/e2e.frontend.yaml index 434b1ac0c2..2b3917ced6 100644 --- a/.github/workflows/e2e.frontend.yaml +++ b/.github/workflows/e2e.frontend.yaml @@ -11,7 +11,7 @@ jobs: - groups/groups.spec.ts - projects/access.spec.ts - projects/overview.spec.ts - # - projects/settings.spec.ts + - projects/settings.spec.ts - projects/notifications.spec.ts - segments/segments.spec.ts - import/import.spec.ts diff --git a/frontend/cypress.d.ts b/frontend/cypress.d.ts new file mode 100644 index 0000000000..d12adb035f --- /dev/null +++ b/frontend/cypress.d.ts @@ -0,0 +1,5 @@ +/// + +declare namespace Cypress { + interface Chainable {} +} diff --git a/frontend/cypress/README.md b/frontend/cypress/README.md new file mode 100644 index 0000000000..edd06efe20 --- /dev/null +++ b/frontend/cypress/README.md @@ -0,0 +1,36 @@ +## Unleash Behavioural tests + +### Add common commands to Cypress + +- `global.d.ts` is where we extend Cypress types +- `API.ts` contains api requests for common actions (great place for cleanup actions) +- `UI.ts` contains common functions for UI operations +- `commands.ts` is the place to map the functions to a cypress command + +### Test Format + +Ideally each test should manage its own data. + +Avoid using `after` and `afterEach` hooks for cleaning up. According to Cypress docs, there is no guarantee that the functions will run + +Suggested Format: + +- `prepare` +- `when` +- `then` +- `clean` + +#### Passing (returned) parameters around + +```ts +it('can add, update and delete a gradual rollout strategy to the development environment', async () => { + cy.addFlexibleRolloutStrategyToFeature_UI({ + featureToggleName, + }).then(value => { + strategyId = value; + cy.updateFlexibleRolloutStrategy_UI(featureToggleName, strategyId).then( + () => cy.deleteFeatureStrategy_UI(featureToggleName, strategyId) + ); + }); +}); +``` diff --git a/frontend/cypress/global.d.ts b/frontend/cypress/global.d.ts new file mode 100644 index 0000000000..b12c6349b9 --- /dev/null +++ b/frontend/cypress/global.d.ts @@ -0,0 +1,81 @@ +/// + +declare namespace Cypress { + interface AddFlexibleRolloutStrategyOptions { + featureToggleName: string; + project?: string; + environment?: string; + stickiness?: string; + } + + interface UserCredentials { + email: string; + password: string; + } + interface Chainable { + runBefore(): Chainable; + + login_UI(user = AUTH_USER, password = AUTH_PASSWORD): Chainable; + logout_UI(): Chainable; + + createProject_UI( + projectName: string, + defaultStickiness: string + ): Chainable; + + createFeature_UI( + name: string, + shouldWait?: boolean, + project?: string + ): Chainable; + + // VARIANTS + addVariantsToFeature_UI( + featureToggleName: string, + variants: Array, + projectName?: string + ); + deleteVariant_UI( + featureToggleName: string, + variant: string, + projectName?: string + ): Chainable; + + // SEGMENTS + createSegment_UI(segmentName: string): Chainable; + deleteSegment_UI(segmentName: string, id: string): Chainable; + + // STRATEGY + addUserIdStrategyToFeature_UI( + featureName: string, + strategyId: string, + projectName?: string + ): Chainable; + addFlexibleRolloutStrategyToFeature_UI( + options: AddFlexibleRolloutStrategyOptions + ): Chainable; + updateFlexibleRolloutStrategy_UI( + featureToggleName: string, + strategyId: string + ); + deleteFeatureStrategy_UI( + featureName: string, + strategyId: string, + shouldWait?: boolean, + projectName?: string + ): Chainable; + + // API + createUser_API(userName: string, role: number): Chainable; + updateUserPassword_API(id: number, pass?: string): Chainable; + addUserToProject_API( + id: number, + role: number, + projectName?: string + ): Chainable; + createProject_API(name: string): Chainable; + deleteProject_API(name: string): Chainable; + createFeature_API(name: string, projectName: string): Chainable; + deleteFeature_API(name: string): Chainable; + } +} diff --git a/frontend/cypress/integration/feature/feature.spec.ts b/frontend/cypress/integration/feature/feature.spec.ts index 2160b679e8..24c270a089 100644 --- a/frontend/cypress/integration/feature/feature.spec.ts +++ b/frontend/cypress/integration/feature/feature.spec.ts @@ -1,269 +1,76 @@ -/// +/// const ENTERPRISE = Boolean(Cypress.env('ENTERPRISE')); const randomId = String(Math.random()).split('.')[1]; const featureToggleName = `unleash-e2e-${randomId}`; -const baseUrl = Cypress.config().baseUrl; + const variant1 = 'variant1'; const variant2 = 'variant2'; let strategyId = ''; -// 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('feature', () => { before(() => { - disableFeatureStrategiesProdGuard(); - disableActiveSplashScreens(); + cy.runBefore(); }); after(() => { - cy.request({ - method: 'DELETE', - url: `${baseUrl}/api/admin/features/${featureToggleName}`, - }); - cy.request({ - method: 'DELETE', - url: `${baseUrl}/api/admin/archive/${featureToggleName}`, - }); + cy.deleteFeature_API(featureToggleName); }); beforeEach(() => { - cy.login(); + cy.login_UI(); cy.visit('/features'); }); it('can create a feature toggle', () => { - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } - - cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); - - cy.intercept('POST', '/api/admin/projects/default/features').as( - 'createFeature' - ); - - cy.get("[data-testid='CF_NAME_ID'").type(featureToggleName); - cy.get("[data-testid='CF_DESC_ID'").type('hello-world'); - cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); - cy.wait('@createFeature'); + cy.createFeature_UI(featureToggleName, true); cy.url().should('include', featureToggleName); }); it('gives an error if a toggle exists with the same name', () => { - cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); - - cy.intercept('POST', '/api/admin/projects/default/features').as( - 'createFeature' - ); - - cy.get("[data-testid='CF_NAME_ID'").type(featureToggleName); - cy.get("[data-testid='CF_DESC_ID'").type('hello-world'); - cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); + cy.createFeature_UI(featureToggleName, false); cy.get("[data-testid='INPUT_ERROR_TEXT']").contains( 'A toggle with that name already exists' ); }); it('gives an error if a toggle name is url unsafe', () => { - cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); - - cy.intercept('POST', '/api/admin/projects/default/features').as( - 'createFeature' - ); - - cy.get("[data-testid='CF_NAME_ID'").type('featureToggleUnsafe####$#//'); - cy.get("[data-testid='CF_DESC_ID'").type('hello-world'); - cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); + cy.createFeature_UI('featureToggleUnsafe####$#//', false); cy.get("[data-testid='INPUT_ERROR_TEXT']").contains( `"name" must be URL friendly` ); }); - it('can add a gradual rollout strategy to the development environment', () => { - cy.visit( - `/projects/default/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=flexibleRollout` - ); - - if (ENTERPRISE) { - cy.get('[data-testid=ADD_CONSTRAINT_ID]').click(); - cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); - } - - cy.intercept( - 'POST', - `/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`, - req => { - expect(req.body.name).to.equal('flexibleRollout'); - expect(req.body.parameters.groupId).to.equal(featureToggleName); - expect(req.body.parameters.stickiness).to.equal('default'); - expect(req.body.parameters.rollout).to.equal('50'); - - if (ENTERPRISE) { - expect(req.body.constraints.length).to.equal(1); - } else { - expect(req.body.constraints.length).to.equal(0); - } - - req.continue(res => { - strategyId = res.body.id; - }); - } - ).as('addStrategyToFeature'); - - cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click(); - cy.wait('@addStrategyToFeature'); - }); - - it('can update a strategy in the development environment', () => { - cy.visit( - `/projects/default/features/${featureToggleName}/strategies/edit?environmentId=development&strategyId=${strategyId}` - ); - - cy.get('[data-testid=FLEXIBLE_STRATEGY_STICKINESS_ID]') - .first() - .click() - .get('[data-testid=SELECT_ITEM_ID-sessionId') - .first() - .click(); - - cy.get('[data-testid=FLEXIBLE_STRATEGY_GROUP_ID]') - .first() - .clear() - .type('new-group-id'); - - cy.intercept( - 'PUT', - `/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies/${strategyId}`, - req => { - expect(req.body.parameters.groupId).to.equal('new-group-id'); - expect(req.body.parameters.stickiness).to.equal('sessionId'); - expect(req.body.parameters.rollout).to.equal('50'); - - if (ENTERPRISE) { - expect(req.body.constraints.length).to.equal(1); - } else { - expect(req.body.constraints.length).to.equal(0); - } - - req.continue(res => { - expect(res.statusCode).to.equal(200); - }); - } - ).as('updateStrategy'); - - cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click(); - cy.wait('@updateStrategy'); - }); - - it('can delete a strategy in the development environment', () => { - cy.visit(`/projects/default/features/${featureToggleName}`); - - cy.intercept( - 'DELETE', - `/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies/${strategyId}`, - req => { - req.continue(res => { - expect(res.statusCode).to.equal(200); - }); - } - ).as('deleteStrategy'); - - cy.get( - '[data-testid=FEATURE_ENVIRONMENT_ACCORDION_development]' - ).click(); - cy.get('[data-testid=STRATEGY_FORM_REMOVE_ID]').click(); - cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); - cy.wait('@deleteStrategy'); + it('can add, update and delete a gradual rollout strategy to the development environment', async () => { + cy.addFlexibleRolloutStrategyToFeature_UI({ + featureToggleName, + }).then(value => { + strategyId = value; + cy.updateFlexibleRolloutStrategy_UI( + featureToggleName, + strategyId + ).then(() => + cy.deleteFeatureStrategy_UI(featureToggleName, strategyId) + ); + }); }); it('can add a userId strategy to the development environment', () => { - cy.visit( - `/projects/default/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=userWithId` + cy.addUserIdStrategyToFeature_UI(featureToggleName, strategyId).then( + value => { + cy.deleteFeatureStrategy_UI(featureToggleName, value, false); + } ); - - if (ENTERPRISE) { - cy.get('[data-testid=ADD_CONSTRAINT_ID]').click(); - cy.get('[data-testid=CONSTRAINT_AUTOCOMPLETE_ID]') - .type('{downArrow}'.repeat(1)) - .type('{enter}'); - cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); - } - - cy.get('[data-testid=STRATEGY_INPUT_LIST]') - .type('user1') - .type('{enter}') - .type('user2') - .type('{enter}'); - cy.get('[data-testid=ADD_TO_STRATEGY_INPUT_LIST]').click(); - - cy.intercept( - 'POST', - `/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`, - req => { - expect(req.body.name).to.equal('userWithId'); - - expect(req.body.parameters.userIds.length).to.equal(11); - - if (ENTERPRISE) { - expect(req.body.constraints.length).to.equal(1); - } else { - expect(req.body.constraints.length).to.equal(0); - } - - req.continue(res => { - strategyId = res.body.id; - }); - } - ).as('addStrategyToFeature'); - - cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click(); - cy.wait('@addStrategyToFeature'); }); - it('can add two variants to the development environment', () => { - cy.visit(`/projects/default/features/${featureToggleName}/variants`); - - cy.intercept( - 'PATCH', - `/api/admin/projects/default/features/${featureToggleName}/environments/development/variants`, - req => { - expect(req.body[0].op).to.equal('add'); - expect(req.body[0].path).to.equal('/0'); - expect(req.body[0].value.name).to.equal(variant1); - expect(req.body[0].value.weight).to.equal(500); - expect(req.body[1].op).to.equal('add'); - expect(req.body[1].path).to.equal('/1'); - expect(req.body[1].value.name).to.equal(variant2); - expect(req.body[1].value.weight).to.equal(500); - } - ).as('variantCreation'); - - cy.get('[data-testid=ADD_VARIANT_BUTTON]').first().click(); - cy.wait(1000); - cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variant1); - cy.get('[data-testid=MODAL_ADD_VARIANT_BUTTON]').click(); - cy.get('[data-testid=VARIANT_NAME_INPUT]').last().type(variant2); - cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); - cy.wait('@variantCreation'); + it('can add variants to the development environment', () => { + cy.addVariantsToFeature_UI(featureToggleName, [variant1, variant2]); }); - it('can set weight to fixed value for one of the variants', () => { + it('can update variants', () => { cy.visit(`/projects/default/features/${featureToggleName}/variants`); cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click(); - cy.wait(1000); cy.get('[data-testid=VARIANT_NAME_INPUT]') .last() .children() @@ -292,32 +99,13 @@ describe('feature', () => { ).as('variantUpdate'); cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); - cy.wait('@variantUpdate'); cy.get(`[data-testid=VARIANT_WEIGHT_${variant2}]`).should( 'have.text', '15 %' ); }); - it('can delete variant', () => { - cy.visit(`/projects/default/features/${featureToggleName}/variants`); - cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click(); - cy.wait(1000); - cy.get(`[data-testid=VARIANT_DELETE_BUTTON_${variant2}]`).click(); - - cy.intercept( - 'PATCH', - `/api/admin/projects/default/features/${featureToggleName}/environments/development/variants`, - req => { - expect(req.body[0].op).to.equal('remove'); - expect(req.body[0].path).to.equal('/1'); - expect(req.body[1].op).to.equal('replace'); - expect(req.body[1].path).to.equal('/0/weight'); - expect(req.body[1].value).to.equal(1000); - } - ).as('delete'); - - cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); - cy.wait('@delete'); + it('can delete variants', () => { + cy.deleteVariant_UI(featureToggleName, variant2); }); }); diff --git a/frontend/cypress/integration/groups/groups.spec.ts b/frontend/cypress/integration/groups/groups.spec.ts index 1626ac8aeb..1d8d1f8713 100644 --- a/frontend/cypress/integration/groups/groups.spec.ts +++ b/frontend/cypress/integration/groups/groups.spec.ts @@ -1,4 +1,4 @@ -/// +/// const baseUrl = Cypress.config().baseUrl; const randomId = String(Math.random()).split('.')[1]; @@ -12,8 +12,8 @@ const disableActiveSplashScreens = () => { describe('groups', () => { before(() => { - disableActiveSplashScreens(); - cy.login(); + 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}-${randomId}`, @@ -31,7 +31,7 @@ describe('groups', () => { }); beforeEach(() => { - cy.login(); + cy.login_UI(); cy.visit('/admin/groups'); if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { cy.get("[data-testid='CLOSE_SPLASH']").click(); diff --git a/frontend/cypress/integration/import/import.spec.ts b/frontend/cypress/integration/import/import.spec.ts index 4038d3f931..4dd08af5b8 100644 --- a/frontend/cypress/integration/import/import.spec.ts +++ b/frontend/cypress/integration/import/import.spec.ts @@ -1,4 +1,4 @@ -/// +/// const baseUrl = Cypress.config().baseUrl; const randomSeed = String(Math.random()).split('.')[1]; @@ -13,7 +13,7 @@ const disableActiveSplashScreens = () => { describe('imports', () => { before(() => { disableActiveSplashScreens(); - cy.login(); + cy.login_UI(); for (let i = 1; i <= 2; i++) { cy.request('POST', `${baseUrl}/api/admin/user-admin`, { name: `unleash-e2e-user${i}-${randomFeatureName}`, @@ -31,7 +31,7 @@ describe('imports', () => { }); beforeEach(() => { - cy.login(); + cy.login_UI(); if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { cy.get("[data-testid='CLOSE_SPLASH']").click(); } diff --git a/frontend/cypress/integration/projects/access.spec.ts b/frontend/cypress/integration/projects/access.spec.ts index 67dfac45b9..73d4a35769 100644 --- a/frontend/cypress/integration/projects/access.spec.ts +++ b/frontend/cypress/integration/projects/access.spec.ts @@ -1,4 +1,4 @@ -/// +/// import { PA_ASSIGN_BUTTON_ID, @@ -8,6 +8,7 @@ import { PA_ROLE_ID, PA_USERS_GROUPS_ID, PA_USERS_GROUPS_TITLE_ID, + //@ts-ignore } from '../../../src/utils/testIds'; const baseUrl = Cypress.config().baseUrl; @@ -25,7 +26,7 @@ const disableActiveSplashScreens = () => { describe('project-access', () => { before(() => { disableActiveSplashScreens(); - cy.login(); + cy.login_UI(); for (let i = 1; i <= 2; i++) { const name = `${i}-${userName}`; cy.request('POST', `${baseUrl}/api/admin/user-admin`, { @@ -68,7 +69,7 @@ describe('project-access', () => { }); beforeEach(() => { - cy.login(); + cy.login_UI(); cy.visit(`/projects/${groupAndProjectName}/settings/access`); if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { cy.get("[data-testid='CLOSE_SPLASH']").click(); diff --git a/frontend/cypress/integration/projects/notifications.spec.ts b/frontend/cypress/integration/projects/notifications.spec.ts index 125c9b861b..30385288ca 100644 --- a/frontend/cypress/integration/projects/notifications.spec.ts +++ b/frontend/cypress/integration/projects/notifications.spec.ts @@ -1,134 +1,62 @@ -/// +/// + +import UserCredentials = Cypress.UserCredentials; -type UserCredentials = { email: string; password: string }; const ENTERPRISE = Boolean(Cypress.env('ENTERPRISE')); const randomId = String(Math.random()).split('.')[1]; const featureToggleName = `notifications_test-${randomId}`; const baseUrl = Cypress.config().baseUrl; let strategyId = ''; -const userIds: number[] = []; -const userCredentials: UserCredentials[] = []; +let userIds: number[] = []; +let userCredentials: UserCredentials[] = []; const userName = `notifications_user-${randomId}`; const projectName = `default`; -const password = Cypress.env(`AUTH_PASSWORD`) + '_A'; + const EDITOR = 2; -const PROJECT_MEMBER = 5; - -// Disable all active splash pages by visiting them. -const disableActiveSplashScreens = () => { - cy.visit(`/splash/operators`); -}; - -const createUser = () => { - const name = `${userName}`; - const email = `${name}@test.com`; - cy.request('POST', `${baseUrl}/api/admin/user-admin`, { - name: name, - email: `${name}@test.com`, - username: `${name}@test.com`, - sendEmail: false, - rootRole: EDITOR, - }) - .as(name) - .then(response => { - const id = response.body.id; - updateUserPassword(id).then(() => { - addUserToProject(id).then(() => { - userIds.push(id); - userCredentials.push({ email, password }); - }); - }); - }); -}; - -const updateUserPassword = (id: number) => - cy.request( - 'POST', - `${baseUrl}/api/admin/user-admin/${id}/change-password`, - { - password, - } - ); - -const addUserToProject = (id: number) => - cy.request( - 'POST', - `${baseUrl}/api/admin/projects/${projectName}/role/${PROJECT_MEMBER}/access`, - { - groups: [], - users: [{ id }], - } - ); describe('notifications', () => { before(() => { - disableActiveSplashScreens(); - cy.login(); - createUser(); + cy.runBefore(); }); - after(() => { - // We need to login as admin for cleanup - cy.login(); - userIds.forEach(id => - cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`) - ); - - cy.request( - 'DELETE', - `${baseUrl}/api/admin/features/${featureToggleName}` - ); - }); - - beforeEach(() => { - cy.login(); - cy.visit(`/projects/${projectName}`); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } - }); - - afterEach(() => { - cy.logout(); - }); - - const createFeature = () => { - cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); - - cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as( - 'createFeature' - ); - - cy.get("[data-testid='CF_NAME_ID'").type(featureToggleName); - cy.get("[data-testid='CF_DESC_ID'").type('hello-world'); - cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); - cy.wait('@createFeature'); - }; - it('should create a notification when a feature is created in a project', () => { - createFeature(); + cy.login_UI(); + cy.createUser_API(userName, EDITOR).then(value => { + userIds = value.userIds; + userCredentials = value.userCredentials; - //Should not show own notifications - cy.get("[data-testid='NOTIFICATIONS_BUTTON']").click(); + cy.login_UI(); + cy.visit(`/projects/${projectName}`); - //then - cy.get("[data-testid='NOTIFICATIONS_MODAL']").should('exist'); + cy.createFeature_UI(featureToggleName); - const credentials = userCredentials[0]; + //Should not show own notifications + cy.get("[data-testid='NOTIFICATIONS_BUTTON']").click(); - //Sign in as a different user - cy.login(credentials.email, credentials.password); - cy.visit(`/projects/${projectName}`); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } - cy.get("[data-testid='NOTIFICATIONS_BUTTON']").click(); + //then + cy.get("[data-testid='NOTIFICATIONS_MODAL']").should('exist'); - //then - cy.get("[data-testid='UNREAD_NOTIFICATIONS']").should('exist'); - cy.get("[data-testid='NOTIFICATIONS_LIST']") - .should('have.length', 1) - .eq(0) - .should('contain.text', 'New feature'); + const credentials = userCredentials[0]; + + //Sign in as a different user + cy.login_UI(credentials.email, credentials.password); + cy.visit(`/projects/${projectName}`); + cy.get("[data-testid='NOTIFICATIONS_BUTTON']").click(); + + //then + cy.get("[data-testid='UNREAD_NOTIFICATIONS']").should('exist'); + cy.get("[data-testid='NOTIFICATIONS_LIST']") + .eq(0) + .should('contain.text', `New feature ${featureToggleName}`); + + //clean + // We need to login as admin for cleanup + cy.login_UI(); + userIds.forEach(id => + cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`) + ); + + cy.deleteFeature_API(featureToggleName); + }); }); }); diff --git a/frontend/cypress/integration/projects/overview.spec.ts b/frontend/cypress/integration/projects/overview.spec.ts index 4d02dbafa0..248f54cde5 100644 --- a/frontend/cypress/integration/projects/overview.spec.ts +++ b/frontend/cypress/integration/projects/overview.spec.ts @@ -1,10 +1,11 @@ -/// +/// import { BATCH_ACTIONS_BAR, BATCH_SELECT, BATCH_SELECTED_COUNT, MORE_BATCH_ACTIONS, SEARCH_INPUT, + //@ts-ignore } from '../../../src/utils/testIds'; const randomId = String(Math.random()).split('.')[1]; @@ -13,51 +14,9 @@ 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(); - } + cy.runBefore(); }); after(() => { @@ -82,6 +41,9 @@ describe('project overview', () => { }); it('loads the table', () => { + cy.login_UI(); + cy.createFeature_API(`${featureToggleName}-A`); + cy.createFeature_API(`${featureToggleName}-B`); cy.visit('/projects/default'); // Use search to filter feature toggles and check that the feature toggle is listed in the table. @@ -91,6 +53,7 @@ describe('project overview', () => { }); it('can select and deselect feature toggles', () => { + cy.login_UI(); cy.visit('/projects/default'); cy.viewport(1920, 1080); cy.get("[data-testid='SEARCH_INPUT']").click().type(featureToggleName); @@ -138,9 +101,12 @@ describe('project overview', () => { }); it('can mark selected togggles as stale', () => { + cy.login_UI(); cy.visit('/projects/default'); cy.viewport(1920, 1080); - cy.get(`[data-testid='${SEARCH_INPUT}']`).click().type(featureToggleName); + cy.get(`[data-testid='${SEARCH_INPUT}']`) + .click() + .type(featureToggleName); cy.get('table tbody tr').should('have.length', 2); cy.get(selectAll).click(); @@ -153,9 +119,12 @@ describe('project overview', () => { }); it('can archive selected togggles', () => { + cy.login_UI(); cy.visit('/projects/default'); cy.viewport(1920, 1080); - cy.get(`[data-testid='${SEARCH_INPUT}']`).click().type(featureToggleName); + cy.get(`[data-testid='${SEARCH_INPUT}']`) + .click() + .type(featureToggleName); cy.get('table tbody tr').should('have.length', 2); cy.get(selectAll).click(); diff --git a/frontend/cypress/integration/projects/settings.spec.ts b/frontend/cypress/integration/projects/settings.spec.ts index 51307231e9..60137c0cfb 100644 --- a/frontend/cypress/integration/projects/settings.spec.ts +++ b/frontend/cypress/integration/projects/settings.spec.ts @@ -1,117 +1,80 @@ -/// +/// -type UserCredentials = { email: string; password: string }; -const ENTERPRISE = Boolean(Cypress.env('ENTERPRISE')); const randomId = String(Math.random()).split('.')[1]; -const featureToggleName = `settings-${randomId}`; const baseUrl = Cypress.config().baseUrl; let strategyId = ''; const userName = `settings-user-${randomId}`; const projectName = `stickiness-project-${randomId}`; +const TEST_STICKINESS = 'userId'; +const featureToggleName = `settings-${randomId}`; +let cleanFeature = false; +let cleanProject = false; -// Disable all active splash pages by visiting them. -const disableActiveSplashScreens = () => { - cy.visit(`/splash/operators`); -}; - -const disableFeatureStrategiesProdGuard = () => { - localStorage.setItem( - 'useFeatureStrategyProdGuardSettings:v2', - JSON.stringify({ hide: true }) - ); -}; - -describe('notifications', () => { +describe('project settings', () => { before(() => { - disableFeatureStrategiesProdGuard(); - disableActiveSplashScreens(); - cy.login(); - }); - - after(() => { - cy.request( - 'DELETE', - `${baseUrl}/api/admin/features/${featureToggleName}` - ); - - cy.request('DELETE', `${baseUrl}/api/admin/projects/${projectName}`); + cy.runBefore(); }); beforeEach(() => { - cy.login(); - cy.visit(`/projects`); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); + cy.login_UI(); + if (cleanFeature) { + cy.deleteFeature_API(featureToggleName); } + if (cleanProject) { + cy.deleteProject_API(projectName); + } + cy.visit(`/projects`); + cy.wait(300); }); - afterEach(() => { - cy.request('DELETE', `${baseUrl}/api/admin/projects/${projectName}`); - }); - - const createFeature = () => { - cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); - - cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as( - 'createFeature' - ); - - cy.get("[data-testid='CF_NAME_ID'").type(featureToggleName); - cy.get("[data-testid='CF_DESC_ID'").type('hello-world'); - cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); - cy.wait('@createFeature'); - }; - - const createProject = () => { - cy.get('[data-testid=NAVIGATE_TO_CREATE_PROJECT').click(); - - cy.get("[data-testid='PROJECT_ID_INPUT']").type(projectName); - cy.get("[data-testid='PROJECT_NAME_INPUT']").type(projectName); - cy.get("[id='stickiness-select']") - .first() - .click() - .get('[data-testid=SELECT_ITEM_ID-userId') - .first() - .click(); - cy.get("[data-testid='CREATE_PROJECT_BTN']").click(); - }; - it('should store default project stickiness when creating, retrieve it when editing a project', () => { - createProject(); - + //when + cleanProject = true; + cy.createProject_UI(projectName, TEST_STICKINESS); cy.visit(`/projects/${projectName}`); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } cy.get("[data-testid='NAVIGATE_TO_EDIT_PROJECT']").click(); //then cy.get("[id='stickiness-select']") .first() .should('have.text', 'userId'); + + //clean + cy.request('DELETE', `${baseUrl}/api/admin/projects/${projectName}`); }); it('should respect the default project stickiness when creating a Gradual Rollout Strategy', () => { - createProject(); - createFeature(); + cy.createProject_UI(projectName, TEST_STICKINESS); + cy.createFeature_UI(featureToggleName, true, projectName); + cleanFeature = true; + + //when - then + cy.addFlexibleRolloutStrategyToFeature_UI({ + featureToggleName, + project: projectName, + stickiness: TEST_STICKINESS, + }); + + //clean + }); + + it('should respect the default project stickiness when creating a variant', () => { + cy.createProject_UI(projectName, TEST_STICKINESS); + cy.createFeature_UI(featureToggleName, true, projectName); + + //when cy.visit( - `/projects/default/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=flexibleRollout` + `/projects/${projectName}/features/${featureToggleName}/variants` ); + cy.get("[data-testid='ADD_VARIANT_BUTTON']").first().click(); //then cy.get("[id='stickiness-select']") .first() .should('have.text', 'userId'); - }); - it('should respect the default project stickiness when creating a variant', () => { - createProject(); - createFeature(); - - cy.visit(`/projects/default/features/${featureToggleName}/variants`); - - cy.get("[data-testid='EDIT_VARIANTS_BUTTON']").click(); - //then - cy.get('#menu-stickiness').first().should('have.text', 'userId'); + //clean + cy.deleteFeature_API(featureToggleName); + cy.deleteProject_API(projectName); }); }); diff --git a/frontend/cypress/integration/segments/segments.spec.ts b/frontend/cypress/integration/segments/segments.spec.ts index aa8b00ca97..0a54a32400 100644 --- a/frontend/cypress/integration/segments/segments.spec.ts +++ b/frontend/cypress/integration/segments/segments.spec.ts @@ -1,43 +1,29 @@ -/// +/// const randomId = String(Math.random()).split('.')[1]; const segmentName = `unleash-e2e-${randomId}`; - -// Disable all active splash pages by visiting them. -const disableActiveSplashScreens = () => { - cy.visit(`/splash/operators`); -}; +let segmentId: string; describe('segments', () => { before(() => { - disableActiveSplashScreens(); + cy.runBefore(); }); beforeEach(() => { - cy.login(); + cy.login_UI(); cy.visit('/segments'); - }); - - it('can create a segment', () => { if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { cy.get("[data-testid='CLOSE_SPLASH']").click(); } + }); - cy.get("[data-testid='NAVIGATE_TO_CREATE_SEGMENT']").click(); - - cy.intercept('POST', '/api/admin/segments').as('createSegment'); - - cy.get("[data-testid='SEGMENT_NAME_ID']").type(segmentName); - cy.get("[data-testid='SEGMENT_DESC_ID']").type('hello-world'); - cy.get("[data-testid='SEGMENT_NEXT_BTN_ID']").click(); - cy.get("[data-testid='SEGMENT_CREATE_BTN_ID']").click(); - cy.wait('@createSegment'); + it('can create a segment', () => { + cy.createSegment_UI(segmentName); cy.contains(segmentName); }); it('gives an error if a segment exists with the same name', () => { cy.get("[data-testid='NAVIGATE_TO_CREATE_SEGMENT']").click(); - cy.get("[data-testid='SEGMENT_NAME_ID']").type(segmentName); cy.get("[data-testid='SEGMENT_NEXT_BTN_ID']").should('be.disabled'); cy.get("[data-testid='INPUT_ERROR_TEXT']").contains( @@ -46,11 +32,7 @@ describe('segments', () => { }); it('can delete a segment', () => { - cy.get(`[data-testid='SEGMENT_DELETE_BTN_ID_${segmentName}']`).click(); - - cy.get("[data-testid='SEGMENT_DIALOG_NAME_ID']").type(segmentName); - cy.get("[data-testid='DIALOGUE_CONFIRM_ID'").click(); - + cy.deleteSegment_UI(segmentName, segmentId); cy.contains(segmentName).should('not.exist'); }); }); diff --git a/frontend/cypress/support/API.ts b/frontend/cypress/support/API.ts new file mode 100644 index 0000000000..cde08909f7 --- /dev/null +++ b/frontend/cypress/support/API.ts @@ -0,0 +1,103 @@ +/// + +import Chainable = Cypress.Chainable; +const baseUrl = Cypress.config().baseUrl; +const password = Cypress.env(`AUTH_PASSWORD`) + '_A'; +const PROJECT_MEMBER = 5; +export const createFeature_API = ( + featureName: string, + projectName?: string +): Chainable => { + const project = projectName || 'default'; + return cy.request({ + url: `/api/admin/projects/${project}/features`, + method: 'POST', + body: { + name: `${featureName}`, + description: 'hello-world', + type: 'release', + impressionData: false, + }, + }); +}; + +export const deleteFeature_API = (name: string): Chainable => { + cy.request({ + method: 'DELETE', + url: `${baseUrl}/api/admin/features/${name}`, + }); + return cy.request({ + method: 'DELETE', + url: `${baseUrl}/api/admin/archive/${name}`, + }); +}; + +export const createProject_API = (project: string): Chainable => { + return cy.request({ + url: `/api/admin/projects`, + method: 'POST', + body: { + id: project, + name: project, + description: project, + impressionData: false, + }, + }); +}; + +export const deleteProject_API = (name: string): Chainable => { + return cy.request({ + method: 'DELETE', + url: `${baseUrl}/api/admin/projects/${name}`, + }); +}; + +export const createUser_API = (userName: string, role: number) => { + const name = `${userName}`; + const email = `${name}@test.com`; + const userIds: number[] = []; + const userCredentials: Cypress.UserCredentials[] = []; + cy.request('POST', `${baseUrl}/api/admin/user-admin`, { + name: name, + email: `${name}@test.com`, + username: `${name}@test.com`, + sendEmail: false, + rootRole: role, + }) + .as(name) + .then(response => { + const id = response.body.id; + updateUserPassword_API(id).then(() => { + addUserToProject_API(id, PROJECT_MEMBER).then(value => { + userIds.push(id); + userCredentials.push({ email, password }); + }); + }); + }); + return cy.wrap({ userIds, userCredentials }); +}; + +export const updateUserPassword_API = (id: number, pass?: string): Chainable => + cy.request( + 'POST', + `${baseUrl}/api/admin/user-admin/${id}/change-password`, + { + password: pass || password, + } + ); + +export const addUserToProject_API = ( + id: number, + role: number, + projectName?: string +): Chainable => { + const project = projectName || 'default'; + return cy.request( + 'POST', + `${baseUrl}/api/admin/projects/${project}/role/${role}/access`, + { + groups: [], + users: [{ id }], + } + ); +}; diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts new file mode 100644 index 0000000000..a3f97ecf3d --- /dev/null +++ b/frontend/cypress/support/UI.ts @@ -0,0 +1,347 @@ +/// + +import Chainable = Cypress.Chainable; +import AddStrategyOptions = Cypress.AddFlexibleRolloutStrategyOptions; +const AUTH_USER = Cypress.env('AUTH_USER'); +const AUTH_PASSWORD = Cypress.env('AUTH_PASSWORD'); +const ENTERPRISE = Boolean(Cypress.env('ENTERPRISE')); +const disableActiveSplashScreens = () => { + return cy.visit(`/splash/operators`); +}; + +const disableFeatureStrategiesProdGuard = () => { + localStorage.setItem( + 'useFeatureStrategyProdGuardSettings:v2', + JSON.stringify({ hide: true }) + ); +}; + +export const runBefore = () => { + disableFeatureStrategiesProdGuard(); + disableActiveSplashScreens(); +}; + +export const login_UI = ( + user = AUTH_USER, + password = AUTH_PASSWORD +): Chainable => { + return cy.session(user, () => { + cy.visit('/'); + cy.wait(1500); + cy.get("[data-testid='LOGIN_EMAIL_ID']").type(user); + + if (AUTH_PASSWORD) { + cy.get("[data-testid='LOGIN_PASSWORD_ID']").type(password); + } + + cy.get("[data-testid='LOGIN_BUTTON']").click(); + + // Wait for the login redirect to complete. + cy.get("[data-testid='HEADER_USER_AVATAR']"); + + if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { + cy.get("[data-testid='CLOSE_SPLASH']").click(); + } + }); +}; + +export const createFeature_UI = ( + name: string, + shouldWait?: boolean, + project?: string +): Chainable => { + const projectName = project || 'default'; + + cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); + + cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as( + 'createFeature' + ); + + cy.wait(300); + + cy.get("[data-testid='CF_NAME_ID'").type(name); + cy.get("[data-testid='CF_DESC_ID'").type('hello-world'); + if (!shouldWait) return cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); + else cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); + return cy.wait('@createFeature'); +}; + +export const createProject_UI = ( + projectName: string, + defaultStickiness: string +): Chainable => { + cy.get('[data-testid=NAVIGATE_TO_CREATE_PROJECT').click(); + + cy.intercept('POST', `/api/admin/projects`).as('createProject'); + + cy.get("[data-testid='PROJECT_ID_INPUT']").type(projectName); + cy.get("[data-testid='PROJECT_NAME_INPUT']").type(projectName); + cy.get("[id='stickiness-select']") + .first() + .click() + .get(`[data-testid=SELECT_ITEM_ID-${defaultStickiness}`) + .first() + .click(); + cy.get("[data-testid='CREATE_PROJECT_BTN']").click(); + cy.wait('@createProject'); + return cy.visit(`/projects/${projectName}`); +}; + +export const createSegment_UI = (segmentName: string): Chainable => { + cy.get("[data-testid='NAVIGATE_TO_CREATE_SEGMENT']").click(); + let segmentId; + cy.intercept('POST', '/api/admin/segments', req => { + req.continue(res => { + segmentId = res.body.id; + }); + }).as('createSegment'); + + cy.get("[data-testid='SEGMENT_NAME_ID']").type(segmentName); + cy.get("[data-testid='SEGMENT_DESC_ID']").type('hello-world'); + cy.get("[data-testid='SEGMENT_NEXT_BTN_ID']").click(); + cy.get("[data-testid='SEGMENT_CREATE_BTN_ID']").click(); + cy.wait('@createSegment'); + return cy.wrap(segmentId); +}; + +export const deleteSegment_UI = (segmentName: string): Chainable => { + cy.get(`[data-testid='SEGMENT_DELETE_BTN_ID_${segmentName}']`).click(); + + cy.get("[data-testid='SEGMENT_DIALOG_NAME_ID']").type(segmentName); + return cy.get("[data-testid='DIALOGUE_CONFIRM_ID'").click(); +}; + +export const addFlexibleRolloutStrategyToFeature_UI = ( + options: AddStrategyOptions +): Chainable => { + const { featureToggleName, project, environment, stickiness } = options; + const projectName = project || 'default'; + const env = environment || 'development'; + const defaultStickiness = stickiness || 'default'; + + cy.visit(`/projects/default/features/${featureToggleName}`); + let strategyId; + cy.intercept( + 'POST', + `/api/admin/projects/${projectName}/features/${featureToggleName}/environments/development/strategies`, + req => { + expect(req.body.name).to.equal('flexibleRollout'); + expect(req.body.parameters.groupId).to.equal(featureToggleName); + expect(req.body.parameters.stickiness).to.equal(defaultStickiness); + expect(req.body.parameters.rollout).to.equal('50'); + + if (ENTERPRISE) { + expect(req.body.constraints.length).to.equal(1); + } else { + expect(req.body.constraints.length).to.equal(0); + } + + req.continue(res => { + strategyId = res.body.id; + }); + } + ).as('addStrategyToFeature'); + + cy.visit( + `/projects/${projectName}/features/${featureToggleName}/strategies/create?environmentId=${env}&strategyName=flexibleRollout` + ); + cy.wait(500); + // Takes a bit to load the screen - this will wait until it finds it or fail + cy.get('[data-testid=FLEXIBLE_STRATEGY_STICKINESS_ID]'); + if (ENTERPRISE) { + cy.get('[data-testid=ADD_CONSTRAINT_ID]').click(); + cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); + } + cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click(); + cy.wait('@addStrategyToFeature'); + return cy.wrap(strategyId); +}; + +export const updateFlexibleRolloutStrategy_UI = ( + featureToggleName: string, + strategyId: string, + projectName?: string +) => { + const project = projectName || 'default'; + cy.visit( + `/projects/${project}/features/${featureToggleName}/strategies/edit?environmentId=development&strategyId=${strategyId}` + ); + + cy.wait(500); + + cy.get('[data-testid=FLEXIBLE_STRATEGY_STICKINESS_ID]') + .first() + .click() + .get('[data-testid=SELECT_ITEM_ID-sessionId') + .first() + .click(); + + cy.wait(500); + cy.get('[data-testid=FLEXIBLE_STRATEGY_GROUP_ID]') + .first() + .clear() + .type('new-group-id'); + + cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click(); + cy.intercept( + 'PUT', + `/api/admin/projects/${project}/features/${featureToggleName}/environments/*/strategies/${strategyId}`, + req => { + expect(req.body.parameters.groupId).to.equal('new-group-id'); + expect(req.body.parameters.stickiness).to.equal('sessionId'); + expect(req.body.parameters.rollout).to.equal('50'); + + if (ENTERPRISE) { + expect(req.body.constraints.length).to.equal(1); + } else { + expect(req.body.constraints.length).to.equal(0); + } + + req.continue(res => { + expect(res.statusCode).to.equal(200); + }); + } + ).as('updateStrategy'); + return cy.wait('@updateStrategy'); +}; + +export const deleteFeatureStrategy_UI = ( + featureToggleName: string, + strategyId: string, + shouldWait?: boolean, + projectName?: string +): Chainable => { + const project = projectName || 'default'; + + cy.intercept( + 'DELETE', + `/api/admin/projects/${project}/features/${featureToggleName}/environments/*/strategies/${strategyId}`, + req => { + req.continue(res => { + expect(res.statusCode).to.equal(200); + }); + } + ).as('deleteUserStrategy'); + cy.visit(`/projects/${project}/features/${featureToggleName}`); + cy.get('[data-testid=FEATURE_ENVIRONMENT_ACCORDION_development]').click(); + cy.get('[data-testid=STRATEGY_FORM_REMOVE_ID]').click(); + if (!shouldWait) return cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); + else cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); + return cy.wait('@deleteUserStrategy'); +}; + +export const addUserIdStrategyToFeature_UI = ( + featureToggleName: string, + projectName: string +): Chainable => { + const project = projectName || 'default'; + cy.visit( + `/projects/${project}/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=userWithId` + ); + + if (ENTERPRISE) { + cy.get('[data-testid=ADD_CONSTRAINT_ID]').click(); + cy.get('[data-testid=CONSTRAINT_AUTOCOMPLETE_ID]') + .type('{downArrow}'.repeat(1)) + .type('{enter}'); + cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); + } + + cy.get('[data-testid=STRATEGY_INPUT_LIST]') + .type('user1') + .type('{enter}') + .type('user2') + .type('{enter}'); + cy.get('[data-testid=ADD_TO_STRATEGY_INPUT_LIST]').click(); + let strategyId; + cy.intercept( + 'POST', + `/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`, + req => { + expect(req.body.name).to.equal('userWithId'); + + expect(req.body.parameters.userIds.length).to.equal(11); + + if (ENTERPRISE) { + expect(req.body.constraints.length).to.equal(1); + } else { + expect(req.body.constraints.length).to.equal(0); + } + + req.continue(res => { + strategyId = res.body.id; + }); + } + ).as('addStrategyToFeature'); + + cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click(); + cy.wait('@addStrategyToFeature'); + return cy.wrap(strategyId); +}; + +export const addVariantsToFeature_UI = ( + featureToggleName: string, + variants: Array, + projectName: string +) => { + const project = projectName || 'default'; + cy.visit(`/projects/${project}/features/${featureToggleName}/variants`); + cy.wait(1000); + cy.intercept( + 'PATCH', + `/api/admin/projects/${project}/features/${featureToggleName}/environments/development/variants`, + req => { + variants.forEach((variant, index) => { + expect(req.body[index].op).to.equal('add'); + expect(req.body[index].path).to.equal(`/${index}`); + expect(req.body[index].value.name).to.equal(variant); + expect(req.body[index].value.weight).to.equal( + 1000 / variants.length + ); + }); + } + ).as('variantCreation'); + + cy.get('[data-testid=ADD_VARIANT_BUTTON]').first().click(); + cy.wait(500); + variants.forEach((variant, index) => { + cy.get('[data-testid=VARIANT_NAME_INPUT]').eq(index).type(variant); + index + 1 < variants.length && + cy.get('[data-testid=MODAL_ADD_VARIANT_BUTTON]').first().click(); + }); + + cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').first().click(); + return cy.wait('@variantCreation'); +}; + +export const deleteVariant_UI = ( + featureToggleName: string, + variant: string, + projectName?: string +): Chainable => { + const project = projectName || 'default'; + cy.visit(`/projects/${project}/features/${featureToggleName}/variants`); + cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click(); + cy.wait(300); + cy.get(`[data-testid=VARIANT_DELETE_BUTTON_${variant}]`).first().click(); + + cy.intercept( + 'PATCH', + `/api/admin/projects/${project}/features/${featureToggleName}/environments/development/variants`, + req => { + expect(req.body[0].op).to.equal('remove'); + expect(req.body[0].path).to.equal('/1'); + expect(req.body[1].op).to.equal('replace'); + expect(req.body[1].path).to.equal('/0/weight'); + expect(req.body[1].value).to.equal(1000); + } + ).as('delete'); + + cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); + return cy.wait('@delete'); +}; + +export const logout_UI = (): Chainable => { + return cy.visit('/logout'); +}; diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts index 0d16067cc2..b76560c09d 100644 --- a/frontend/cypress/support/commands.ts +++ b/frontend/cypress/support/commands.ts @@ -1,49 +1,57 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +/// -const AUTH_USER = Cypress.env('AUTH_USER'); -const AUTH_PASSWORD = Cypress.env('AUTH_PASSWORD'); +import { + runBefore, + login_UI, + logout_UI, + createProject_UI, + createFeature_UI, + createSegment_UI, + deleteSegment_UI, + deleteVariant_UI, + deleteFeatureStrategy_UI, + addFlexibleRolloutStrategyToFeature_UI, + addUserIdStrategyToFeature_UI, + updateFlexibleRolloutStrategy_UI, + addVariantsToFeature_UI, + //@ts-ignore +} from './UI'; +import { + addUserToProject_API, + createFeature_API, + createProject_API, + createUser_API, + deleteFeature_API, + deleteProject_API, + updateUserPassword_API, + //@ts-ignore +} from './API'; -Cypress.Commands.add('login', (user = AUTH_USER, password = AUTH_PASSWORD) => - cy.session(user, () => { - cy.visit('/'); - cy.wait(1500); - cy.get("[data-testid='LOGIN_EMAIL_ID']").type(user); - - if (AUTH_PASSWORD) { - cy.get("[data-testid='LOGIN_PASSWORD_ID']").type(password); - } - - cy.get("[data-testid='LOGIN_BUTTON']").click(); - - // Wait for the login redirect to complete. - cy.get("[data-testid='HEADER_USER_AVATAR']"); - }) +Cypress.Commands.add('runBefore', runBefore); +Cypress.Commands.add('login_UI', login_UI); +Cypress.Commands.add('createSegment_UI', createSegment_UI); +Cypress.Commands.add('deleteSegment_UI', deleteSegment_UI); +Cypress.Commands.add('deleteFeature_API', deleteFeature_API); +Cypress.Commands.add('deleteProject_API', deleteProject_API); +Cypress.Commands.add('logout_UI', logout_UI); +Cypress.Commands.add('createProject_UI', createProject_UI); +Cypress.Commands.add('createUser_API', createUser_API); +Cypress.Commands.add('addUserToProject_API', addUserToProject_API); +Cypress.Commands.add('updateUserPassword_API', updateUserPassword_API); +Cypress.Commands.add('createFeature_UI', createFeature_UI); +Cypress.Commands.add('deleteFeatureStrategy_UI', deleteFeatureStrategy_UI); +Cypress.Commands.add('createFeature_API', createFeature_API); +Cypress.Commands.add('deleteVariant_UI', deleteVariant_UI); +Cypress.Commands.add('addVariantsToFeature_UI', addVariantsToFeature_UI); +Cypress.Commands.add( + 'addUserIdStrategyToFeature_UI', + addUserIdStrategyToFeature_UI +); +Cypress.Commands.add( + 'addFlexibleRolloutStrategyToFeature_UI', + addFlexibleRolloutStrategyToFeature_UI +); +Cypress.Commands.add( + 'updateFlexibleRolloutStrategy_UI', + updateFlexibleRolloutStrategy_UI ); - -Cypress.Commands.add('logout', () => { - cy.visit('/logout'); -}); diff --git a/frontend/cypress/support/index.ts b/frontend/cypress/support/index.ts index a8f805772b..c7cca105d0 100644 --- a/frontend/cypress/support/index.ts +++ b/frontend/cypress/support/index.ts @@ -14,16 +14,8 @@ // *********************************************************** // Import commands.js using ES2015 syntax: +// @ts-ignore import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') - -declare global { - namespace Cypress { - interface Chainable { - login(user?: string, password?: string): Chainable; - logout(user?: string): Chainable; - } - } -} diff --git a/frontend/cypress/tsconfig.json b/frontend/cypress/tsconfig.json new file mode 100644 index 0000000000..a49f6356d0 --- /dev/null +++ b/frontend/cypress/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "include": ["./**/*.ts", "../cypress.d.ts"], + "exclude": [], + "compilerOptions": { + "types": ["cypress"], + "lib": ["es2015", "dom"], + "isolatedModules": false, + "allowJs": true, + "noEmit": true + } +} diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal.tsx index 1415f69d78..f82bd20c13 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal.tsx @@ -20,6 +20,7 @@ import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashCon import { updateWeightEdit } from 'component/common/util'; import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect'; import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings'; +import Loader from 'component/common/Loader/Loader'; const StyledFormSubtitle = styled('div')(({ theme }) => ({ display: 'flex', @@ -145,7 +146,7 @@ export const EnvironmentVariantsModal = ({ const { uiConfig } = useUiConfig(); const { context } = useUnleashContext(); - const { defaultStickiness } = useDefaultProjectSettings(projectId); + const { defaultStickiness, loading } = useDefaultProjectSettings(projectId); const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); const { data } = usePendingChangeRequests(projectId); @@ -157,31 +158,33 @@ export const EnvironmentVariantsModal = ({ const [newVariant, setNewVariant] = useState(); useEffect(() => { - setVariantsEdit( - oldVariants.length - ? oldVariants.map(oldVariant => ({ - ...oldVariant, - isValid: true, - new: false, - id: uuidv4(), - })) - : [ - { - name: '', - weightType: WeightType.VARIABLE, - weight: 0, - overrides: [], - stickiness: - variantsEdit?.length > 0 - ? variantsEdit[0].stickiness - : defaultStickiness, - new: true, - isValid: false, + if (!loading) { + setVariantsEdit( + oldVariants.length + ? oldVariants.map(oldVariant => ({ + ...oldVariant, + isValid: true, + new: false, id: uuidv4(), - }, - ] - ); - }, [open]); + })) + : [ + { + name: '', + weightType: WeightType.VARIABLE, + weight: 0, + overrides: [], + stickiness: + variantsEdit?.length > 0 + ? variantsEdit[0].stickiness + : defaultStickiness, + new: true, + isValid: false, + id: uuidv4(), + }, + ] + ); + } + }, [open, loading]); const updateVariant = (updatedVariant: IFeatureVariantEdit, id: string) => { setVariantsEdit(prevVariants => @@ -303,6 +306,11 @@ export const EnvironmentVariantsModal = ({ setError(apiPayload.error); } }, [apiPayload.error]); + + if (loading || stickiness === '') { + return ; + } + return ( { const projectId = useOptionalPathParam('projectId'); const { defaultStickiness, loading } = useDefaultProjectSettings(projectId); + const onUpdate = (field: string) => (newValue: string) => { updateParameter(field, newValue); }; @@ -43,15 +45,13 @@ const FlexibleStrategy = ({ ? parseParameterNumber(parameters.rollout) : 100; - const resolveStickiness = () => { - if (parameters.stickiness === '') { + const stickiness = useMemo(() => { + if (parameters.stickiness === '' && !loading) { return defaultStickiness; } return parseParameterString(parameters.stickiness); - }; - - const stickiness = resolveStickiness(); + }, [loading, parameters.stickiness]); if (parameters.stickiness === '') { onUpdate('stickiness')(stickiness); diff --git a/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx b/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx index 5c8204627c..0193691613 100644 --- a/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx +++ b/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx @@ -13,7 +13,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import Select from 'component/common/select'; -import { DefaultStickiness, ProjectMode } from '../hooks/useProjectForm'; +import { ProjectMode } from '../hooks/useProjectForm'; import { Box } from '@mui/material'; import { CollaborationModeTooltip } from './CollaborationModeTooltip'; @@ -23,9 +23,7 @@ interface IProjectForm { projectDesc: string; projectStickiness?: string; projectMode?: string; - setProjectStickiness?: React.Dispatch< - React.SetStateAction - >; + setProjectStickiness?: React.Dispatch>; setProjectMode?: React.Dispatch>; setProjectId: React.Dispatch>; setProjectName: React.Dispatch>; @@ -127,9 +125,7 @@ const ProjectForm: React.FC = ({ data-testid={PROJECT_STICKINESS_SELECT} onChange={e => setProjectStickiness && - setProjectStickiness( - e.target.value as DefaultStickiness - ) + setProjectStickiness(e.target.value) } editable /> diff --git a/frontend/src/component/project/Project/hooks/useProjectForm.ts b/frontend/src/component/project/Project/hooks/useProjectForm.ts index 7e34bb45d4..8c7a670cf9 100644 --- a/frontend/src/component/project/Project/hooks/useProjectForm.ts +++ b/frontend/src/component/project/Project/hooks/useProjectForm.ts @@ -1,27 +1,23 @@ import { useEffect, useState } from 'react'; import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; import { formatUnknownError } from 'utils/formatUnknownError'; -import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings'; export type ProjectMode = 'open' | 'protected'; -export type DefaultStickiness = 'default' | 'userId' | 'sessionId' | 'random'; export const DEFAULT_PROJECT_STICKINESS = 'default'; const useProjectForm = ( initialProjectId = '', initialProjectName = '', initialProjectDesc = '', - initialProjectStickiness: DefaultStickiness = DEFAULT_PROJECT_STICKINESS, + initialProjectStickiness = DEFAULT_PROJECT_STICKINESS, initialProjectMode: ProjectMode = 'open' ) => { const [projectId, setProjectId] = useState(initialProjectId); - const { defaultStickiness } = useDefaultProjectSettings(projectId); const [projectName, setProjectName] = useState(initialProjectName); const [projectDesc, setProjectDesc] = useState(initialProjectDesc); - const [projectStickiness, setProjectStickiness] = - useState( - defaultStickiness || initialProjectStickiness - ); + const [projectStickiness, setProjectStickiness] = useState( + initialProjectStickiness + ); const [projectMode, setProjectMode] = useState(initialProjectMode); const [errors, setErrors] = useState({}); diff --git a/frontend/src/hooks/useDefaultProjectSettings.ts b/frontend/src/hooks/useDefaultProjectSettings.ts index 0e069c5914..1f9add93b1 100644 --- a/frontend/src/hooks/useDefaultProjectSettings.ts +++ b/frontend/src/hooks/useDefaultProjectSettings.ts @@ -3,14 +3,11 @@ import { SWRConfiguration } from 'swr'; import { useCallback } from 'react'; import handleErrorResponses from './api/getters/httpErrorResponseHandler'; import { useConditionalSWR } from './api/getters/useConditionalSWR/useConditionalSWR'; -import { - DefaultStickiness, - ProjectMode, -} from 'component/project/Project/hooks/useProjectForm'; +import { ProjectMode } from 'component/project/Project/hooks/useProjectForm'; import { formatApiPath } from 'utils/formatPath'; export interface ISettingsResponse { - defaultStickiness?: DefaultStickiness; + defaultStickiness?: string; mode?: ProjectMode; } const DEFAULT_STICKINESS = 'default'; @@ -23,24 +20,32 @@ export const useDefaultProjectSettings = ( const PATH = `api/admin/projects/${projectId}/settings`; const { projectScopedStickiness } = uiConfig.flags; - const { data, error, mutate } = useConditionalSWR( - Boolean(projectId) && Boolean(projectScopedStickiness), - {}, - ['useDefaultProjectSettings', PATH], - () => fetcher(formatApiPath(PATH)), - options - ); + const { data, isLoading, error, mutate } = + useConditionalSWR( + Boolean(projectId) && Boolean(projectScopedStickiness), + {}, + ['useDefaultProjectSettings', PATH], + () => fetcher(formatApiPath(PATH)), + options + ); - const defaultStickiness: DefaultStickiness = - data?.defaultStickiness ?? DEFAULT_STICKINESS; + const defaultStickiness = (): string => { + if (!isLoading) { + if (data?.defaultStickiness) { + return data?.defaultStickiness; + } + return DEFAULT_STICKINESS; + } + return ''; + }; const refetch = useCallback(() => { mutate().catch(console.warn); }, [mutate]); return { - defaultStickiness, + defaultStickiness: defaultStickiness(), refetch, - loading: !error && !data, + loading: isLoading, error, }; }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 2dcb13545f..37ee123501 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -18,8 +18,9 @@ "strict": true, "paths": { "@server/*": ["./../../src/lib/*"] - } + }, + "types": ["cypress"] }, - "include": ["./src"], + "include": ["./src", "cypress.d.ts"], "references": [{ "path": "./tsconfig.node.json" }] }