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" }]
}