diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml
index 49c37fd4c896b..b7dadc4173b9f 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 dc6abe3a7125f..36fc0af7e27d7 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 39c755738ce9f..7efd9b0470664 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 19465ed74951f..89d1bad69685e 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 fae27a545c8af..fafb1d9d79254 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 db6b38d53a7b6..79b6c200e1846 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 7908e8d1287f5..cce375b05d8d6 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 c9f3cc08cbfb2..b53b0fdf53a9d 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 35416ebd3e42d..2b83f9abf9b7d 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 6fdcfba295535..dd0d3b06ba6cc 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 7a1ee28f2266b..21319dd79b5a5 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 71c733c2548ff..5d2bd76cac3a4 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 d5f0a67f7e127..5cc6657416a7b 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]+/);
+ 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: [],
+ },
+ ];
+
+ 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"');
});
- mainSidebar.getters.templates().parent('a').should('have.attr', 'target', '_blank');
- });
+ });
+
+ 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=');
- 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');
+ // 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 423c92110b178..6258a7698ecd7 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 ba3edbe4c9a17..de9afc28915aa 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 7553a55a7b9df..c5d9f2643f24d 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 05e70aa339ab4..c8585118e7270 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 2d93223ebb81f..1d4fc5180882f 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',
- },
- {
- 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.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',
},
- ]);
-
- 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 96e98a00312d0..4301e20ac63c3 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 4b86bdbb20ca1..35021e733309d 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 d18dc85d453d6..7c7c3be554073 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 5a0a1eb5ad5b1..fe8ba3e3e42c4 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 c77be3db9c28d..0000000000000
--- 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 4d3ca1e548f9a..0000000000000
--- 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 ffc6404e37639..132843c34329b 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 651c58feb35af..55dd8f72d648c 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 69227603dbc78..9872fbc668d33 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 0910d47632074..3fa4d2067103d 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 84464d0ae60cb..0000000000000
--- 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 4c0225be48bbd..a17da87ba2330 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 234da9c9e557e..e3a75c508db80 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}`);
+ },
+ hitSelectAll: () => {
+ this.actions.hitComboShortcut(`{${META_KEY}}`, 'a');
},
- selectAll: () => {
- cy.get('body').type(META_KEY, { delay: 500, release: false }).type('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}`);
+ },
+ hitExecuteWorkflow: () => {
+ cy.get('body').type(`{${META_KEY}+enter}`);
},
- hitDuplicateNodeShortcut: () => {
- cy.get('body').type(META_KEY, { delay: 500, release: false }).type('d');
+ hitDuplicateNode: () => {
+ cy.get('body').type(`{${META_KEY}+d}`);
},
- hitAddStickyShortcut: () => {
- cy.get('body').type('{shift}', { delay: 500, release: false }).type('S');
+ 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 a5d75c5f4b71d..8da6b5a857fe8 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 dec7d79f5ea29..da875e3caed67 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 0cf29b09a433d..3968a09b5b0ba 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 247dc5745ecbc..a8865fa1ea764 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 dc823f5e5cfb3..cd0a1f3a97ee6 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 232664d7f126a..682c5d68b5259 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 1bae71482c011..11e76ff22f9a4 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 bcb10455873d7..d22b03546b259 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