From e059caf99330cc0d8e4362f2d3dd070b08333e07 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 8 Feb 2023 22:41:35 +0200 Subject: [PATCH] feat: Add e2e user invite test suite (no-changelog) (#5412) --- cypress/e2e/17-sharing.cy.ts | 53 ++++++++++++ cypress/pages/index.ts | 1 + cypress/pages/settings-users.ts | 3 + cypress/pages/sidebar/main-sidebar.ts | 8 ++ cypress/pages/workflow.ts | 11 ++- cypress/support/commands.ts | 86 +++++++++++++++---- cypress/support/index.ts | 15 ++++ .../src/components/InviteUsersModal.vue | 2 + .../components/MainHeader/WorkflowDetails.vue | 7 +- .../editor-ui/src/components/MainSidebar.vue | 24 ++---- packages/editor-ui/src/constants.ts | 1 + packages/editor-ui/src/router.ts | 19 +++- .../editor-ui/src/views/SettingsUsersView.vue | 1 + packages/editor-ui/src/views/SignoutView.vue | 31 +++++++ 14 files changed, 227 insertions(+), 35 deletions(-) create mode 100644 cypress/e2e/17-sharing.cy.ts create mode 100644 packages/editor-ui/src/views/SignoutView.vue diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts new file mode 100644 index 0000000000..ba93c3b72e --- /dev/null +++ b/cypress/e2e/17-sharing.cy.ts @@ -0,0 +1,53 @@ +import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; + +/** + * User A - Instance owner + * User B - User, owns C1, W1, W2 + * User C - User, owns C2 + * + * W1 - Workflow owned by User B, shared with User C + * W2 - Workflow owned by User B + * + * C1 - Credential owned by User B + * C2 - Credential owned by User C, shared with User A and User B + */ + +const instanceOwner = { + email: `${DEFAULT_USER_EMAIL}A`, + password: DEFAULT_USER_PASSWORD, + firstName: 'User', + lastName: 'A', +}; + +const users = [ + { + email: `${DEFAULT_USER_EMAIL}B`, + password: DEFAULT_USER_PASSWORD, + firstName: 'User', + lastName: 'B', + }, + { + email: `${DEFAULT_USER_EMAIL}C`, + password: DEFAULT_USER_PASSWORD, + firstName: 'User', + lastName: 'C', + }, +]; + +describe('Sharing', () => { + before(() => { + cy.resetAll(); + cy.setupOwner(instanceOwner); + }); + + beforeEach(() => { + cy.on('uncaught:exception', (err, runnable) => { + expect(err.message).to.include('Not logged in'); + return false; + }); + }); + + it(`should invite User A and UserB to instance`, () => { + cy.inviteUsers({ instanceOwner, users }); + }); +}); diff --git a/cypress/pages/index.ts b/cypress/pages/index.ts index 870fbaa58c..7b4c1e0931 100644 --- a/cypress/pages/index.ts +++ b/cypress/pages/index.ts @@ -7,5 +7,6 @@ export * from './workflow'; export * from './modals'; export * from './settings-users'; export * from './settings-log-streaming'; +export * from './sidebar'; export * from './ndv'; export * from './canvas-node'; diff --git a/cypress/pages/settings-users.ts b/cypress/pages/settings-users.ts index 8e5002680e..ae0048c937 100644 --- a/cypress/pages/settings-users.ts +++ b/cypress/pages/settings-users.ts @@ -4,6 +4,9 @@ export class SettingsUsersPage extends BasePage { url = '/settings/users'; getters = { setUpOwnerButton: () => cy.getByTestId('action-box').find('button').first(), + inviteButton: () => cy.getByTestId('settings-users-invite-button').last(), + inviteUsersModal: () => cy.getByTestId('inviteUser-modal').last(), + inviteUsersModalEmailsInput: () => cy.getByTestId('emails').find('input').first(), }; actions = { goToOwnerSetup: () => this.getters.setUpOwnerButton().click(), diff --git a/cypress/pages/sidebar/main-sidebar.ts b/cypress/pages/sidebar/main-sidebar.ts index e117a7f32d..b86ed326f1 100644 --- a/cypress/pages/sidebar/main-sidebar.ts +++ b/cypress/pages/sidebar/main-sidebar.ts @@ -9,6 +9,7 @@ export class MainSidebar extends BasePage { workflows: () => this.getters.menuItem('Workflows'), credentials: () => this.getters.menuItem('Credentials'), executions: () => this.getters.menuItem('Executions'), + userMenu: () => cy.getByTestId('main-sidebar-user-menu'), }; actions = { goToSettings: () => { @@ -22,5 +23,12 @@ export class MainSidebar extends BasePage { cy.get('[data-old-overflow]').should('not.exist'); this.getters.credentials().click(); }, + openUserMenu: () => { + this.getters.userMenu().find('[role="button"]').last().click(); + }, + signout: () => { + this.actions.openUserMenu(); + cy.getByTestId('workflow-menu-item-logout').click(); + }, }; } diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 80ef05b3cc..6f4f49727b 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -78,6 +78,8 @@ export class WorkflowPage extends BasePage { workflowSettingsSaveButton: () => cy.getByTestId('workflow-settings-save-button').find('button'), + shareButton: () => cy.getByTestId('workflow-share-button').find('button'), + duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'), nodeViewBackground: () => cy.getByTestId('node-view-background'), nodeView: () => cy.getByTestId('node-view'), @@ -109,7 +111,11 @@ export class WorkflowPage extends BasePage { if (keepNdvOpen) return; cy.get('body').type('{esc}'); }, - addNodeToCanvas: (nodeDisplayName: string, plusButtonClick = true, preventNdvClose?: boolean) => { + addNodeToCanvas: ( + nodeDisplayName: string, + plusButtonClick = true, + preventNdvClose?: boolean, + ) => { if (plusButtonClick) { this.getters.nodeCreatorPlusButton().click(); } @@ -133,6 +139,9 @@ export class WorkflowPage extends BasePage { openWorkflowMenu: () => { this.getters.workflowMenu().click(); }, + openShareModal: () => { + this.getters.shareButton().click(); + }, saveWorkflowOnButtonClick: () => { this.getters.saveButton().should('contain', 'Save'); this.getters.saveButton().click(); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index c5e9f898ce..571c240827 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -23,8 +23,8 @@ // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -import "cypress-real-events"; -import { WorkflowsPage, SigninPage, SignupPage } from '../pages'; +import 'cypress-real-events'; +import { WorkflowsPage, SigninPage, SignupPage, SettingsUsersPage } from '../pages'; import { N8N_AUTH_COOKIE } from '../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { MessageBox } from '../pages/modals/message-box'; @@ -87,6 +87,28 @@ Cypress.Commands.add('signin', ({ email, password }) => { ); }); +Cypress.Commands.add('signout', () => { + cy.visit('/signout'); + cy.waitForLoad(); + cy.url().should('include', '/signin'); + cy.getCookie(N8N_AUTH_COOKIE).should('not.exist'); +}); + +Cypress.Commands.add('signup', ({ firstName, lastName, password, url }) => { + const signupPage = new SignupPage(); + + cy.visit(url); + + signupPage.getters.form().within(() => { + cy.url().then((url) => { + signupPage.getters.firstName().type(firstName); + signupPage.getters.lastName().type(lastName); + signupPage.getters.password().type(password); + signupPage.getters.submit().click(); + }); + }); +}); + Cypress.Commands.add('setup', ({ email, firstName, lastName, password }) => { const signupPage = new SignupPage(); @@ -94,7 +116,7 @@ Cypress.Commands.add('setup', ({ email, firstName, lastName, password }) => { signupPage.getters.form().within(() => { cy.url().then((url) => { - if (url.endsWith(signupPage.url)) { + if (url.includes(signupPage.url)) { signupPage.getters.email().type(email); signupPage.getters.firstName().type(firstName); signupPage.getters.lastName().type(lastName); @@ -107,6 +129,36 @@ Cypress.Commands.add('setup', ({ email, firstName, lastName, password }) => { }); }); +Cypress.Commands.add('interceptREST', (method, url) => { + cy.intercept(method, `http://localhost:5678/rest${url}`); +}); + +Cypress.Commands.add('inviteUsers', ({ instanceOwner, users }) => { + const settingsUsersPage = new SettingsUsersPage(); + + cy.signin(instanceOwner); + + users.forEach((user) => { + cy.signin(instanceOwner); + cy.visit(settingsUsersPage.url); + + cy.interceptREST('POST', '/users').as('inviteUser'); + + settingsUsersPage.getters.inviteButton().click(); + settingsUsersPage.getters.inviteUsersModal().within((modal) => { + settingsUsersPage.getters.inviteUsersModalEmailsInput().type(user.email).type('{enter}'); + }); + + cy.wait('@inviteUser').then((interception) => { + const inviteLink = interception.response!.body.data[0].user.inviteAcceptUrl; + cy.log(JSON.stringify(interception.response!.body.data[0].user)); + cy.log(inviteLink); + cy.signout(); + cy.signup({ ...user, url: inviteLink }); + }); + }); +}); + Cypress.Commands.add('skipSetup', () => { const signupPage = new SignupPage(); const workflowsPage = new WorkflowsPage(); @@ -194,20 +246,20 @@ Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => { cy.get(draggableSelector).should('exist'); cy.get(droppableSelector).should('exist'); - cy.get(droppableSelector).first().then(([$el]) => { - const coords = $el.getBoundingClientRect(); + cy.get(droppableSelector) + .first() + .then(([$el]) => { + const coords = $el.getBoundingClientRect(); - const pageX = coords.left + coords.width / 2; - const pageY = coords.top + coords.height / 2; + const pageX = coords.left + coords.width / 2; + const pageY = coords.top + coords.height / 2; - // We can't use realMouseDown here because it hangs headless run - cy.get(draggableSelector).trigger('mousedown'); - // We don't chain these commands to make sure cy.get is re-trying correctly - cy.get(droppableSelector).realMouseMove(pageX, pageY) - cy.get(droppableSelector).realHover() - cy.get(droppableSelector).realMouseUp(); - cy.get(draggableSelector).realMouseUp(); - }) + // We can't use realMouseDown here because it hangs headless run + cy.get(draggableSelector).trigger('mousedown'); + // We don't chain these commands to make sure cy.get is re-trying correctly + cy.get(droppableSelector).realMouseMove(pageX, pageY); + cy.get(droppableSelector).realHover(); + cy.get(droppableSelector).realMouseUp(); + cy.get(draggableSelector).realMouseUp(); + }); }); - - diff --git a/cypress/support/index.ts b/cypress/support/index.ts index f0417ce8cc..8eee49e10f 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -1,6 +1,8 @@ // Load type definitions that come with Cypress module /// +import { Interception } from 'cypress/types/net-stubbing'; + interface SigninPayload { email: string; password: string; @@ -13,6 +15,15 @@ interface SetupPayload { lastName: string; } +interface SignupPayload extends SetupPayload { + url: string; +} + +interface InviteUsersPayload { + instanceOwner: SigninPayload; + users: SetupPayload[]; +} + declare global { namespace Cypress { interface Chainable { @@ -23,8 +34,12 @@ declare global { findChildByTestId(childTestId: string): Chainable>; createFixtureWorkflow(fixtureKey: string, workflowName: string): void; signin(payload: SigninPayload): void; + signout(): void; + signup(payload: SignupPayload): void; setup(payload: SetupPayload): void; setupOwner(payload: SetupPayload): void; + inviteUsers(payload: InviteUsersPayload): void; + interceptREST(method: string, url: string): Chainable; skipSetup(): void; resetAll(): void; enableFeature(feature: string): void; diff --git a/packages/editor-ui/src/components/InviteUsersModal.vue b/packages/editor-ui/src/components/InviteUsersModal.vue index 17864b97b5..db1f1fead3 100644 --- a/packages/editor-ui/src/components/InviteUsersModal.vue +++ b/packages/editor-ui/src/components/InviteUsersModal.vue @@ -22,6 +22,8 @@ diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 02e054fe7f..199e659f62 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -62,7 +62,12 @@ - + {{ $locale.baseText('workflowDetails.share') }}