From 1c36c37a122c58bb43a34ecb95e2a52fbe069522 Mon Sep 17 00:00:00 2001 From: OlegIvaniv Date: Wed, 7 Dec 2022 18:16:38 +0100 Subject: [PATCH] test(editor): Fix flaky e2e tests (#4779) * test(editor): Fix default-owner and credentials saving e2e specs * test(editor): fix webhook node test * test(editor): add cy command for browser permissions * test(editor): add cy command for reading clipboard * Fix 3-default-owner spec * Resolve review comments * Merge spec * Fix http node and expression editor modal specs * Add optional param to credentials modal saving action to wait for the test endpoint * Improve sidebar items clicking and increase credentials saving timeout * Rename http e2e spec to fix ordering * Fix pasting and copying of nodes e2e spec * Make sure to only access error.cause if it exists * Wait longer for the keyboard press * Make sure to focus the body when typing * Try type delay * Use meta key based on the running platform * Fix flaky workflowTagElements getter Co-authored-by: Csaba Tuncsik --- cypress/e2e/3-default-owner.cy.ts | 110 +++++++++--------- cypress/e2e/5-ndv.cy.ts | 14 +-- cypress/e2e/7-workflow-actions.cy.ts | 10 +- ...t-node.cy.ts => 8-http-request-node.cy.ts} | 4 +- cypress/e2e/9-expression-editor-modal.cy.ts | 2 +- cypress/pages/modals/credentials-modal.ts | 13 ++- cypress/pages/sidebar/main-sidebar.ts | 24 ++-- cypress/pages/workflow.ts | 4 +- cypress/support/commands.ts | 12 ++ cypress/support/index.ts | 2 + packages/cli/src/CredentialsHelper.ts | 2 +- .../src/components/N8nMenuItem/MenuItem.vue | 4 +- 12 files changed, 109 insertions(+), 92 deletions(-) rename cypress/e2e/{4-http-request-node.cy.ts => 8-http-request-node.cy.ts} (82%) diff --git a/cypress/e2e/3-default-owner.cy.ts b/cypress/e2e/3-default-owner.cy.ts index 59500954f15be..1fe111edc1d80 100644 --- a/cypress/e2e/3-default-owner.cy.ts +++ b/cypress/e2e/3-default-owner.cy.ts @@ -24,92 +24,86 @@ const firstName = randFirstName(); const lastName = randLastName(); describe('Default owner', () => { - // todo test should redirect to setup if have not skipped - - beforeEach(() => { + before(() => { cy.resetAll(); }); + beforeEach(() => { + cy.visit('/'); + }) - it('should be able to use n8n without user management and setup UM', () => { - describe('should skip owner setup', () => { - cy.skipSetup(); - cy.url().should('include', workflowsPage.url); - }); - - describe('should be able to create workflows', () => { - workflowsPage.getters.newWorkflowButtonCard().should('be.visible'); - workflowsPage.getters.newWorkflowButtonCard().click(); + it('should skip owner setup', () => { + cy.skipSetup(); + }); - cy.createFixtureWorkflow('Test_workflow_1.json', `Test workflow`); + it('should be able to create workflows', () => { + workflowsPage.getters.newWorkflowButtonCard().should('be.visible'); + workflowsPage.getters.newWorkflowButtonCard().click(); - // reload page, ensure owner still has access - cy.reload(); + cy.createFixtureWorkflow('Test_workflow_1.json', `Test workflow`); - workflowPage.getters.workflowNameInput().should('contain.value', 'Test workflow'); - }); + // reload page, ensure owner still has access + cy.reload(); - describe('should be able to add new credentials', () => { - cy.visit(credentialsPage.url); + workflowPage.getters.workflowNameInput().should('contain.value', 'Test workflow'); + }); - credentialsPage.getters.emptyListCreateCredentialButton().click(); + it('should be able to add new credentials', () => { + cy.visit(credentialsPage.url); - credentialsModal.getters.newCredentialModal().should('be.visible'); - credentialsModal.getters.newCredentialTypeSelect().should('be.visible'); - credentialsModal.getters.newCredentialTypeOption('Notion API').click(); + credentialsPage.getters.emptyListCreateCredentialButton().click(); - credentialsModal.getters.newCredentialTypeButton().click(); + credentialsModal.getters.newCredentialModal().should('be.visible'); + credentialsModal.getters.newCredentialTypeSelect().should('be.visible'); + credentialsModal.getters.newCredentialTypeOption('Notion API').click(); - credentialsModal.getters.connectionParameter('API Key').type('1234567890'); + credentialsModal.getters.newCredentialTypeButton().click(); - credentialsModal.actions.setName('My awesome Notion account'); - credentialsModal.actions.save(); + credentialsModal.getters.connectionParameter('API Key').type('1234567890'); - credentialsModal.actions.close(); + credentialsModal.actions.setName('My awesome Notion account'); + credentialsModal.actions.save(); - credentialsModal.getters.newCredentialModal().should('not.exist'); - credentialsModal.getters.editCredentialModal().should('not.exist'); + credentialsModal.actions.close(); - credentialsPage.getters.credentialCards().should('have.length', 1); - }); + credentialsModal.getters.newCredentialModal().should('not.exist'); + credentialsModal.getters.editCredentialModal().should('not.exist'); - describe('should be able to setup UM from settings', () => { - mainSidebar.getters.settings().should('be.visible'); - mainSidebar.actions.goToSettings(); - cy.url().should('include', settingsUsersPage.url); + credentialsPage.getters.credentialCards().should('have.length', 1); + }); - settingsUsersPage.actions.goToOwnerSetup(); + it('should be able to setup UM from settings', () => { + mainSidebar.getters.settings().should('be.visible'); + mainSidebar.actions.goToSettings(); + cy.url().should('include', settingsUsersPage.url); - cy.url().should('include', signupPage.url); - }); + settingsUsersPage.actions.goToOwnerSetup(); - describe('should be able to setup instance and migrate workflows and credentials', () => { - cy.setup({ email, firstName, lastName, password }); + cy.url().should('include', signupPage.url); + }); - messageBox.getters.content().should('contain.text', '1 existing workflow and 1 credential') + it('should be able to setup instance and migrate workflows and credentials', () => { + cy.setup({ email, firstName, lastName, password }); - messageBox.actions.confirm(); - }); + messageBox.getters.content().should('contain.text', '1 existing workflow and 1 credential') - describe('should be redirected back to users page after setup', () => { - cy.url().should('include', settingsUsersPage.url); - // todo test users and that owner exist - }); + messageBox.actions.confirm(); + cy.url().should('include', settingsUsersPage.url); + settingsSidebar.actions.back(); - describe('can click back to workflows and have migrated workflow after setup', () => { - settingsSidebar.actions.back(); + cy.url().should('include', workflowsPage.url); - cy.url().should('include', workflowsPage.url); + workflowsPage.getters.workflowCards().should('have.length', 1); + }); - workflowsPage.getters.workflowCards().should('have.length', 1); - }); + it('can click back to main menu and have migrated credential after setup', () => { + cy.signin({ email, password }); + cy.visit(workflowsPage.url); - describe('can click back to main menu and have migrated credential after setup', () => { - mainSidebar.actions.goToCredentials(); + mainSidebar.actions.goToCredentials(); - cy.url().should('include', workflowsPage.url); + cy.url().should('include', credentialsPage.url); - workflowsPage.getters.workflowCards().should('have.length', 1); - }); + credentialsPage.getters.credentialCards().should('have.length', 1); }); }); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 69f25fd361c4e..c9fee28e2b8f6 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -29,19 +29,9 @@ describe('NDV', () => { ndv.getters.nodeExecuteButton().first().click(); ndv.getters.copyInput().click(); - cy.wrap(Cypress.automation('remote:debugger:protocol', { - command: 'Browser.grantPermissions', - params: { - permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'], - origin: window.location.origin, - }, - })); + cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); - cy.window().its('navigator.permissions') - .invoke('query', {name: 'clipboard-read'}) - .its('state').should('equal', 'granted'); - - cy.window().its('navigator.clipboard').invoke('readText').then(url => { + cy.readClipboard().then(url => { cy.request({ method: 'GET', url, diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index 9891d6a387de7..495614d569a9a 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -90,10 +90,16 @@ describe('Workflow Actions', () => { }); it('should copy nodes', () => { + const metaKey = Cypress.platform === 'darwin' ? '{meta}' : '{ctrl}'; + WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE); - cy.get('body').type('{meta}', { release: false }).type('a'); - cy.get('body').type('{meta}', { release: false }).type('c'); + WorkflowPage.getters.canvasNodes().should('have.have.length', 2); + + cy.get("#node-creator").should('not.exist'); + cy.get('body').type(metaKey, { delay: 500, release: false }).type('a'); + cy.get('.jtk-drag-selected').should('have.length', 2); + cy.get('body').type(metaKey, { delay: 500, release: false }).type('c'); WorkflowPage.getters.successToast().should('exist'); }); diff --git a/cypress/e2e/4-http-request-node.cy.ts b/cypress/e2e/8-http-request-node.cy.ts similarity index 82% rename from cypress/e2e/4-http-request-node.cy.ts rename to cypress/e2e/8-http-request-node.cy.ts index e0803a93a6c64..e4a78205d4338 100644 --- a/cypress/e2e/4-http-request-node.cy.ts +++ b/cypress/e2e/8-http-request-node.cy.ts @@ -15,10 +15,10 @@ describe('HTTP Request node', () => { WorkflowPage.actions.addInitialNodeToCanvas('Manual Trigger'); WorkflowPage.actions.addNodeToCanvas('HTTP Request'); WorkflowPage.actions.openNodeNdv('HTTP Request'); - WorkflowPage.actions.typeIntoParameterInput('url', 'https://google.com'); + WorkflowPage.actions.typeIntoParameterInput('url', 'https://catfact.ninja/fact'); WorkflowPage.actions.executeNodeFromNdv(); - WorkflowPage.getters.ndvOutputPanel().contains(''); + WorkflowPage.getters.ndvOutputPanel().contains('fact'); }); }); diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts index f6aabae4df70a..9e0db82665a72 100644 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ b/cypress/e2e/9-expression-editor-modal.cy.ts @@ -4,7 +4,7 @@ const WorkflowPage = new WorkflowPageClass(); describe('Expression editor modal', () => { before(() => { - cy.task('db:reset'); + cy.task('reset'); cy.skipSetup(); }); diff --git a/cypress/pages/modals/credentials-modal.ts b/cypress/pages/modals/credentials-modal.ts index 39fd613065e5e..b4046382fcab3 100644 --- a/cypress/pages/modals/credentials-modal.ts +++ b/cypress/pages/modals/credentials-modal.ts @@ -13,7 +13,8 @@ export class CredentialsModal extends BasePage { .find('.n8n-input input'), name: () => cy.getByTestId('credential-name'), nameInput: () => cy.getByTestId('credential-name').find('input'), - saveButton: () => cy.getByTestId('credential-save-button'), + // Saving of the credentials takes a while on the CI so we need to increase the timeout + saveButton: () => cy.getByTestId('credential-save-button', { timeout: 5000 }), closeButton: () => this.getters.editCredentialModal().find('.el-dialog__close').first(), }; actions = { @@ -21,12 +22,16 @@ export class CredentialsModal extends BasePage { this.getters.name().click(); this.getters.nameInput().clear().type(name); }, - save: () => { + save: (test = false) => { cy.intercept('POST', '/rest/credentials').as('saveCredential'); - cy.intercept('POST', '/rest/credentials/test').as('testCredential'); + if(test) { + cy.intercept('POST', '/rest/credentials/test').as('testCredential'); + } this.getters.saveButton().click(); - cy.wait('@saveCredential').wait('@testCredential'); + + cy.wait('@saveCredential'); + if(test) cy.wait('@testCredential') this.getters.saveButton().should('contain.text', 'Saved'); }, close: () => { diff --git a/cypress/pages/sidebar/main-sidebar.ts b/cypress/pages/sidebar/main-sidebar.ts index 85e853627a5f0..d9398961e9ae8 100644 --- a/cypress/pages/sidebar/main-sidebar.ts +++ b/cypress/pages/sidebar/main-sidebar.ts @@ -2,14 +2,24 @@ import { BasePage } from "../base"; export class MainSidebar extends BasePage { getters = { - settings: () => cy.getByTestId('menu-item-settings', { timeout: 5000 }), - templates: () => cy.getByTestId('menu-item-templates'), - workflows: () => cy.getByTestId('menu-item-workflows'), - credentials: () => cy.getByTestId('menu-item-credentials'), - executions: () => cy.getByTestId('menu-item-executions'), + menuItem: (menuLabel: string) => cy.getByTestId('menu-item').filter(`:contains("${menuLabel}")`), + settings: () => this.getters.menuItem('Settings'), + templates: () => this.getters.menuItem('Templates'), + workflows: () => this.getters.menuItem('Workflows'), + credentials: () => this.getters.menuItem('Credentials'), + executions: () => this.getters.menuItem('Executions'), }; actions = { - goToSettings: () => this.getters.settings().click(), - goToCredentials: () => this.getters.credentials().click(), + goToSettings: () => { + this.getters.settings().should('be.visible'); + // We must wait before ElementUI menu is done with its animations + cy.get('[data-old-overflow]').should('not.exist'); + this.getters.settings().click(); + }, + goToCredentials: () => { + this.getters.credentials().should('be.visible'); + cy.get('[data-old-overflow]').should('not.exist'); + this.getters.credentials().click() + }, }; } diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index ed68f676856ce..77001f2fc0dd2 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -9,7 +9,7 @@ export class WorkflowPage extends BasePage { workflowTags: () => cy.getByTestId('workflow-tags'), workflowTagsContainer: () => cy.getByTestId('workflow-tags-container'), workflowTagsInput: () => this.getters.workflowTagsContainer().then(($el) => cy.wrap($el.find('input').first())), - workflowTagElements: () => this.getters.workflowTagsContainer().find('span.tags').children(), + workflowTagElements: () => cy.get('[data-test-id="workflow-tags-container"] span.tags > span'), workflowTagsDropdown: () => cy.getByTestId('workflow-tags-dropdown'), newTagLink: () => cy.getByTestId('new-tag-link'), saveButton: () => cy.getByTestId('workflow-save-button'), @@ -22,7 +22,6 @@ export class WorkflowPage extends BasePage { cy.getByTestId(`parameter-input-${parameterName}`), ndvOutputPanel: () => cy.getByTestId('output-panel'), ndvRunDataPaneHeader: () => cy.getByTestId('run-data-pane-header'), - successToast: () => cy.get('.el-notification__title'), activatorSwitch: () => cy.getByTestId('workflow-activate-switch'), workflowMenu: () => cy.getByTestId('workflow-menu'), @@ -34,7 +33,6 @@ export class WorkflowPage extends BasePage { nodeViewRoot: () => cy.getByTestId('node-view-root'), copyPasteInput: () => cy.getByTestId('hidden-copy-paste'), - canvasNodes: () => cy.getByTestId('canvas-node'), }; actions = { visit: () => { diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 8a3d135901a5f..3318305a475ee 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -129,6 +129,18 @@ Cypress.Commands.add('setupOwner', (payload) => { cy.task('setup-owner', payload); }); +Cypress.Commands.add('grantBrowserPermissions', (...permissions: string[]) => { + if(Cypress.isBrowser('chrome')) { + cy.wrap(Cypress.automation('remote:debugger:protocol', { + command: 'Browser.grantPermissions', + params: { + permissions, + origin: window.location.origin, + }, + })); + } +}); +Cypress.Commands.add('readClipboard', () => cy.window().its('navigator.clipboard').invoke('readText')); Cypress.Commands.add('paste', { prevSubject: true }, (selector, pastePayload) => { // https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event cy.wrap(selector).then($destination => { diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 40d7c0840a4a2..f03e681b19c8c 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -24,6 +24,8 @@ declare global { setupOwner(payload: SetupPayload): void; skipSetup(): void; resetAll(): void; + grantBrowserPermissions(...permissions: string[]): void; + readClipboard(): Chainable; paste(pastePayload: string): void, } } diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index cecb8ad5ceba4..1f3824610a39c 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -695,7 +695,7 @@ export class CredentialsHelper extends ICredentialsHelper { `Received HTTP status code: ${errorResponseData.statusCode}`, }; } - } else if (error.cause.code) { + } else if (error.cause?.code) { return { status: 'Error', message: error.cause.code, diff --git a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue index c1c7f58527e5d..7bcc61341b7bd 100644 --- a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue +++ b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue @@ -30,7 +30,7 @@ [$style.disableActiveStyle]: !isItemActive(child), [$style.active]: isItemActive(child), }" - :data-test-id="`menu-item-${child.id}`" + data-test-id="menu-item" :index="child.id" @click="onItemClick(child)" > @@ -54,7 +54,7 @@ [$style.active]: isItemActive(item), [$style.compact]: compact, }" - :data-test-id="`menu-item-${item.id}`" + data-test-id="menu-item" :index="item.id" @click="onItemClick(item)" >