From 9bd595ffc44af5bae8c623f4d70dd2508a701241 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Wed, 5 Apr 2023 15:34:17 +0300 Subject: [PATCH] Tmp 4.22.2 (#3461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: - Stickiness accept any string (removed enum) - UI sync issues when creating project->feature->Flexible rollout strategy - Fixes stickiness UI issue when adding variants ## About the changes Closes # ### Important files ## Discussion points --------- Signed-off-by: andreas-unleash --- .github/workflows/e2e.frontend.yaml | 2 +- frontend/cypress.d.ts | 5 + frontend/cypress/README.md | 36 ++ frontend/cypress/global.d.ts | 81 ++++ .../integration/feature/feature.spec.ts | 281 ++------------ .../cypress/integration/groups/groups.spec.ts | 23 +- .../cypress/integration/import/import.spec.ts | 23 +- .../integration/projects/access.spec.ts | 28 +- .../projects/notifications.spec.ts | 28 +- .../integration/projects/overview.spec.ts | 74 ++-- .../integration/projects/settings.spec.ts | 19 +- .../integration/segments/segments.spec.ts | 40 +- frontend/cypress/support/API.ts | 103 ++++++ frontend/cypress/support/UI.ts | 347 ++++++++++++++++++ frontend/cypress/support/commands.ts | 100 ++--- frontend/cypress/support/index.ts | 10 +- frontend/cypress/tsconfig.json | 12 + .../EnvironmentVariantsModal.tsx | 79 ++-- .../FlexibleStrategy/FlexibleStrategy.tsx | 10 +- .../Project/ProjectForm/ProjectForm.tsx | 10 +- .../project/Project/hooks/useProjectForm.ts | 12 +- .../src/hooks/useDefaultProjectSettings.ts | 37 +- frontend/tsconfig.json | 5 +- src/lib/services/project-schema.ts | 5 +- 24 files changed, 836 insertions(+), 534 deletions(-) create mode 100644 frontend/cypress.d.ts create mode 100644 frontend/cypress/README.md create mode 100644 frontend/cypress/global.d.ts create mode 100644 frontend/cypress/support/API.ts create mode 100644 frontend/cypress/support/UI.ts create mode 100644 frontend/cypress/tsconfig.json diff --git a/.github/workflows/e2e.frontend.yaml b/.github/workflows/e2e.frontend.yaml index 434b1ac0c2a..2b3917ced65 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 00000000000..d12adb035f8 --- /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 00000000000..edd06efe20e --- /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 00000000000..90783d8bd08 --- /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 2160b679e8b..0003da5f163 100644 --- a/frontend/cypress/integration/feature/feature.spec.ts +++ b/frontend/cypress/integration/feature/feature.spec.ts @@ -1,269 +1,75 @@ -/// +/// -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 }) - ); -}; +describe('feature', () => { + const randomId = String(Math.random()).split('.')[1]; + const featureToggleName = `unleash-e2e-${randomId}`; -// Disable all active splash pages by visiting them. -const disableActiveSplashScreens = () => { - cy.visit(`/splash/operators`); -}; + const variant1 = 'variant1'; + const variant2 = 'variant2'; + let strategyId = ''; -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` - ); - - 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; - }); + cy.addUserIdStrategyToFeature_UI(featureToggleName, strategyId).then( + value => { + cy.deleteFeatureStrategy_UI(featureToggleName, value, false); } - ).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 +98,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 1626ac8aebf..2b4a2431dc7 100644 --- a/frontend/cypress/integration/groups/groups.spec.ts +++ b/frontend/cypress/integration/groups/groups.spec.ts @@ -1,19 +1,14 @@ -/// - -const baseUrl = Cypress.config().baseUrl; -const randomId = String(Math.random()).split('.')[1]; -const groupName = `unleash-e2e-${randomId}`; -const userIds: any[] = []; - -// Disable all active splash pages by visiting them. -const disableActiveSplashScreens = () => { - cy.visit(`/splash/operators`); -}; +/// describe('groups', () => { + const baseUrl = Cypress.config().baseUrl; + const randomId = String(Math.random()).split('.')[1]; + const groupName = `unleash-e2e-${randomId}`; + const userIds: any[] = []; + 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 +26,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 4038d3f931a..0c67d8b8c1b 100644 --- a/frontend/cypress/integration/import/import.spec.ts +++ b/frontend/cypress/integration/import/import.spec.ts @@ -1,19 +1,14 @@ -/// - -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', () => { + const baseUrl = Cypress.config().baseUrl; + const randomSeed = String(Math.random()).split('.')[1]; + const randomFeatureName = `cypress-features${randomSeed}`; + const userIds: any[] = []; + 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}-${randomFeatureName}`, @@ -31,7 +26,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 67dfac45b96..2cd6c7f1cd5 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,24 +8,20 @@ import { PA_ROLE_ID, PA_USERS_GROUPS_ID, PA_USERS_GROUPS_TITLE_ID, + //@ts-ignore } from '../../../src/utils/testIds'; -const baseUrl = Cypress.config().baseUrl; -const randomId = String(Math.random()).split('.')[1]; -const groupAndProjectName = `group-e2e-${randomId}`; -const userName = `user-e2e-${randomId}`; -const groupIds: any[] = []; -const userIds: any[] = []; - -// Disable all active splash pages by visiting them. -const disableActiveSplashScreens = () => { - cy.visit(`/splash/operators`); -}; - describe('project-access', () => { + const baseUrl = Cypress.config().baseUrl; + const randomId = String(Math.random()).split('.')[1]; + const groupAndProjectName = `group-e2e-${randomId}`; + const userName = `user-e2e-${randomId}`; + const groupIds: any[] = []; + const userIds: any[] = []; + before(() => { - disableActiveSplashScreens(); - cy.login(); + cy.runBefore(); + cy.login_UI(); for (let i = 1; i <= 2; i++) { const name = `${i}-${userName}`; cy.request('POST', `${baseUrl}/api/admin/user-admin`, { @@ -68,7 +64,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 e214cdc45ae..8cd2bef1e37 100644 --- a/frontend/cypress/integration/projects/notifications.spec.ts +++ b/frontend/cypress/integration/projects/notifications.spec.ts @@ -1,25 +1,20 @@ /// -import UserCredentials = Cypress.UserCredentials; - -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 = ''; -let userIds: number[] = []; -let userCredentials: UserCredentials[] = []; -const userName = `notifications_user-${randomId}`; -const projectName = `default`; - const EDITOR = 2; describe('notifications', () => { + const randomId = String(Math.random()).split('.')[1]; + const featureToggleName = `notifications_test-${randomId}`; + const baseUrl = Cypress.config().baseUrl; + let userIds: number[] = []; + let userCredentials: Cypress.UserCredentials[] = []; + const userName = `notifications_user-${randomId}`; + const projectName = `default`; + before(() => { cy.runBefore(); }); - // This one is failing on CI: https://github.com/Unleash/unleash/actions/runs/4609305167/jobs/8160244872#step:4:193 it.skip('should create a notification when a feature is created in a project', () => { cy.login_UI(); cy.createUser_API(userName, EDITOR).then(value => { @@ -46,9 +41,10 @@ describe('notifications', () => { //then cy.get("[data-testid='UNREAD_NOTIFICATIONS']").should('exist'); - cy.get("[data-testid='NOTIFICATIONS_LIST']") - .eq(0) - .should('contain.text', `New feature ${featureToggleName}`); + cy.get("[data-testid='NOTIFICATIONS_LIST']").should( + 'contain.text', + `New feature ${featureToggleName}` + ); //clean // We need to login as admin for cleanup diff --git a/frontend/cypress/integration/projects/overview.spec.ts b/frontend/cypress/integration/projects/overview.spec.ts index 4d02dbafa0e..126542288fe 100644 --- a/frontend/cypress/integration/projects/overview.spec.ts +++ b/frontend/cypress/integration/projects/overview.spec.ts @@ -1,63 +1,23 @@ -/// +/// 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]; -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, - }, - }); - }); + 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"]'; - beforeEach(() => { - cy.login(); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } + before(() => { + cy.runBefore(); }); after(() => { @@ -82,6 +42,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 +54,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 +102,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 +120,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 9637a2bcb12..03512897c9e 100644 --- a/frontend/cypress/integration/projects/settings.spec.ts +++ b/frontend/cypress/integration/projects/settings.spec.ts @@ -1,16 +1,14 @@ /// -const randomId = String(Math.random()).split('.')[1]; -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; - describe('project settings', () => { + const randomId = String(Math.random()).split('.')[1]; + const baseUrl = Cypress.config().baseUrl; + const projectName = `stickiness-project-${randomId}`; + const TEST_STICKINESS = 'userId'; + const featureToggleName = `settings-${randomId}`; + let cleanFeature = false; + let cleanProject = false; + before(() => { cy.runBefore(); }); @@ -68,6 +66,7 @@ describe('project settings', () => { ); cy.get("[data-testid='ADD_VARIANT_BUTTON']").first().click(); + cy.wait(300); //then cy.get("[id='stickiness-select']") .first() diff --git a/frontend/cypress/integration/segments/segments.spec.ts b/frontend/cypress/integration/segments/segments.spec.ts index aa8b00ca973..4d7f0733d1f 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`); -}; +/// describe('segments', () => { + const randomId = String(Math.random()).split('.')[1]; + const segmentName = `unleash-e2e-${randomId}`; + let segmentId: string; + 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 00000000000..cde08909f7f --- /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 00000000000..a3f97ecf3dd --- /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 0d16067cc2e..b76560c09d1 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 a8f805772b2..c7cca105d05 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 00000000000..a49f6356d02 --- /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 1415f69d784..9289bb5aab7 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 => @@ -206,7 +209,7 @@ export const EnvironmentVariantsModal = ({ stickiness: variantsEdit?.length > 0 ? variantsEdit[0].stickiness - : 'default', + : defaultStickiness, new: true, isValid: false, id, @@ -303,14 +306,18 @@ export const EnvironmentVariantsModal = ({ setError(apiPayload.error); } }, [apiPayload.error]); + + const handleClose = () => { + updateStickiness(defaultStickiness).catch(console.warn); + setOpen(false); + }; + + if (loading || stickiness === '') { + return ; + } + return ( - { - setOpen(false); - }} - label="" - > + - { - setOpen(false); - }} - > + Cancel diff --git a/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx b/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx index bc1797739ee..bfd09d6a420 100644 --- a/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx +++ b/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx @@ -15,6 +15,7 @@ import { StickinessSelect } from './StickinessSelect/StickinessSelect'; import { useOptionalPathParam } from 'hooks/useOptionalPathParam'; import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings'; import Loader from '../../../common/Loader/Loader'; +import { useMemo } from 'react'; interface IFlexibleStrategyProps { parameters: IFeatureStrategyParameters; @@ -30,6 +31,7 @@ const FlexibleStrategy = ({ }: IFlexibleStrategyProps) => { 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 d8193e431b2..39f74898443 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 7e34bb45d4b..8c7a670cf90 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 0e069c59147..1f9add93b18 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 2dcb13545f9..37ee123501f 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" }] } diff --git a/src/lib/services/project-schema.ts b/src/lib/services/project-schema.ts index 6051f55cee9..ca6282b7b4b 100644 --- a/src/lib/services/project-schema.ts +++ b/src/lib/services/project-schema.ts @@ -8,9 +8,6 @@ export const projectSchema = joi name: joi.string().required(), description: joi.string().allow(null).allow('').optional(), mode: joi.string().valid('open', 'protected').default('open'), - defaultStickiness: joi - .string() - .valid('default', 'userId', 'sessionId', 'random') - .default('default'), + defaultStickiness: joi.string().default('default'), }) .options({ allowUnknown: false, stripUnknown: true });