Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: Refactor e2e tests to be less flaky (no-changelog) #9695

Merged
merged 16 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand Down
12 changes: 11 additions & 1 deletion cypress/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
},
};
2 changes: 1 addition & 1 deletion cypress/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 2 additions & 3 deletions cypress/e2e/10-undo-redo.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions cypress/e2e/12-canvas-actions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!');
Expand All @@ -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);
Expand Down
18 changes: 8 additions & 10 deletions cypress/e2e/12-canvas.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
});

Expand Down
18 changes: 9 additions & 9 deletions cypress/e2e/17-sharing.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -144,15 +144,15 @@ 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();
credentialsModal.getters.testSuccessTag().should('be.visible');
});

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();
Expand All @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/18-user-management.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 2 additions & 5 deletions cypress/e2e/25-stickies.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 7 additions & 32 deletions cypress/e2e/27-cloud.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,25 @@ 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);
planData.expirationDate = fiveDaysFromNow.toJSON();
});

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');
});
Expand All @@ -49,30 +38,18 @@ 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');

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');
Expand All @@ -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']);

Expand Down
3 changes: 1 addition & 2 deletions cypress/e2e/27-two-factor-authentication.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
3 changes: 1 addition & 2 deletions cypress/e2e/28-debug.cy.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();

Expand Down
Loading
Loading