diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index 49c37fd4c8..b7dadc4173 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -40,7 +40,7 @@ on: containers: description: 'Number of containers to run tests in.' required: false - default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]' + default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]' type: string pr_number: description: 'PR number to run tests for.' diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js index dc6abe3a71..36fc0af7e2 100644 --- a/cypress/.eslintrc.js +++ b/cypress/.eslintrc.js @@ -4,10 +4,16 @@ const sharedOptions = require('@n8n_io/eslint-config/shared'); * @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/base'], + extends: ['@n8n_io/eslint-config/base', 'plugin:cypress/recommended'], ...sharedOptions(__dirname), + plugins: ['cypress'], + + env: { + 'cypress/globals': true, + }, + rules: { // TODO: remove these rules '@typescript-eslint/no-explicit-any': 'off', @@ -20,5 +26,9 @@ module.exports = { '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/promise-function-async': 'off', 'n8n-local-rules/no-uncaught-json-parse': 'off', + + 'cypress/no-assigning-return-values': 'warn', + 'cypress/no-unnecessary-waiting': 'warn', + 'cypress/unsafe-to-chain-command': 'warn', }, }; diff --git a/cypress/constants.ts b/cypress/constants.ts index 39c755738c..7efd9b0470 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -59,7 +59,7 @@ export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model' export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser'; export const WEBHOOK_NODE_NAME = 'Webhook'; -export const META_KEY = Cypress.platform === 'darwin' ? '{meta}' : '{ctrl}'; +export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl'; export const NEW_GOOGLE_ACCOUNT_NAME = 'Gmail account'; export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account'; diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts index 19465ed749..89d1bad696 100644 --- a/cypress/e2e/10-undo-redo.cy.ts +++ b/cypress/e2e/10-undo-redo.cy.ts @@ -122,8 +122,7 @@ describe('Undo/Redo', () => { WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); cy.get('body').type('{esc}'); cy.get('body').type('{esc}'); - WorkflowPage.actions.selectAll(); - cy.get('body').type('{backspace}'); + WorkflowPage.actions.hitDeleteAllNodes(); WorkflowPage.getters.canvasNodes().should('have.have.length', 0); WorkflowPage.actions.hitUndo(); WorkflowPage.getters.canvasNodes().should('have.have.length', 2); @@ -208,7 +207,7 @@ describe('Undo/Redo', () => { WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); cy.get('body').type('{esc}'); cy.get('body').type('{esc}'); - WorkflowPage.actions.selectAll(); + WorkflowPage.actions.hitSelectAll(); WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.getters.disabledNodes().should('have.length', 2); WorkflowPage.actions.hitUndo(); diff --git a/cypress/e2e/12-canvas-actions.cy.ts b/cypress/e2e/12-canvas-actions.cy.ts index fae27a545c..fafb1d9d79 100644 --- a/cypress/e2e/12-canvas-actions.cy.ts +++ b/cypress/e2e/12-canvas-actions.cy.ts @@ -199,7 +199,7 @@ describe('Canvas Actions', () => { it('should copy selected nodes', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - WorkflowPage.actions.selectAll(); + WorkflowPage.actions.hitSelectAll(); WorkflowPage.actions.hitCopy(); successToast().should('contain', 'Copied!'); @@ -211,7 +211,7 @@ describe('Canvas Actions', () => { it('should select/deselect all nodes', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - WorkflowPage.actions.selectAll(); + WorkflowPage.actions.hitSelectAll(); WorkflowPage.getters.selectedNodes().should('have.length', 2); WorkflowPage.actions.deselectAll(); WorkflowPage.getters.selectedNodes().should('have.length', 0); diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index db6b38d53a..79b6c200e1 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -164,8 +164,7 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); cy.wait(500); - WorkflowPage.actions.selectAll(); - cy.get('body').type('{backspace}'); + WorkflowPage.actions.hitDeleteAllNodes(); WorkflowPage.getters.canvasNodes().should('have.length', 0); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); @@ -181,8 +180,7 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); cy.wait(500); - WorkflowPage.actions.selectAll(); - cy.get('body').type('{backspace}'); + WorkflowPage.actions.hitDeleteAllNodes(); WorkflowPage.getters.canvasNodes().should('have.length', 0); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); @@ -315,7 +313,7 @@ describe('Canvas Node Manipulation and Navigation', () => { cy.get('body').type('{esc}'); // Keyboard shortcut - WorkflowPage.actions.selectAll(); + WorkflowPage.actions.hitSelectAll(); WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.getters.disabledNodes().should('have.length', 2); WorkflowPage.actions.hitDisableNodeShortcut(); @@ -324,12 +322,12 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.getters.disabledNodes().should('have.length', 1); - WorkflowPage.actions.selectAll(); + WorkflowPage.actions.hitSelectAll(); WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.getters.disabledNodes().should('have.length', 2); // Context menu - WorkflowPage.actions.selectAll(); + WorkflowPage.actions.hitSelectAll(); WorkflowPage.actions.openContextMenu(); WorkflowPage.actions.contextMenuAction('toggle_activation'); WorkflowPage.getters.disabledNodes().should('have.length', 0); @@ -341,7 +339,7 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.actions.openContextMenu(); WorkflowPage.actions.contextMenuAction('toggle_activation'); WorkflowPage.getters.disabledNodes().should('have.length', 1); - WorkflowPage.actions.selectAll(); + WorkflowPage.actions.hitSelectAll(); WorkflowPage.actions.openContextMenu(); WorkflowPage.actions.contextMenuAction('toggle_activation'); WorkflowPage.getters.disabledNodes().should('have.length', 2); @@ -383,8 +381,8 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.getters.canvasNodes().should('have.length', 3); WorkflowPage.getters.nodeConnections().should('have.length', 1); - WorkflowPage.actions.selectAll(); - WorkflowPage.actions.hitDuplicateNodeShortcut(); + WorkflowPage.actions.hitSelectAll(); + WorkflowPage.actions.hitDuplicateNode(); WorkflowPage.getters.canvasNodes().should('have.length', 5); }); diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts index 7908e8d128..cce375b05d 100644 --- a/cypress/e2e/17-sharing.cy.ts +++ b/cypress/e2e/17-sharing.cy.ts @@ -34,7 +34,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { let workflowW2Url = ''; it('should create C1, W1, W2, share W1 with U3, as U2', () => { - cy.signin(INSTANCE_MEMBERS[0]); + cy.signinAsMember(0); cy.visit(credentialsPage.url); credentialsPage.getters.emptyListCreateCredentialButton().click(); @@ -67,7 +67,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { }); it('should create C2, share C2 with U1 and U2, as U3', () => { - cy.signin(INSTANCE_MEMBERS[1]); + cy.signinAsMember(1); cy.visit(credentialsPage.url); credentialsPage.getters.emptyListCreateCredentialButton().click(); @@ -83,7 +83,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { }); it('should open W1, add node using C2 as U3', () => { - cy.signin(INSTANCE_MEMBERS[1]); + cy.signinAsMember(1); cy.visit(workflowsPage.url); workflowsPage.getters.workflowCards().should('have.length', 1); @@ -99,7 +99,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { }); it('should open W1, add node using C2 as U2', () => { - cy.signin(INSTANCE_MEMBERS[0]); + cy.signinAsMember(0); cy.visit(workflowsPage.url); workflowsPage.getters.workflowCards().should('have.length', 2); @@ -119,7 +119,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { }); it('should not have access to W2, as U3', () => { - cy.signin(INSTANCE_MEMBERS[1]); + cy.signinAsMember(1); cy.visit(workflowW2Url); cy.waitForLoad(); @@ -128,7 +128,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { }); it('should have access to W1, W2, as U1', () => { - cy.signin(INSTANCE_OWNER); + cy.signinAsOwner(); cy.visit(workflowsPage.url); workflowsPage.getters.workflowCards().should('have.length', 2); @@ -144,7 +144,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { }); it('should automatically test C2 when opened by U2 sharee', () => { - cy.signin(INSTANCE_MEMBERS[0]); + cy.signinAsMember(0); cy.visit(credentialsPage.url); credentialsPage.getters.credentialCard('Credential C2').click(); @@ -152,7 +152,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { }); it('should work for admin role on credentials created by others (also can share it with themselves)', () => { - cy.signin(INSTANCE_MEMBERS[0]); + cy.signinAsMember(0); cy.visit(credentialsPage.url); credentialsPage.getters.createCredentialButton().click(); @@ -164,7 +164,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { credentialsModal.actions.close(); cy.signout(); - cy.signin(INSTANCE_ADMIN); + cy.signinAsAdmin(); cy.visit(credentialsPage.url); credentialsPage.getters.credentialCard('Credential C3').click(); credentialsModal.getters.testSuccessTag().should('be.visible'); diff --git a/cypress/e2e/18-user-management.cy.ts b/cypress/e2e/18-user-management.cy.ts index c9f3cc08cb..b53b0fdf53 100644 --- a/cypress/e2e/18-user-management.cy.ts +++ b/cypress/e2e/18-user-management.cy.ts @@ -34,7 +34,7 @@ describe('User Management', { disableAutoLogin: true }, () => { cy.enableFeature('sharing'); }); - it.only('should login and logout', () => { + it('should login and logout', () => { cy.visit('/'); cy.get('input[name="email"]').type(INSTANCE_OWNER.email); cy.get('input[name="password"]').type(INSTANCE_OWNER.password); diff --git a/cypress/e2e/25-stickies.cy.ts b/cypress/e2e/25-stickies.cy.ts index 35416ebd3e..2b83f9abf9 100644 --- a/cypress/e2e/25-stickies.cy.ts +++ b/cypress/e2e/25-stickies.cy.ts @@ -34,15 +34,12 @@ describe('Canvas Actions', () => { addDefaultSticky(); workflowPage.actions.deselectAll(); workflowPage.actions.addStickyFromContextMenu(); - workflowPage.actions.hitAddStickyShortcut(); + workflowPage.actions.hitAddSticky(); workflowPage.getters.stickies().should('have.length', 3); // Should not add a sticky for ctrl+shift+s - cy.get('body') - .type(META_KEY, { delay: 500, release: false }) - .type('{shift}', { release: false }) - .type('s'); + cy.get('body').type(`{${META_KEY}+shift+s}`); workflowPage.getters.stickies().should('have.length', 3); workflowPage.getters diff --git a/cypress/e2e/27-cloud.cy.ts b/cypress/e2e/27-cloud.cy.ts index 6fdcfba295..dd0d3b06ba 100644 --- a/cypress/e2e/27-cloud.cy.ts +++ b/cypress/e2e/27-cloud.cy.ts @@ -6,13 +6,12 @@ import { getPublicApiUpgradeCTA, } from '../pages'; import planData from '../fixtures/Plan_data_opt_in_trial.json'; -import { INSTANCE_OWNER } from '../constants'; const mainSidebar = new MainSidebar(); const bannerStack = new BannerStack(); const workflowPage = new WorkflowPage(); -describe('Cloud', { disableAutoLogin: true }, () => { +describe('Cloud', () => { before(() => { const now = new Date(); const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000); @@ -20,22 +19,12 @@ describe('Cloud', { disableAutoLogin: true }, () => { }); beforeEach(() => { - cy.intercept('GET', '/rest/admin/cloud-plan', { - body: planData, - }).as('getPlanData'); - - cy.intercept('GET', '/rest/settings', (req) => { - req.on('response', (res) => { - res.send({ - data: { - ...res.body.data, - deployment: { type: 'cloud' }, - n8nMetadata: { userId: 1 }, - }, - }); - }); - }).as('loadSettings'); - + cy.overrideSettings({ + deployment: { type: 'cloud' }, + n8nMetadata: { userId: '1' }, + }); + cy.intercept('GET', '/rest/admin/cloud-plan', planData).as('getPlanData'); + cy.intercept('GET', '/rest/cloud/proxy/user/me', {}).as('getCloudUserInfo'); cy.intercept('GET', new RegExp('/rest/projects*')).as('projects'); cy.intercept('GET', new RegExp('/rest/roles')).as('roles'); }); @@ -49,8 +38,6 @@ describe('Cloud', { disableAutoLogin: true }, () => { describe('BannerStack', () => { it('should render trial banner for opt-in cloud user', () => { - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - visitWorkflowPage(); bannerStack.getters.banner().should('be.visible'); @@ -58,21 +45,11 @@ describe('Cloud', { disableAutoLogin: true }, () => { mainSidebar.actions.signout(); bannerStack.getters.banner().should('not.be.visible'); - - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - - visitWorkflowPage(); - - bannerStack.getters.banner().should('be.visible'); - - mainSidebar.actions.signout(); }); }); describe('Admin Home', () => { it('Should show admin button', () => { - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - visitWorkflowPage(); mainSidebar.getters.adminPanel().should('be.visible'); @@ -81,8 +58,6 @@ describe('Cloud', { disableAutoLogin: true }, () => { describe('Public API', () => { it('Should show upgrade CTA for Public API if user is trialing', () => { - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - visitPublicApiPage(); cy.wait(['@loadSettings', '@projects', '@roles', '@getPlanData']); diff --git a/cypress/e2e/27-two-factor-authentication.cy.ts b/cypress/e2e/27-two-factor-authentication.cy.ts index 7a1ee28f22..21319dd79b 100644 --- a/cypress/e2e/27-two-factor-authentication.cy.ts +++ b/cypress/e2e/27-two-factor-authentication.cy.ts @@ -34,9 +34,8 @@ const signinPage = new SigninPage(); const personalSettingsPage = new PersonalSettingsPage(); const mainSidebar = new MainSidebar(); -describe('Two-factor authentication', () => { +describe('Two-factor authentication', { disableAutoLogin: true }, () => { beforeEach(() => { - void Cypress.session.clearAllSavedSessions(); cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, { owner: user, members: [], diff --git a/cypress/e2e/28-debug.cy.ts b/cypress/e2e/28-debug.cy.ts index 71c733c254..5d2bd76cac 100644 --- a/cypress/e2e/28-debug.cy.ts +++ b/cypress/e2e/28-debug.cy.ts @@ -1,7 +1,6 @@ import { HTTP_REQUEST_NODE_NAME, IF_NODE_NAME, - INSTANCE_OWNER, MANUAL_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME, } from '../constants'; @@ -21,7 +20,7 @@ describe('Debug', () => { cy.intercept('GET', '/rest/executions/*').as('getExecution'); cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun'); - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + cy.signinAsOwner(); workflowPage.actions.visit(); diff --git a/cypress/e2e/29-templates.cy.ts b/cypress/e2e/29-templates.cy.ts index d5f0a67f7e..5cc6657416 100644 --- a/cypress/e2e/29-templates.cy.ts +++ b/cypress/e2e/29-templates.cy.ts @@ -1,55 +1,246 @@ import { TemplatesPage } from '../pages/templates'; +import { WorkflowPage } from '../pages/workflow'; import { WorkflowsPage } from '../pages/workflows'; import { MainSidebar } from '../pages/sidebar/main-sidebar'; +import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json'; +import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json'; const templatesPage = new TemplatesPage(); +const workflowPage = new WorkflowPage(); const workflowsPage = new WorkflowsPage(); const mainSidebar = new MainSidebar(); describe('Workflow templates', () => { - beforeEach(() => { - cy.intercept('GET', '**/rest/settings', (req) => { - // Disable cache - delete req.headers['if-none-match']; - req.reply((res) => { - if (res.body.data) { - // Disable custom templates host if it has been overridden by another intercept - res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' }; - } - }); - }).as('settingsRequest'); + const mockTemplateHost = (host: string) => { + cy.overrideSettings({ + templates: { enabled: true, host }, + }); + }; + + describe('For api.n8n.io', () => { + beforeEach(() => { + mockTemplateHost('https://api.n8n.io/api/'); + }); + + it('Opens website when clicking templates sidebar link', () => { + cy.visit(workflowsPage.url); + mainSidebar.getters.templates().should('be.visible'); + // Templates should be a link to the website + mainSidebar.getters + .templates() + .parent('a') + .should('have.attr', 'href') + .and('include', 'https://n8n.io/workflows'); + // Link should contain instance address and n8n version + mainSidebar.getters + .templates() + .parent('a') + .then(($a) => { + const href = $a.attr('href'); + const params = new URLSearchParams(href); + // Link should have all mandatory parameters expected on the website + expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include( + window.location.origin, + ); + expect(params.get('utm_n8n_version')).to.match(/[0-9]+\.[0-9]+\.[0-9]+/); + expect(params.get('utm_awc')).to.match(/[0-9]+/); + }); + mainSidebar.getters.templates().parent('a').should('have.attr', 'target', '_blank'); + }); + + it('Redirects to website when visiting templates page directly', () => { + cy.intercept( + { + hostname: 'n8n.io', + pathname: '/workflows', + }, + 'Mock Template Page', + ).as('templatesPage'); + + cy.visit(templatesPage.url); + + cy.wait('@templatesPage'); + }); }); - it('Opens website when clicking templates sidebar link', () => { - cy.visit(workflowsPage.url); - mainSidebar.getters.templates().should('be.visible'); - // Templates should be a link to the website - mainSidebar.getters - .templates() - .parent('a') - .should('have.attr', 'href') - .and('include', 'https://n8n.io/workflows'); - // Link should contain instance address and n8n version - mainSidebar.getters - .templates() - .parent('a') - .then(($a) => { - const href = $a.attr('href'); - const params = new URLSearchParams(href); - // Link should have all mandatory parameters expected on the website - expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include( - window.location.origin, - ); - expect(params.get('utm_n8n_version')).to.match(/[0-9]+\.[0-9]+\.[0-9]+/); - expect(params.get('utm_awc')).to.match(/[0-9]+/); - }); - mainSidebar.getters.templates().parent('a').should('have.attr', 'target', '_blank'); - }); + describe('For a custom template host', () => { + const hostname = 'random.domain'; + const categories = [ + { id: 1, name: 'Engineering' }, + { id: 2, name: 'Finance' }, + { id: 3, name: 'Sales' }, + ]; + const collections = [ + { + id: 1, + name: 'Test Collection', + workflows: [{ id: 1 }], + nodes: [], + }, + ]; - it('Redirects to website when visiting templates page directly', () => { - cy.visit(templatesPage.url); - cy.origin('https://n8n.io', () => { - cy.url().should('include', 'https://n8n.io/workflows'); + beforeEach(() => { + cy.intercept({ hostname, pathname: '/api/health' }, { status: 'OK' }); + cy.intercept({ hostname, pathname: '/api/templates/categories' }, { categories }); + cy.intercept( + { hostname, pathname: '/api/templates/collections', query: { category: '**' } }, + (req) => { + req.reply({ collections: req.query['category[]'] === '3' ? [] : collections }); + }, + ); + cy.intercept( + { hostname, pathname: '/api/templates/search', query: { category: '**' } }, + (req) => { + const fixture = + req.query.category === 'Sales' + ? 'templates_search/sales_templates_search_response.json' + : 'templates_search/all_templates_search_response.json'; + req.reply({ statusCode: 200, fixture }); + }, + ); + + cy.intercept( + { hostname, pathname: '/api/workflows/templates/1' }, + { + statusCode: 200, + body: { + id: 1, + name: OnboardingWorkflow.name, + workflow: OnboardingWorkflow, + }, + }, + ).as('getTemplate'); + + cy.intercept( + { hostname, pathname: '/api/templates/workflows/1' }, + { + statusCode: 200, + body: WorkflowTemplate, + }, + ).as('getTemplatePreview'); + + mockTemplateHost(`https://${hostname}/api`); + }); + + it('can open onboarding flow', () => { + templatesPage.actions.openOnboardingFlow(); + cy.url().should('match', /.*\/workflow\/.*?onboardingId=1$/); + + workflowPage.actions.shouldHaveWorkflowName('Demo: ' + OnboardingWorkflow.name); + workflowPage.getters.canvasNodes().should('have.length', 4); + workflowPage.getters.stickies().should('have.length', 1); + workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon'); + }); + + it('can import template', () => { + templatesPage.actions.importTemplate(); + cy.url().should('include', '/workflow/new?templateId=1'); + + workflowPage.getters.canvasNodes().should('have.length', 4); + workflowPage.getters.stickies().should('have.length', 1); + workflowPage.actions.shouldHaveWorkflowName(OnboardingWorkflow.name); + }); + + it('should save template id with the workflow', () => { + templatesPage.actions.importTemplate(); + + cy.visit(templatesPage.url); + cy.get('.el-skeleton.n8n-loading').should('not.exist'); + templatesPage.getters.firstTemplateCard().should('exist'); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + templatesPage.getters.firstTemplateCard().click(); + cy.url().should('include', '/templates/1'); + cy.wait('@getTemplatePreview'); + + templatesPage.getters.useTemplateButton().click(); + cy.url().should('include', '/workflow/new'); + workflowPage.actions.saveWorkflowOnButtonClick(); + + workflowPage.actions.hitSelectAll(); + workflowPage.actions.hitCopy(); + + cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); + // Check workflow JSON by copying it to clipboard + cy.readClipboard().then((workflowJSON) => { + expect(workflowJSON).to.contain('"templateId": "1"'); + }); + }); + + it('can open template with images and hides workflow screenshots', () => { + cy.visit(`${templatesPage.url}/1`); + cy.wait('@getTemplatePreview'); + + templatesPage.getters.description().find('img').should('have.length', 1); + }); + + it('renders search elements correctly', () => { + cy.visit(templatesPage.url); + templatesPage.getters.searchInput().should('exist'); + templatesPage.getters.allCategoriesFilter().should('exist'); + templatesPage.getters.categoryFilters().should('have.length.greaterThan', 1); + templatesPage.getters.templateCards().should('have.length.greaterThan', 0); + }); + + it('can filter templates by category', () => { + cy.visit(templatesPage.url); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + templatesPage.getters.categoryFilter('sales').should('exist'); + let initialTemplateCount = 0; + let initialCollectionCount = 0; + + templatesPage.getters.templateCountLabel().then(($el) => { + initialTemplateCount = parseInt($el.text().replace(/\D/g, ''), 10); + templatesPage.getters.collectionCountLabel().then(($el1) => { + initialCollectionCount = parseInt($el1.text().replace(/\D/g, ''), 10); + + templatesPage.getters.categoryFilter('sales').click(); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + + // Should have less templates and collections after selecting a category + templatesPage.getters.templateCountLabel().should(($el2) => { + expect(parseInt($el2.text().replace(/\D/g, ''), 10)).to.be.lessThan( + initialTemplateCount, + ); + }); + templatesPage.getters.collectionCountLabel().should(($el2) => { + expect(parseInt($el2.text().replace(/\D/g, ''), 10)).to.be.lessThan( + initialCollectionCount, + ); + }); + }); + }); + }); + + it('should preserve search query in URL', () => { + cy.visit(templatesPage.url); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + templatesPage.getters.categoryFilter('sales').should('exist'); + templatesPage.getters.categoryFilter('sales').click(); + templatesPage.getters.searchInput().type('auto'); + + cy.url().should('include', '?categories='); + cy.url().should('include', '&search='); + + cy.reload(); + + // Should preserve search query in URL + cy.url().should('include', '?categories='); + cy.url().should('include', '&search='); + + // Sales category should still be selected + templatesPage.getters + .categoryFilter('sales') + .find('label') + .should('have.class', 'is-checked'); + // Search input should still have the search query + templatesPage.getters.searchInput().should('have.value', 'auto'); + // Sales checkbox should be pushed to the top + templatesPage.getters + .categoryFilters() + .eq(1) + .then(($el) => { + expect($el.text()).to.equal('Sales'); + }); }); }); }); diff --git a/cypress/e2e/30-editor-after-route-changes.cy.ts b/cypress/e2e/30-editor-after-route-changes.cy.ts index 423c92110b..6258a7698e 100644 --- a/cypress/e2e/30-editor-after-route-changes.cy.ts +++ b/cypress/e2e/30-editor-after-route-changes.cy.ts @@ -2,7 +2,6 @@ import { CODE_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME, IF_NODE_NAME, - INSTANCE_OWNER, SCHEDULE_TRIGGER_NODE_NAME, } from '../constants'; import { @@ -125,7 +124,7 @@ describe('Editor actions should work', () => { beforeEach(() => { cy.enableFeature('debugInEditor'); cy.enableFeature('workflowHistory'); - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + cy.signinAsOwner(); createNewWorkflowAndActivate(); }); @@ -186,7 +185,7 @@ describe('Editor zoom should work after route changes', () => { beforeEach(() => { cy.enableFeature('debugInEditor'); cy.enableFeature('workflowHistory'); - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + cy.signinAsOwner(); workflowPage.actions.visit(); cy.createFixtureWorkflow('Lots_of_nodes.json', 'Lots of nodes'); workflowPage.actions.saveWorkflowOnButtonClick(); diff --git a/cypress/e2e/32-worker-view.cy.ts b/cypress/e2e/32-worker-view.cy.ts index ba3edbe4c9..de9afc2891 100644 --- a/cypress/e2e/32-worker-view.cy.ts +++ b/cypress/e2e/32-worker-view.cy.ts @@ -1,4 +1,3 @@ -import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants'; import { WorkerViewPage } from '../pages'; const workerViewPage = new WorkerViewPage(); @@ -10,13 +9,13 @@ describe('Worker View (unlicensed)', () => { }); it('should not show up in the menu sidebar', () => { - cy.signin(INSTANCE_MEMBERS[0]); + cy.signinAsMember(0); cy.visit(workerViewPage.url); workerViewPage.getters.menuItem().should('not.exist'); }); it('should show action box', () => { - cy.signin(INSTANCE_MEMBERS[0]); + cy.signinAsMember(0); cy.visit(workerViewPage.url); workerViewPage.getters.workerViewUnlicensed().should('exist'); }); @@ -29,14 +28,14 @@ describe('Worker View (licensed)', () => { }); it('should show up in the menu sidebar', () => { - cy.signin(INSTANCE_OWNER); + cy.signinAsOwner(); cy.enableQueueMode(); cy.visit(workerViewPage.url); workerViewPage.getters.menuItem().should('exist'); }); it('should show worker list view', () => { - cy.signin(INSTANCE_MEMBERS[0]); + cy.signinAsMember(0); cy.visit(workerViewPage.url); workerViewPage.getters.workerViewLicensed().should('exist'); }); diff --git a/cypress/e2e/34-template-credentials-setup.cy.ts b/cypress/e2e/34-template-credentials-setup.cy.ts index 7553a55a7b..c5d9f2643f 100644 --- a/cypress/e2e/34-template-credentials-setup.cy.ts +++ b/cypress/e2e/34-template-credentials-setup.cy.ts @@ -8,10 +8,19 @@ import { WorkflowPage } from '../pages/workflow'; import * as formStep from '../composables/setup-template-form-step'; import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button'; import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal'; +import TestTemplate1 from '../fixtures/Test_Template_1.json'; +import TestTemplate2 from '../fixtures/Test_Template_2.json'; const workflowPage = new WorkflowPage(); -const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate; +const testTemplate = { + id: 1205, + data: TestTemplate1, +}; +const templateWithoutCredentials = { + id: 1344, + data: TestTemplate2, +}; // NodeView uses beforeunload listener that will show a browser // native popup, which will block cypress from continuing / exiting. @@ -29,19 +38,19 @@ Cypress.on('window:before:load', (win) => { describe('Template credentials setup', () => { beforeEach(() => { - cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${testTemplate.id}`, { - fixture: testTemplate.fixture, + cy.intercept( + 'GET', + `https://api.n8n.io/api/templates/workflows/${testTemplate.id}`, + testTemplate.data, + ).as('getTemplatePreview'); + cy.intercept( + 'GET', + `https://api.n8n.io/api/workflows/templates/${testTemplate.id}`, + testTemplate.data.workflow, + ).as('getTemplate'); + cy.overrideSettings({ + templates: { enabled: true, host: 'https://api.n8n.io/api/' }, }); - cy.intercept('GET', '**/rest/settings', (req) => { - // Disable cache - delete req.headers['if-none-match']; - req.reply((res) => { - if (res.body.data) { - // Disable custom templates host if it has been overridden by another intercept - res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' }; - } - }); - }).as('settingsRequest'); }); it('can be opened from template collection page', () => { @@ -108,7 +117,7 @@ describe('Template credentials setup', () => { // Focus the canvas so the copy to clipboard works workflowPage.getters.canvasNodes().eq(0).realClick(); - workflowPage.actions.selectAll(); + workflowPage.actions.hitSelectAll(); workflowPage.actions.hitCopy(); cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); @@ -125,11 +134,9 @@ describe('Template credentials setup', () => { }); it('should work with a template that has no credentials (ADO-1603)', () => { - const templateWithoutCreds = templateCredentialsSetupPage.testData.templateWithoutCredentials; - cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${templateWithoutCreds.id}`, { - fixture: templateWithoutCreds.fixture, - }); - templateCredentialsSetupPage.visitTemplateCredentialSetupPage(templateWithoutCreds.id); + const { id, data } = templateWithoutCredentials; + cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${id}`, data); + templateCredentialsSetupPage.visitTemplateCredentialSetupPage(id); const expectedAppNames = ['1. Email (IMAP)', '2. Nextcloud']; const expectedAppDescriptions = [ @@ -152,7 +159,7 @@ describe('Template credentials setup', () => { workflowPage.getters.canvasNodes().should('have.length', 3); }); - describe('Credential setup from workflow editor', () => { + describe('Credential setup from workflow editor', { disableAutoLogin: true }, () => { beforeEach(() => { cy.resetDatabase(); cy.signinAsOwner(); @@ -190,7 +197,7 @@ describe('Template credentials setup', () => { // Focus the canvas so the copy to clipboard works workflowPage.getters.canvasNodes().eq(0).realClick(); - workflowPage.actions.selectAll(); + workflowPage.actions.hitSelectAll(); workflowPage.actions.hitCopy(); cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); diff --git a/cypress/e2e/35-admin-user-smoke-test.cy.ts b/cypress/e2e/35-admin-user-smoke-test.cy.ts index 05e70aa339..c8585118e7 100644 --- a/cypress/e2e/35-admin-user-smoke-test.cy.ts +++ b/cypress/e2e/35-admin-user-smoke-test.cy.ts @@ -1,11 +1,10 @@ -import { INSTANCE_ADMIN, INSTANCE_OWNER } from '../constants'; import { SettingsPage } from '../pages/settings'; const settingsPage = new SettingsPage(); describe('Admin user', { disableAutoLogin: true }, () => { it('should see same Settings sub menu items as instance owner', () => { - cy.signin(INSTANCE_OWNER); + cy.signinAsOwner(); cy.visit(settingsPage.url); let ownerMenuItems = 0; @@ -15,7 +14,7 @@ describe('Admin user', { disableAutoLogin: true }, () => { }); cy.signout(); - cy.signin(INSTANCE_ADMIN); + cy.signinAsAdmin(); cy.visit(settingsPage.url); settingsPage.getters.menuItems().should('have.length', ownerMenuItems); diff --git a/cypress/e2e/36-versions.cy.ts b/cypress/e2e/36-versions.cy.ts index 2d93223ebb..1d4fc51808 100644 --- a/cypress/e2e/36-versions.cy.ts +++ b/cypress/e2e/36-versions.cy.ts @@ -1,4 +1,3 @@ -import { INSTANCE_OWNER } from '../constants'; import { WorkflowsPage } from '../pages/workflows'; import { closeVersionUpdatesPanel, @@ -11,52 +10,18 @@ const workflowsPage = new WorkflowsPage(); describe('Versions', () => { it('should open updates panel', () => { - cy.intercept('GET', '/rest/settings', (req) => { - req.continue((res) => { - if (res.body.hasOwnProperty('data')) { - res.body.data = { - ...res.body.data, - releaseChannel: 'stable', - versionCli: '1.0.0', - versionNotifications: { - enabled: true, - endpoint: 'https://api.n8n.io/api/versions/', - infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html', - }, - }; - } - }); - }).as('settings'); - - cy.intercept('GET', 'https://api.n8n.io/api/versions/1.0.0', [ - { - name: '1.3.1', - createdAt: '2023-08-18T11:53:12.857Z', - hasSecurityIssue: null, - hasSecurityFix: null, - securityIssueFixVersion: null, - hasBreakingChange: null, - documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131', - nodes: [], - description: 'Includes bug fixes', + cy.overrideSettings({ + releaseChannel: 'stable', + versionCli: '1.0.0', + versionNotifications: { + enabled: true, + endpoint: 'https://api.n8n.io/api/versions/', + infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html', }, - { - name: '1.0.5', - createdAt: '2023-07-24T10:54:56.097Z', - hasSecurityIssue: false, - hasSecurityFix: null, - securityIssueFixVersion: null, - hasBreakingChange: true, - documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104', - nodes: [], - description: 'Includes core functionality and bug fixes', - }, - ]); - - cy.signin(INSTANCE_OWNER); + }); cy.visit(workflowsPage.url); - cy.wait('@settings'); + cy.wait('@loadSettings'); getVersionUpdatesPanelOpenButton().should('contain', '2 updates'); openVersionUpdatesPanel(); diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts index 6cec66d1cd..65a41165b5 100644 --- a/cypress/e2e/39-projects.cy.ts +++ b/cypress/e2e/39-projects.cy.ts @@ -1,10 +1,4 @@ -import { - INSTANCE_ADMIN, - INSTANCE_MEMBERS, - INSTANCE_OWNER, - MANUAL_TRIGGER_NODE_NAME, - NOTION_NODE_NAME, -} from '../constants'; +import { INSTANCE_MEMBERS, MANUAL_TRIGGER_NODE_NAME, NOTION_NODE_NAME } from '../constants'; import { WorkflowsPage, WorkflowPage, @@ -23,7 +17,7 @@ const credentialsModal = new CredentialsModal(); const executionsTab = new WorkflowExecutionsTab(); const ndv = new NDV(); -describe('Projects', () => { +describe('Projects', { disableAutoLogin: true }, () => { before(() => { cy.resetDatabase(); cy.enableFeature('sharing'); @@ -34,7 +28,7 @@ describe('Projects', () => { }); it('should handle workflows and credentials and menu items', () => { - cy.signin(INSTANCE_ADMIN); + cy.signinAsAdmin(); cy.visit(workflowsPage.url); workflowsPage.getters.workflowCards().should('not.have.length'); @@ -230,8 +224,7 @@ describe('Projects', () => { }); it('should not show project add button and projects to a member if not invited to any project', () => { - cy.signout(); - cy.signin(INSTANCE_MEMBERS[1]); + cy.signinAsMember(1); cy.visit(workflowsPage.url); projects.getAddProjectButton().should('not.exist'); @@ -249,7 +242,7 @@ describe('Projects', () => { }); it('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', () => { - cy.signin(INSTANCE_OWNER); + cy.signinAsOwner(); cy.visit(workflowsPage.url); // Create a project and add a credential to it diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 4b86bdbb20..35021e7333 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -345,7 +345,7 @@ describe('NDV', () => { ndv.getters.parameterInput('remoteOptions').click(); getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3); - ndv.actions.setInvalidExpression({ fieldName: 'fieldId', delay: 200 }); + ndv.actions.setInvalidExpression({ fieldName: 'fieldId' }); ndv.getters.inputPanel().click(); // remove focus from input, hide expression preview @@ -363,7 +363,7 @@ describe('NDV', () => { getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3); ndv.getters.parameterInput('remoteOptions').click(); - ndv.actions.setInvalidExpression({ fieldName: 'otherField', delay: 50 }); + ndv.actions.setInvalidExpression({ fieldName: 'otherField' }); ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index d18dc85d45..7c7c3be554 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -1,11 +1,8 @@ import { CODE_NODE_NAME, MANUAL_TRIGGER_NODE_NAME, - META_KEY, SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME, - INSTANCE_MEMBERS, - INSTANCE_OWNER, NOTION_NODE_NAME, } from '../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; @@ -136,13 +133,13 @@ describe('Workflow Actions', () => { ); cy.reload(); cy.get('.el-loading-mask').should('exist'); - cy.get('body').type(META_KEY, { release: false }).type('s'); - cy.get('body').type(META_KEY, { release: false }).type('s'); - cy.get('body').type(META_KEY, { release: false }).type('s'); + WorkflowPage.actions.hitSaveWorkflow(); + WorkflowPage.actions.hitSaveWorkflow(); + WorkflowPage.actions.hitSaveWorkflow(); cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0)); cy.waitForLoad(); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); - cy.get('body').type(META_KEY, { release: false }).type('s'); + WorkflowPage.actions.hitSaveWorkflow(); cy.wait('@saveWorkflow'); cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1)); }); @@ -172,9 +169,10 @@ describe('Workflow Actions', () => { WorkflowPage.getters.canvasNodes().should('have.have.length', 2); cy.get('#node-creator').should('not.exist'); - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('a'); + + WorkflowPage.actions.hitSelectAll(); cy.get('.jtk-drag-selected').should('have.length', 2); - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('c'); + WorkflowPage.actions.hitCopy(); successToast().should('exist'); }); @@ -338,33 +336,32 @@ describe('Workflow Actions', () => { it('should run workflow using keyboard shortcut', () => { WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.saveWorkflowOnButtonClick(); - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('{enter}'); + WorkflowPage.actions.hitExecuteWorkflow(); successToast().should('contain.text', 'Workflow executed successfully'); }); it('should not run empty workflows', () => { // Clear the canvas - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('a'); - cy.get('body').type('{backspace}'); + WorkflowPage.actions.hitDeleteAllNodes(); WorkflowPage.getters.canvasNodes().should('have.length', 0); // Button should be disabled WorkflowPage.getters.executeWorkflowButton().should('be.disabled'); // Keyboard shortcut should not work - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('{enter}'); + WorkflowPage.actions.hitExecuteWorkflow(); successToast().should('not.exist'); }); }); describe('Menu entry Push To Git', () => { it('should not show up in the menu for members', () => { - cy.signin(INSTANCE_MEMBERS[0]); + cy.signinAsMember(0); cy.visit(WorkflowPages.url); WorkflowPage.actions.visit(); WorkflowPage.getters.workflowMenuItemGitPush().should('not.exist'); }); it('should show up for owners', () => { - cy.signin(INSTANCE_OWNER); + cy.signinAsOwner(); cy.visit(WorkflowPages.url); WorkflowPage.actions.visit(); WorkflowPage.getters.workflowMenuItemGitPush().should('exist'); diff --git a/cypress/fixtures/templates_search/all_templates_search_response.json b/cypress/fixtures/templates_search/all_templates_search_response.json index 5a0a1eb5ad..fe8ba3e3e4 100644 --- a/cypress/fixtures/templates_search/all_templates_search_response.json +++ b/cypress/fixtures/templates_search/all_templates_search_response.json @@ -2,7 +2,7 @@ "totalWorkflows": 506, "workflows": [ { - "id": 60, + "id": 1, "name": "test1 test1", "totalViews": 120000000, "recentViews": 0, diff --git a/cypress/fixtures/templates_search/test_template_import.json b/cypress/fixtures/templates_search/test_template_import.json deleted file mode 100644 index c77be3db9c..0000000000 --- a/cypress/fixtures/templates_search/test_template_import.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": 60, - "name": "test1 test1", - "workflow": { - "nodes": [ - { - "name": "Start", - "type": "n8n-nodes-base.start", - "position": [ - 250, - 300 - ], - "parameters": {}, - "typeVersion": 1 - } - ], - "connections": {} - } -} diff --git a/cypress/fixtures/templates_search/test_template_preview.json b/cypress/fixtures/templates_search/test_template_preview.json deleted file mode 100644 index 4d3ca1e548..0000000000 --- a/cypress/fixtures/templates_search/test_template_preview.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "workflow": { - "id": 60, - "name": "test1 test1", - "views": 120000000, - "recentViews": 0, - "totalViews": 120000000, - "createdAt": "2019-08-30T16:39:31.362Z", - "description": "here is a description. here is a description. here is a description. \n\n![Screenshot from 20190806 091433.png](fileId:88)", - "workflow": { - "nodes": [ - { - "name": "Start", - "type": "n8n-nodes-base.start", - "position": [ - 250, - 300 - ], - "parameters": {}, - "typeVersion": 1 - } - ], - "connections": {} - }, - "lastUpdatedBy": null, - "workflowInfo": { - "nodeCount": 1, - "nodeTypes": { - "n8n-nodes-base.start": { - "count": 1 - } - } - }, - "user": { - "username": "admin" - }, - "nodes": [ - { - "id": 11, - "icon": "file:amqp.png", - "name": "n8n-nodes-base.amqpTrigger", - "defaults": { - "name": "AMQP Trigger" - }, - "iconData": { - "type": "file", - "fileBuffer": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAB7UlEQVRoge2W4W3CMBCFj26QjkBHSEdIR4AR6Ah0BBgBRqAjhBFgBBghHaEVlV29PN0lDr+o9D7JEjhn+975bJ8JIYQQQgghhBBCCCGEEA9CY2bf0NaBW2uyu7UN2XSOzTyY60J2BzNbObbsH7eTmS2mhHJHE1wmCD7A93ngEAquHaHc2omCcysSXQW74g32BHfwfTEiuCoQm9vuDsEndPYpELxKjjBj0foCEXX6XdM3by3c7aOZPZvZzMzeaBzbIh9pzIuZXaG/RqNIMAq7Ur8XCHQ2kx3LC56DMQ39X4LI23zbAd88ruRHD09wTVF5p+/eBZI5g7O8w5FgXOvsZAI7PxRwS4HGIPbm8wRjBL/Sgp/QNyQYHWySmOxgJBgFeGnPfZHgDVyufET+YMEVCdo7gziCTBbGmRKlGQpCMXOnj+1L6B0JFsxndO3cjjZyjo6OnZeqGb5gqhTQS3qKeK1SwbesfB3IrF/awqu+g8Dgs5SLE37SciHiPUv8rLVp7k2wdl63tDDqgTs8lqpINWGXbSTKe9rlJgXME7C9I6V7oGAWsEzv2gzeN2TstkbCZyIJWBYKWUwtF4foKGU9TpRGdZDSdVDpDNXSVVBLt5TeucS9K6X/E3USX3rshBBCCCGEEEIIIYQQ4tExsx8PuuPnwhCIbgAAAABJRU5ErkJggg==" - }, - "categories": [ - { - "id": 5, - "name": "Development" - }, - { - "id": 6, - "name": "Communication" - } - ], - "displayName": "AMQP Trigger", - "typeVersion": 1 - }, - { - "id": 18, - "icon": "file:autopilot.svg", - "name": "n8n-nodes-base.autopilot", - "defaults": { - "name": "Autopilot" - }, - "iconData": { - "type": "file", - "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjM4IDI2IDM1IDM1Ij48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSIjMThkNGIyIiBzdHJva2Utd2lkdGg9IjMiIGZpbGw9IiMxOGQ0YjIiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDUuNCA0Mi42aDE5LjlsMy40LTQuOEg0MmwzLjQgNC44em0zLjEgOC4zaDEzLjFsMy40LTQuOEg0NS40bDMuMSA0Ljh6bTU0LS43Ii8+PC9zdmc+" - }, - "categories": [ - { - "id": 1, - "name": "Marketing" - } - ], - "displayName": "Autopilot", - "typeVersion": 1 - }, - { - "id": 20, - "icon": "file:lambda.svg", - "name": "n8n-nodes-base.awsLambda", - "defaults": { - "name": "AWS Lambda" - }, - "iconData": { - "type": "file", - "fileBuffer": "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI1MDAiIHZpZXdCb3g9Ii0zLjAyMyAtMC4yMiA0MjAuOTIzIDQzMy41NCIgd2lkdGg9IjI0NDMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIwOC40NSAyMjcuODljLTEuNTkgMi4yNi0yLjkzIDQuMTItNC4yMiA2cS0zMC44NiA0NS40Mi02MS43IDkwLjgzLTI4LjY5IDQyLjI0LTU3LjQ0IDg0LjQzYTMuODggMy44OCAwIDAxLTIuNzMgMS41OXEtNDAuNTktLjM1LTgxLjE2LS44OGMtLjMgMC0uNjEtLjA5LTEuMi0uMThhMTQuNDQgMTQuNDQgMCAwMS43Ni0xLjY1cTI4LjMxLTQzLjg5IDU2LjYyLTg3Ljc2IDI1LjExLTM4Ljg4IDUwLjI1LTc3Ljc0IDI3Ljg2LTQzLjE4IDU1LjY5LTg2LjQyYzIuNzQtNC4yNSA1LjU5LTguNDIgOC4xOS0xMi43NWE1LjI2IDUuMjYgMCAwMC41Ni0zLjgzYy01LTE1Ljk0LTEwLjEtMzEuODQtMTUuMTktNDcuNzQtMi4xOC02LjgxLTQuNDYtMTMuNTgtNi41LTIwLjQzLS42Ni0yLjItMS43NS0yLjg3LTQtMi44Ni0xNyAuMDctMzMuOS4wNS01MC44NS4wNS0zLjIyIDAtMy4yMyAwLTMuMjMtMy4xOCAwLTIwLjg0IDAtNDEuNjgtLjA2LTYyLjUyIDAtMi4zMi43Ni0yLjg0IDIuOTQtMi44NHE1MS4xOS4wOSAxMDIuNCAwYTMuMjkgMy4yOSAwIDAxMy42IDIuNDNxMjcgNjcuOTEgNTQgMTM1Ljc3IDMxLjUgNzkuMTQgNjMgMTU4LjNjNi41MiAxNi4zOCAxMy4wOSAzMi43NSAxOS41NCA0OS4xNy43NyAyIDEuNTcgMi4zOCAzLjU5IDEuNzYgMTcuODktNS41MyAzNS44Mi0xMC45MSA1My43LTE2LjQ1IDIuMjUtLjcgMy4wNy0uMjMgMy43NyAyIDYuMSAxOS4xNyAxMi4zMiAzOC4zIDE4LjUgNTcuNDUuMjEuNjYuMzcgMS4zMy42MiAyLjI1LTEuMjguNDctMi40OCAxLTMuNzEgMS4zNHEtNjEgMTkuMzMtMTIxLjkzIDM4LjY4Yy0xLjk0LjYxLTIuNTItLjA1LTMuMTctMS42OHEtMTguNjEtNDcuMTYtMzcuMzEtOTQuMjgtMTguMjktNDYuMTQtMzYuNi05Mi4yOGMtMS44My00LjYyLTMuNjMtOS4yNi01LjQ2LTEzLjg4LS4yOS0uNzktLjY5LTEuNDgtMS4yNy0yLjd6IiBmaWxsPSIjZmE3ZTE0Ii8+PC9zdmc+" - }, - "categories": [ - { - "id": 5, - "name": "Development" - } - ], - "displayName": "AWS Lambda", - "typeVersion": 1 - }, - { - "id": 40, - "icon": "file:clearbit.svg", - "name": "n8n-nodes-base.clearbit", - "defaults": { - "name": "Clearbit" - }, - "iconData": { - "type": "file", - "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI3MiIgaGVpZ2h0PSI3MiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iNTAlIiB4Mj0iMTAwJSIgeTE9IjAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI0RFRjJGRSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI0RCRjFGRSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJiIiB4MT0iMCUiIHgyPSI1MCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM1N0JDRkQiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM1MUI1RkQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjM3LjUlIiB4Mj0iNjIuNSUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiMxQ0E3RkQiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMxNDhDRkMiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGZpbGw9InVybCgjYSkiIGQ9Ik03MiAzNnYxNi43N2wtLjAwNC44NjhjLS4wNiA2LjAzNS0uNzUgOC4zNTMtMiAxMC42ODhhMTMuNjMgMTMuNjMgMCAwMS01LjY3IDUuNjdsLS4zMjYuMTcxQzYxLjY1OCA3MS4zNjQgNTkuMTYgNzIgNTIuNzcgNzJIMzZWMzZoMzZ6Ii8+PHBhdGggZmlsbD0idXJsKCNiKSIgZD0iTTY0LjMyNiAyLjAwM2ExMy42MyAxMy42MyAwIDAxNS42NyA1LjY3bC4xNzEuMzI3QzcxLjM2NCAxMC4zNDIgNzIgMTIuODQgNzIgMTkuMjNWMzZIMzZWMGgxNi43N2M2LjY4NyAwIDkuMTEyLjY5NiAxMS41NTYgMi4wMDN6Ii8+PHBhdGggZmlsbD0idXJsKCNjKSIgZD0iTTM2IDB2NzJIMTkuMjNsLS44NjgtLjAwNGMtNi4wMzUtLjA2LTguMzUzLS43NS0xMC42ODgtMmExMy42MyAxMy42MyAwIDAxLTUuNjctNS42N0wxLjgzMiA2NEMuNjM2IDYxLjY1OCAwIDU5LjE2IDAgNTIuNzdWMTkuMjNjMC02LjY4Ny42OTYtOS4xMTIgMi4wMDMtMTEuNTU2YTEzLjYzIDEzLjYzIDAgMDE1LjY3LTUuNjdMOCAxLjgzMkMxMC4zNDIuNjM2IDEyLjg0IDAgMTkuMjMgMEgzNnoiLz48L2c+PC9zdmc+" - }, - "categories": [ - { - "id": 2, - "name": "Sales" - } - ], - "displayName": "Clearbit", - "typeVersion": 1 - }, - { - "id": 51, - "icon": "file:convertKit.svg", - "name": "n8n-nodes-base.convertKitTrigger", - "defaults": { - "name": "ConvertKit Trigger" - }, - "iconData": { - "type": "file", - "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTcyIiBoZWlnaHQ9IjE2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNODIuNzIgMTI2LjMxNmMyOS43NyAwIDUyLjc4LTIyLjYyMiA1Mi43OC01MC41MjYgMC0yNi4xNDMtMjEuNjE3LTQyLjEwNi0zNS45MzUtNDIuMTA2LTE5Ljk0NSAwLTM1LjkzIDE0LjA4NC0zOC4xOTggMzQuOTg4LS40MTggMy44NTYtMy40NzYgNy4wOS03LjM1NSA3LjA2MS02LjQyMy0uMDQ2LTE1Ljc0Ni0uMS0yMS42NTgtLjA4LTIuNTU1LjAwOC00LjY2OS0yLjA2NS00LjU0My00LjYxOC44OS0xOC4xMjMgNi45MTQtMzUuMDcgMTguNDAyLTQ4LjA4N0M1OC45NzYgOC40ODggNzcuNTYxIDAgOTkuNTY1IDBjMzYuOTY5IDAgNzEuODY5IDMzLjc4NiA3MS44NjkgNzUuNzkgMCA0Ni41MDgtMzguMzEyIDg0LjIxLTg3LjkyNyA4NC4yMS0zNS4zODQgMC03MS4wMjEtMjMuMjU4LTgzLjQ2NC01NS43NzVhLjcwMi43MDIgMCAwMS0uMDMtLjM3N2MuMTY1LS45NjIuNDk0LTEuODQxLjgxOC0yLjcwNy40NzEtMS4yNTguOTMxLTIuNDg4Ljg2NC0zLjkwNmwtLjIxNS00LjUyOWE1LjUyMyA1LjUyMyAwIDAxMy4xOC01LjI2M2wxLjc5OC0uODQyYTYuOTgyIDYuOTgyIDAgMDAzLjkxMi01LjA3NSA2Ljk5MyA2Ljk5MyAwIDAxNi44ODctNS43MzZjNS4yODIgMCA5Ljg3NSAzLjUxNSAxMS41OSA4LjUxMiA4LjMwNyAyNC4yMTIgMjEuNTExIDQyLjAxNCA1My44NzMgNDIuMDE0eiIgZmlsbD0iI0ZCNjk3MCIvPjwvc3ZnPg==" - }, - "categories": [ - { - "id": 1, - "name": "Marketing" - }, - { - "id": 2, - "name": "Sales" - } - ], - "displayName": "ConvertKit Trigger", - "typeVersion": 1 - } - ], - "categories": [], - "image": [] - } -} diff --git a/cypress/package.json b/cypress/package.json index ffc6404e37..132843c343 100644 --- a/cypress/package.json +++ b/cypress/package.json @@ -10,19 +10,23 @@ "format": "prettier --write . --ignore-path ../.prettierignore", "lint": "eslint . --quiet", "lintfix": "eslint . --fix", + "develop": "cd ..; pnpm dev", "start": "cd ..; pnpm start" }, "devDependencies": { + "@types/lodash": "^4.14.195", "@types/uuid": "^8.3.2", + "eslint-plugin-cypress": "^3.3.0", "n8n-workflow": "workspace:*" }, "dependencies": { - "@ngneat/falso": "^6.4.0", + "@ngneat/falso": "^7.2.0", "@sinonjs/fake-timers": "^11.2.2", "cross-env": "^7.0.3", - "cypress": "^13.6.2", + "cypress": "^13.11.0", "cypress-otp": "^1.0.3", - "cypress-real-events": "^1.11.0", + "cypress-real-events": "^1.12.0", + "lodash": "4.17.21", "start-server-and-test": "^2.0.3", "uuid": "8.3.2" } diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 651c58feb3..55dd8f72d6 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -158,9 +158,7 @@ export class NDV extends BasePage { this.getters.pinnedDataEditor().click(); this.getters .pinnedDataEditor() - .type(`{selectall}{backspace}${pinnedData.replace(new RegExp('{', 'g'), '{{}')}`, { - delay: 0, - }); + .type(`{selectall}{backspace}${pinnedData.replace(new RegExp('{', 'g'), '{{}')}`); this.actions.savePinnedData(); }, @@ -168,10 +166,7 @@ export class NDV extends BasePage { this.getters.editPinnedDataButton().click(); this.getters.pinnedDataEditor().click(); - this.getters - .pinnedDataEditor() - .type('{selectall}{backspace}', { delay: 0 }) - .paste(JSON.stringify(data)); + this.getters.pinnedDataEditor().type('{selectall}{backspace}').paste(JSON.stringify(data)); this.actions.savePinnedData(); }, @@ -181,7 +176,7 @@ export class NDV extends BasePage { typeIntoParameterInput: ( parameterName: string, content: string, - opts?: { parseSpecialCharSequences: boolean; delay?: number }, + opts?: { parseSpecialCharSequences: boolean }, ) => { this.getters.parameterInput(parameterName).type(content, opts); }, @@ -272,16 +267,13 @@ export class NDV extends BasePage { setInvalidExpression: ({ fieldName, invalidExpression, - delay, }: { fieldName: string; invalidExpression?: string; - delay?: number; }) => { this.actions.typeIntoParameterInput(fieldName, '='); this.actions.typeIntoParameterInput(fieldName, invalidExpression ?? "{{ $('unknown')", { parseSpecialCharSequences: false, - delay, }); this.actions.validateExpressionPreview(fieldName, "node doesn't exist"); }, diff --git a/cypress/pages/settings-personal.ts b/cypress/pages/settings-personal.ts index 69227603db..9872fbc668 100644 --- a/cypress/pages/settings-personal.ts +++ b/cypress/pages/settings-personal.ts @@ -30,6 +30,7 @@ export class PersonalSettingsPage extends BasePage { this.getters.themeSelector().click(); this.getters.selectOptionsVisible().should('have.length', 3); this.getters.selectOptionsVisible().contains(theme).click(); + this.getters.saveSettingsButton().realClick(); }, loginAndVisit: (email: string, password: string) => { cy.signin({ email, password }); diff --git a/cypress/pages/template-credential-setup.ts b/cypress/pages/template-credential-setup.ts index 0910d47632..3fa4d20671 100644 --- a/cypress/pages/template-credential-setup.ts +++ b/cypress/pages/template-credential-setup.ts @@ -2,22 +2,6 @@ import * as formStep from '../composables/setup-template-form-step'; import { overrideFeatureFlag } from '../composables/featureFlags'; import { CredentialsModal, MessageBox } from './modals'; -export type TemplateTestData = { - id: number; - fixture: string; -}; - -export const testData = { - simpleTemplate: { - id: 1205, - fixture: 'Test_Template_1.json', - }, - templateWithoutCredentials: { - id: 1344, - fixture: 'Test_Template_2.json', - }, -}; - const credentialsModal = new CredentialsModal(); const messageBox = new MessageBox(); diff --git a/cypress/pages/template-workflow.ts b/cypress/pages/template-workflow.ts deleted file mode 100644 index 84464d0ae6..0000000000 --- a/cypress/pages/template-workflow.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { BasePage } from './base'; - -export class TemplateWorkflowPage extends BasePage { - url = '/templates'; - - getters = { - useTemplateButton: () => cy.get('[data-test-id="use-template-button"]'), - description: () => cy.get('[data-test-id="template-description"]'), - }; - - actions = { - visit: (templateId: number) => { - cy.visit(`${this.url}/${templateId}`); - }, - - clickUseThisWorkflowButton: () => { - this.getters.useTemplateButton().click(); - }, - - openTemplate: ( - template: { - workflow: { - id: number; - name: string; - description: string; - user: { username: string }; - image: Array<{ id: number; url: string }>; - }; - }, - templateHost: string, - ) => { - cy.intercept('GET', `${templateHost}/api/templates/workflows/${template.workflow.id}`, { - statusCode: 200, - body: template, - }).as('getTemplate'); - - this.actions.visit(template.workflow.id); - cy.wait('@getTemplate'); - }, - }; -} diff --git a/cypress/pages/templates.ts b/cypress/pages/templates.ts index 4c0225be48..a17da87ba2 100644 --- a/cypress/pages/templates.ts +++ b/cypress/pages/templates.ts @@ -5,6 +5,7 @@ export class TemplatesPage extends BasePage { getters = { useTemplateButton: () => cy.getByTestId('use-template-button'), + description: () => cy.getByTestId('template-description'), templateCards: () => cy.getByTestId('template-card'), firstTemplateCard: () => this.getters.templateCards().first(), allCategoriesFilter: () => cy.getByTestId('template-filter-all-categories'), @@ -14,50 +15,30 @@ export class TemplatesPage extends BasePage { collectionCountLabel: () => cy.getByTestId('collection-count-label'), templateCountLabel: () => cy.getByTestId('template-count-label'), templatesLoadingContainer: () => cy.getByTestId('templates-loading-container'), - expandCategoriesButton: () => cy.getByTestId('expand-categories-button'), }; actions = { - openSingleTemplateView: (templateId: number) => { - cy.visit(`${this.url}/${templateId}`); - cy.waitForLoad(); - }, - - openOnboardingFlow: (id: number, name: string, workflow: object, templatesHost: string) => { - const apiResponse = { - id, - name, - workflow, - }; + openOnboardingFlow: () => { cy.intercept('POST', '/rest/workflows').as('createWorkflow'); - cy.intercept('GET', `${templatesHost}/api/workflows/templates/${id}`, { - statusCode: 200, - body: apiResponse, - }).as('getTemplate'); cy.intercept('GET', 'rest/workflows/**').as('getWorkflow'); - cy.visit(`/workflows/onboarding/${id}`); + cy.visit('/workflows/onboarding/1'); + cy.window().then((win) => { + win.preventNodeViewBeforeUnload = true; + }); - cy.wait('@getTemplate'); - cy.wait(['@createWorkflow', '@getWorkflow']); + cy.wait(['@getTemplate', '@createWorkflow', '@getWorkflow']); }, - importTemplate: (id: number, name: string, workflow: object, templatesHost: string) => { - const apiResponse = { - id, - name, - workflow, - }; - cy.intercept('GET', `${templatesHost}/api/workflows/templates/${id}`, { - statusCode: 200, - body: apiResponse, - }).as('getTemplate'); + importTemplate: () => { cy.intercept('GET', 'rest/workflows/**').as('getWorkflow'); - cy.visit(`/workflows/templates/${id}`); + cy.visit('/workflows/templates/1'); + cy.window().then((win) => { + win.preventNodeViewBeforeUnload = true; + }); - cy.wait('@getTemplate'); - cy.wait('@getWorkflow'); + cy.wait(['@getTemplate', '@getWorkflow']); }, }; } diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 234da9c9e5..e3a75c508d 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -283,7 +283,7 @@ export class WorkflowPage extends BasePage { }, saveWorkflowUsingKeyboardShortcut: () => { cy.intercept('POST', '/rest/workflows').as('createWorkflow'); - cy.get('body').type(META_KEY, { release: false }).type('s'); + this.actions.hitSaveWorkflow(); }, deleteNode: (name: string) => { this.getters.canvasNodeByName(name).first().click(); @@ -339,35 +339,43 @@ export class WorkflowPage extends BasePage { }); }); }, + /** Certain keyboard shortcuts are not possible on Cypress via a simple `.type`, and some delays are needed to emulate these events */ + hitComboShortcut: (modifier: string, key: string) => { + cy.get('body').wait(100).type(modifier, { delay: 100, release: false }).type(key); + }, hitUndo: () => { - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('z'); + this.actions.hitComboShortcut(`{${META_KEY}}`, 'z'); }, hitRedo: () => { - cy.get('body') - .type(META_KEY, { delay: 500, release: false }) - .type('{shift}', { release: false }) - .type('z'); + cy.get('body').type(`{${META_KEY}+shift+z}`); }, - selectAll: () => { - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('a'); + hitSelectAll: () => { + this.actions.hitComboShortcut(`{${META_KEY}}`, 'a'); + }, + hitDeleteAllNodes: () => { + this.actions.hitSelectAll(); + cy.get('body').type('{backspace}'); }, hitDisableNodeShortcut: () => { cy.get('body').type('d'); }, hitCopy: () => { - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('c'); + this.actions.hitComboShortcut(`{${META_KEY}}`, 'c'); }, hitPinNodeShortcut: () => { cy.get('body').type('p'); }, - hitExecuteWorkflowShortcut: () => { - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('{enter}'); + hitSaveWorkflow: () => { + cy.get('body').type(`{${META_KEY}+s}`); }, - hitDuplicateNodeShortcut: () => { - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('d'); + hitExecuteWorkflow: () => { + cy.get('body').type(`{${META_KEY}+enter}`); }, - hitAddStickyShortcut: () => { - cy.get('body').type('{shift}', { delay: 500, release: false }).type('S'); + hitDuplicateNode: () => { + cy.get('body').type(`{${META_KEY}+d}`); + }, + hitAddSticky: () => { + cy.get('body').type('{shift+S}'); }, executeWorkflow: () => { this.getters.executeWorkflowButton().click(); diff --git a/cypress/scripts/run-e2e.js b/cypress/scripts/run-e2e.js index a5d75c5f4b..8da6b5a857 100755 --- a/cypress/scripts/run-e2e.js +++ b/cypress/scripts/run-e2e.js @@ -50,7 +50,7 @@ switch (scenario) { break; case 'dev': runTests({ - startCommand: 'dev', + startCommand: 'develop', url: 'http://localhost:8080/favicon.ico', testCommand: 'cypress open', customEnv: { diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index dec7d79f5e..da875e3cae 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,5 +1,6 @@ import 'cypress-real-events'; import FakeTimers from '@sinonjs/fake-timers'; +import type { IN8nUISettings } from 'n8n-workflow'; import { WorkflowPage } from '../pages'; import { BACKEND_BASE_URL, @@ -66,9 +67,9 @@ Cypress.Commands.add('signin', ({ email, password }) => { ); }); -Cypress.Commands.add('signinAsOwner', () => { - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); -}); +Cypress.Commands.add('signinAsOwner', () => cy.signin(INSTANCE_OWNER)); +Cypress.Commands.add('signinAsAdmin', () => cy.signin(INSTANCE_ADMIN)); +Cypress.Commands.add('signinAsMember', (index = 0) => cy.signin(INSTANCE_MEMBERS[index])); Cypress.Commands.add('signout', () => { cy.request({ @@ -79,8 +80,9 @@ Cypress.Commands.add('signout', () => { cy.getCookie(N8N_AUTH_COOKIE).should('not.exist'); }); -Cypress.Commands.add('interceptREST', (method, url) => { - cy.intercept(method, `${BACKEND_BASE_URL}/rest${url}`); +export let settings: Partial; +Cypress.Commands.add('overrideSettings', (value: Partial) => { + settings = value; }); const setFeature = (feature: string, enabled: boolean) => diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 0cf29b09a4..3968a09b5b 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,5 +1,6 @@ -import { INSTANCE_OWNER } from '../constants'; -import './commands'; +import cloneDeep from 'lodash/cloneDeep'; +import merge from 'lodash/merge'; +import { settings } from './commands'; before(() => { cy.resetDatabase(); @@ -11,21 +12,54 @@ before(() => { beforeEach(() => { if (!cy.config('disableAutoLogin')) { - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + cy.signinAsOwner(); } cy.window().then((win): void => { win.localStorage.setItem('N8N_THEME', 'light'); }); - cy.intercept('GET', '/rest/settings').as('loadSettings'); + cy.intercept('GET', '/rest/settings', (req) => { + // Disable cache + delete req.headers['if-none-match']; + req.on('response', (res) => { + const defaultSettings = res.body.data; + res.send({ data: merge(cloneDeep(defaultSettings), settings) }); + }); + }).as('loadSettings'); + cy.intercept('GET', '/types/nodes.json').as('loadNodeTypes'); // Always intercept the request to test credentials and return a success cy.intercept('POST', '/rest/credentials/test', { - statusCode: 200, - body: { - data: { status: 'success', message: 'Tested successfully' }, + data: { status: 'success', message: 'Tested successfully' }, + }).as('credentialTest'); + + cy.intercept('POST', '/rest/license/renew', {}); + + cy.intercept({ pathname: '/api/health' }, { status: 'OK' }).as('healthCheck'); + cy.intercept({ pathname: '/api/versions/*' }, [ + { + name: '1.45.1', + createdAt: '2023-08-18T11:53:12.857Z', + hasSecurityIssue: null, + hasSecurityFix: null, + securityIssueFixVersion: null, + hasBreakingChange: null, + documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131', + nodes: [], + description: 'Includes bug fixes', }, - }); + { + name: '1.0.5', + createdAt: '2023-07-24T10:54:56.097Z', + hasSecurityIssue: false, + hasSecurityFix: null, + securityIssueFixVersion: null, + hasBreakingChange: true, + documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104', + nodes: [], + description: 'Includes core functionality and bug fixes', + }, + ]).as('getVersions'); }); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 247dc5745e..a8865fa1ea 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -1,7 +1,11 @@ // Load type definitions that come with Cypress module /// -import type { Interception } from 'cypress/types/net-stubbing'; +import type { IN8nUISettings } from 'n8n-workflow'; + +Cypress.Keyboard.defaults({ + keystrokeDelay: 0, +}); interface SigninPayload { email: string; @@ -22,10 +26,13 @@ declare global { ): Chainable>; findChildByTestId(childTestId: string): Chainable>; createFixtureWorkflow(fixtureKey: string, workflowName: string): void; + /** @deprecated */ signin(payload: SigninPayload): void; signinAsOwner(): void; + signinAsAdmin(): void; + signinAsMember(index?: number): void; signout(): void; - interceptREST(method: string, url: string): Chainable; + overrideSettings(value: Partial): void; enableFeature(feature: string): void; disableFeature(feature: string): void; enableQueueMode(): void; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index dc823f5e5c..cd0a1f3a97 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -4,7 +4,7 @@ "sourceMap": false, "declaration": false, "lib": ["esnext", "dom"], - "types": ["cypress", "node"] + "types": ["cypress", "node", "cypress-real-events"] }, "include": ["**/*.ts"], "exclude": ["**/dist/**/*", "**/node_modules/**/*"], diff --git a/packages/@n8n_io/eslint-config/base.js b/packages/@n8n_io/eslint-config/base.js index 232664d7f1..682c5d68b5 100644 --- a/packages/@n8n_io/eslint-config/base.js +++ b/packages/@n8n_io/eslint-config/base.js @@ -467,7 +467,7 @@ const config = (module.exports = { overrides: [ { - files: ['test/**/*.ts', '**/__tests__/*.ts'], + files: ['test/**/*.ts', '**/__tests__/*.ts', '**/*.cy.ts'], rules: { 'n8n-local-rules/no-plain-errors': 'off', 'n8n-local-rules/no-skipped-tests': diff --git a/packages/editor-ui/src/components/NodeIcon.vue b/packages/editor-ui/src/components/NodeIcon.vue index 1bae71482c..11e76ff22f 100644 --- a/packages/editor-ui/src/components/NodeIcon.vue +++ b/packages/editor-ui/src/components/NodeIcon.vue @@ -104,8 +104,6 @@ const iconSource = computed(() => { // Otherwise, extract it from icon prop if (nodeType.icon) { const icon = getNodeIcon(nodeType, uiStore.appliedTheme); - console.log(nodeType.icon, icon); - if (icon) { const [type, path] = icon.split(':'); if (type === 'file') { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bcb1045587..d22b03546b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,8 +123,8 @@ importers: cypress: dependencies: '@ngneat/falso': - specifier: ^6.4.0 - version: 6.4.0 + specifier: ^7.2.0 + version: 7.2.0 '@sinonjs/fake-timers': specifier: ^11.2.2 version: 11.2.2 @@ -132,14 +132,17 @@ importers: specifier: ^7.0.3 version: 7.0.3 cypress: - specifier: ^13.6.2 - version: 13.6.2 + specifier: ^13.11.0 + version: 13.11.0 cypress-otp: specifier: ^1.0.3 version: 1.0.3 cypress-real-events: - specifier: ^1.11.0 - version: 1.11.0(cypress@13.6.2) + specifier: ^1.12.0 + version: 1.12.0(cypress@13.11.0) + lodash: + specifier: 4.17.21 + version: 4.17.21 start-server-and-test: specifier: ^2.0.3 version: 2.0.3 @@ -147,9 +150,15 @@ importers: specifier: 8.3.2 version: 8.3.2 devDependencies: + '@types/lodash': + specifier: ^4.14.195 + version: 4.14.195 '@types/uuid': specifier: ^8.3.2 version: 8.3.4 + eslint-plugin-cypress: + specifier: ^3.3.0 + version: 3.3.0(eslint@8.57.0) n8n-workflow: specifier: workspace:* version: link:../packages/workflow @@ -4118,8 +4127,8 @@ packages: '@ndelangen/get-tarball@3.0.7': resolution: {integrity: sha512-NqGfTZIZpRFef1GoVaShSSRwDC3vde3ThtTeqFdcYd6ipKqnfEVhjK2hUeHjCQUcptyZr2TONqcloFXM+5QBrQ==} - '@ngneat/falso@6.4.0': - resolution: {integrity: sha512-f6r036h2fX/AoHw1eV2t8+qWQwrbSrozs3zXMhhwoO7SJBc+DGMxRWEhFeYIinfwx0uhUH8ggx5+PDLzYESLOA==} + '@ngneat/falso@7.2.0': + resolution: {integrity: sha512-283EXBFd05kCbGuGSXgmvhCsQYEYzvD/eJaE7lxd05qRB0tgREvZX7TRlJ1KSp8nHxoK6Ws029G1Y30mt4IVAA==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -7296,13 +7305,13 @@ packages: cypress-otp@1.0.3: resolution: {integrity: sha512-o7LssfI0HRHa+TkaOE5/Aukv6M9vsoZAtYESr9m7Ky2i+HRNb2p/IRelE7Z0wJ/UK2f+nXAGZIfXqraf9EPDqw==} - cypress-real-events@1.11.0: - resolution: {integrity: sha512-4LXVRsyq+xBh5TmlEyO1ojtBXtN7xw720Pwb9rEE9rkJuXmeH3VyoR1GGayMGr+Itqf11eEjfDewtDmcx6PWPQ==} + cypress-real-events@1.12.0: + resolution: {integrity: sha512-oiy+4kGKkzc2PT36k3GGQqkGxNiVypheWjMtfyi89iIk6bYmTzeqxapaLHS3pnhZOX1IEbTDUVxh8T4Nhs1tyQ==} peerDependencies: cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x - cypress@13.6.2: - resolution: {integrity: sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==} + cypress@13.11.0: + resolution: {integrity: sha512-NXXogbAxVlVje4XHX+Cx5eMFZv4Dho/2rIcdBHg9CNPFUGZdM4cRdgIgM7USmNYsC12XY0bZENEQ+KBk72fl+A==} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -7900,6 +7909,11 @@ packages: eslint-import-resolver-webpack: optional: true + eslint-plugin-cypress@3.3.0: + resolution: {integrity: sha512-HPHMPzYBIshzJM8wqgKSKHG2p/8R0Gbg4Pb3tcdC9WrmkuqxiKxSKbjunUrajhV5l7gCIFrh1P7C7GuBqH6YuQ==} + peerDependencies: + eslint: '>=7' + eslint-plugin-import@2.29.1: resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} @@ -17147,7 +17161,7 @@ snapshots: pump: 3.0.0 tar-fs: 2.1.1 - '@ngneat/falso@6.4.0': + '@ngneat/falso@7.2.0': dependencies: seedrandom: 3.0.5 uuid: 8.3.2 @@ -21458,15 +21472,14 @@ snapshots: dependencies: otplib: 12.0.1 - cypress-real-events@1.11.0(cypress@13.6.2): + cypress-real-events@1.12.0(cypress@13.11.0): dependencies: - cypress: 13.6.2 + cypress: 13.11.0 - cypress@13.6.2: + cypress@13.11.0: dependencies: '@cypress/request': 3.0.1 '@cypress/xvfb': 1.2.4(supports-color@8.1.1) - '@types/node': 18.16.16 '@types/sinonjs__fake-timers': 8.1.1 '@types/sizzle': 2.3.3 arch: 2.2.0 @@ -22223,6 +22236,11 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-cypress@3.3.0(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + globals: 13.20.0 + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.7