refactor: Set up Cypress as pnpm workspace (no-changelog) (#6049)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Iván Ovejero 2024-06-10 15:49:50 +02:00 committed by GitHub
parent bc3dcf706f
commit af3ac2db28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
79 changed files with 435 additions and 315 deletions

View file

@ -87,7 +87,7 @@ jobs:
git fetch origin pull/${{ inputs.pr_number }}/head git fetch origin pull/${{ inputs.pr_number }}/head
git checkout FETCH_HEAD git checkout FETCH_HEAD
- uses: pnpm/action-setup@v2.4.0 - uses: pnpm/action-setup@v4.0.0
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@ -103,6 +103,7 @@ jobs:
VUE_APP_MAX_PINNED_DATA_SIZE: 16384 VUE_APP_MAX_PINNED_DATA_SIZE: 16384
- name: Cypress install - name: Cypress install
working-directory: cypress
run: pnpm cypress:install run: pnpm cypress:install
- name: Cache build artifacts - name: Cache build artifacts
@ -138,7 +139,7 @@ jobs:
git fetch origin pull/${{ inputs.pr_number }}/head git fetch origin pull/${{ inputs.pr_number }}/head
git checkout FETCH_HEAD git checkout FETCH_HEAD
- uses: pnpm/action-setup@v2.4.0 - uses: pnpm/action-setup@v4.0.0
- name: Restore cached pnpm modules - name: Restore cached pnpm modules
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.0
@ -155,6 +156,7 @@ jobs:
- name: Cypress run - name: Cypress run
uses: cypress-io/github-action@v6.6.1 uses: cypress-io/github-action@v6.6.1
with: with:
working-directory: cypress
install: false install: false
start: pnpm start start: pnpm start
wait-on: 'http://localhost:5678' wait-on: 'http://localhost:5678'
@ -165,7 +167,7 @@ jobs:
# in the same parent workflow # in the same parent workflow
ci-build-id: ${{ needs.prepare.outputs.uuid }} ci-build-id: ${{ needs.prepare.outputs.uuid }}
spec: '/__w/n8n/n8n/cypress/${{ inputs.spec }}' spec: '/__w/n8n/n8n/cypress/${{ inputs.spec }}'
config-file: /__w/n8n/n8n/cypress.config.js config-file: /__w/n8n/n8n/cypress/cypress.config.js
env: env:
NODE_OPTIONS: --dns-result-order=ipv4first NODE_OPTIONS: --dns-result-order=ipv4first
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

3
.gitignore vendored
View file

@ -18,9 +18,6 @@ nodelinter.config.json
packages/**/.turbo packages/**/.turbo
.turbo .turbo
*.tsbuildinfo *.tsbuildinfo
cypress/videos/*
cypress/screenshots/*
cypress/downloads/*
*.swp *.swp
CHANGELOG-*.md CHANGELOG-*.md
*.mdx *.mdx

24
cypress/.eslintrc.js Normal file
View file

@ -0,0 +1,24 @@
const sharedOptions = require('@n8n_io/eslint-config/shared');
/**
* @type {import('@types/eslint').ESLint.ConfigData}
*/
module.exports = {
extends: ['@n8n_io/eslint-config/base'],
...sharedOptions(__dirname),
rules: {
// TODO: remove these rules
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/promise-function-async': 'off',
'n8n-local-rules/no-uncaught-json-parse': 'off',
},
};

3
cypress/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
videos/
screenshots/
downloads/

4
cypress/augmentation.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module 'cypress-otp' {
// eslint-disable-next-line import/no-default-export
export default function generateOTPToken(secret: string): string;
}

View file

@ -10,7 +10,7 @@ export const getCloseBecomeTemplateCreatorCtaButton = () =>
//#region Actions //#region Actions
export const interceptCtaRequestWithResponse = (becomeCreator: boolean) => { export const interceptCtaRequestWithResponse = (becomeCreator: boolean) => {
return cy.intercept('GET', `/rest/cta/become-creator`, { return cy.intercept('GET', '/rest/cta/become-creator', {
body: becomeCreator, body: becomeCreator,
}); });
}; };

View file

@ -42,7 +42,7 @@ export function closeCredentialModal() {
getCredentialModalCloseButton().click(); getCredentialModalCloseButton().click();
} }
export function setCredentialValues(values: Record<string, any>, save = true) { export function setCredentialValues(values: Record<string, string>, save = true) {
Object.entries(values).forEach(([key, value]) => { Object.entries(values).forEach(([key, value]) => {
setCredentialConnectionParameterInputByName(key, value); setCredentialConnectionParameterInputByName(key, value);
}); });

View file

@ -2,7 +2,7 @@
* Getters * Getters
*/ */
import { getVisibleSelect } from "../utils"; import { getVisibleSelect } from '../utils';
export function getCredentialSelect(eq = 0) { export function getCredentialSelect(eq = 0) {
return cy.getByTestId('node-credentials-select').eq(eq); return cy.getByTestId('node-credentials-select').eq(eq);
@ -75,7 +75,7 @@ export function setParameterInputByName(name: string, value: string) {
} }
export function toggleParameterCheckboxInputByName(name: string) { export function toggleParameterCheckboxInputByName(name: string) {
getParameterInputByName(name).find('input[type="checkbox"]').realClick() getParameterInputByName(name).find('input[type="checkbox"]').realClick();
} }
export function setParameterSelectByContent(name: string, content: string) { export function setParameterSelectByContent(name: string, content: string) {

View file

@ -2,4 +2,4 @@
* Getters * Getters
*/ */
export const getSetupWorkflowCredentialsButton = () => cy.get(`button:contains("Set up template")`); export const getSetupWorkflowCredentialsButton = () => cy.get('button:contains("Set up template")');

View file

@ -51,7 +51,7 @@ export function getNodeByName(name: string) {
export function disableNode(name: string) { export function disableNode(name: string) {
const target = getNodeByName(name); const target = getNodeByName(name);
target.rightclick(name ? 'center' : 'topLeft', { force: true }); target.rightclick(name ? 'center' : 'topLeft', { force: true });
cy.getByTestId(`context-menu-item-toggle_activation`).click(); cy.getByTestId('context-menu-item-toggle_activation').click();
} }
export function getConnectionBySourceAndTarget(source: string, target: string) { export function getConnectionBySourceAndTarget(source: string, target: string) {

View file

@ -18,6 +18,12 @@ module.exports = defineConfig({
screenshotOnRunFailure: true, screenshotOnRunFailure: true,
experimentalInteractiveRunEvents: true, experimentalInteractiveRunEvents: true,
experimentalSessionAndOrigin: true, experimentalSessionAndOrigin: true,
specPattern: 'e2e/**/*.ts',
supportFile: 'support/e2e.ts',
fixturesFolder: 'fixtures',
downloadsFolder: 'downloads',
screenshotsFolder: 'screenshots',
videosFolder: 'videos',
}, },
env: { env: {
MAX_PINNED_DATA_SIZE: process.env.VUE_APP_MAX_PINNED_DATA_SIZE MAX_PINNED_DATA_SIZE: process.env.VUE_APP_MAX_PINNED_DATA_SIZE

View file

@ -1,6 +1,6 @@
import { v4 as uuid } from 'uuid';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { v4 as uuid } from 'uuid';
const WorkflowsPage = new WorkflowsPageClass(); const WorkflowsPage = new WorkflowsPageClass();
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();

View file

@ -1,5 +1,9 @@
import { CODE_NODE_NAME, SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from './../constants'; import {
import { SCHEDULE_TRIGGER_NODE_NAME } from '../constants'; SCHEDULE_TRIGGER_NODE_NAME,
CODE_NODE_NAME,
SET_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
} from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { MessageBox as MessageBoxClass } from '../pages/modals/message-box'; import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
import { NDV } from '../pages/ndv'; import { NDV } from '../pages/ndv';
@ -338,8 +342,8 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1); cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')) cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch'))
.should('have.css', 'left', `637px`) .should('have.css', 'left', '637px')
.should('have.css', 'top', `501px`); .should('have.css', 'top', '501px');
cy.fixture('Test_workflow_form_switch.json').then((data) => { cy.fixture('Test_workflow_form_switch.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));
@ -353,8 +357,8 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1); cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')) cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch'))
.should('have.css', 'left', `637px`) .should('have.css', 'left', '637px')
.should('have.css', 'top', `501px`); .should('have.css', 'top', '501px');
}); });
it('should not undo/redo when NDV or a modal is open', () => { it('should not undo/redo when NDV or a modal is open', () => {

View file

@ -8,7 +8,7 @@ describe('Inline expression editor', () => {
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Schedule'); WorkflowPage.actions.addInitialNodeToCanvas('Schedule');
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError'); cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
}); });
describe('Static data', () => { describe('Static data', () => {

View file

@ -1,3 +1,4 @@
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { import {
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME,
@ -7,7 +8,6 @@ import {
IF_NODE_NAME, IF_NODE_NAME,
HTTP_REQUEST_NODE_NAME, HTTP_REQUEST_NODE_NAME,
} from './../constants'; } from './../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
describe('Canvas Actions', () => { describe('Canvas Actions', () => {

View file

@ -1,3 +1,5 @@
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV, WorkflowExecutionsTab } from '../pages';
import { import {
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME,
@ -7,8 +9,6 @@ import {
SWITCH_NODE_NAME, SWITCH_NODE_NAME,
MERGE_NODE_NAME, MERGE_NODE_NAME,
} from './../constants'; } from './../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV, WorkflowExecutionsTab } from '../pages';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
const ExecutionsTab = new WorkflowExecutionsTab(); const ExecutionsTab = new WorkflowExecutionsTab();
@ -258,7 +258,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.pinchToZoom(1, 'zoomOut'); WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
// Zoom in 1x + Zoom out 1x should reset to default (=1) // Zoom in 1x + Zoom out 1x should reset to default (=1)
WorkflowPage.getters.nodeView().should('have.css', 'transform', `matrix(1, 0, 0, 1, 0, 0)`); WorkflowPage.getters.nodeView().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)');
WorkflowPage.actions.pinchToZoom(1, 'zoomOut'); WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
WorkflowPage.getters WorkflowPage.getters

View file

@ -136,7 +136,7 @@ describe('Data pinning', () => {
ndv.actions.pastePinnedData([ ndv.actions.pastePinnedData([
{ {
test: '1'.repeat(Cypress.env('MAX_PINNED_DATA_SIZE')), test: '1'.repeat(Cypress.env('MAX_PINNED_DATA_SIZE') as number),
}, },
]); ]);
workflowPage.getters workflowPage.getters
@ -151,10 +151,8 @@ describe('Data pinning', () => {
ndv.getters.pinDataButton().should('not.exist'); ndv.getters.pinDataButton().should('not.exist');
ndv.getters.editPinnedDataButton().should('be.visible'); ndv.getters.editPinnedDataButton().should('be.visible');
ndv.actions.setPinnedData('[ { "name": "First item", "code": 2dsa }]') ndv.actions.setPinnedData('[ { "name": "First item", "code": 2dsa }]');
workflowPage.getters workflowPage.getters.errorToast().should('contain', 'Unable to save due to invalid JSON');
.errorToast()
.should('contain', 'Unable to save due to invalid JSON');
}); });
it('Should be able to reference paired items in a node located before pinned data', () => { it('Should be able to reference paired items in a node located before pinned data', () => {
@ -168,6 +166,7 @@ describe('Data pinning', () => {
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true); workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
setExpressionOnStringValueInSet(`{{ $('${HTTP_REQUEST_NODE_NAME}').item`); setExpressionOnStringValueInSet(`{{ $('${HTTP_REQUEST_NODE_NAME}').item`);
const output = '[Object: {"json": {"http": 123}, "pairedItem": {"item": 0}}]'; const output = '[Object: {"json": {"http": 123}, "pairedItem": {"item": 0}}]';

View file

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { WorkflowPage, NDV } from '../pages'; import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const wf = new WorkflowPage(); const wf = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();

View file

@ -1,10 +1,10 @@
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
import { import {
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME,
SCHEDULE_TRIGGER_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME,
} from './../constants'; } from './../constants';
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -170,7 +170,7 @@ describe('Data mapping', () => {
}); });
it('maps expressions from previous nodes', () => { it('maps expressions from previous nodes', () => {
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`); cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow');
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set1'); workflowPage.actions.openNode('Set1');

View file

@ -1,12 +1,13 @@
import { WorkflowPage, WorkflowsPage, NDV } from '../pages'; import { WorkflowPage, WorkflowsPage, NDV } from '../pages';
import { BACKEND_BASE_URL } from '../constants'; import { BACKEND_BASE_URL } from '../constants';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import type { ExecutionResponse } from '../types';
const workflowsPage = new WorkflowsPage(); const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('Schedule Trigger node', async () => { describe('Schedule Trigger node', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
}); });
@ -37,30 +38,34 @@ describe('Schedule Trigger node', async () => {
const workflowId = url.split('/').pop(); const workflowId = url.split('/').pop();
cy.wait(1200); cy.wait(1200);
cy.request('GET', `${BACKEND_BASE_URL}/rest/executions`).then((response) => { cy.request<ExecutionResponse>('GET', `${BACKEND_BASE_URL}/rest/executions`).then(
expect(response.status).to.eq(200); (response) => {
expect(workflowId).to.not.be.undefined;
expect(response.body.data.results.length).to.be.greaterThan(0);
const matchingExecutions = response.body.data.results.filter(
(execution: any) => execution.workflowId === workflowId,
);
expect(matchingExecutions).to.have.length(1);
cy.wait(1200);
cy.request('GET', `${BACKEND_BASE_URL}/rest/executions`).then((response) => {
expect(response.status).to.eq(200); expect(response.status).to.eq(200);
expect(workflowId).to.not.be.undefined;
expect(response.body.data.results.length).to.be.greaterThan(0); expect(response.body.data.results.length).to.be.greaterThan(0);
const matchingExecutions = response.body.data.results.filter( const matchingExecutions = response.body.data.results.filter(
(execution: any) => execution.workflowId === workflowId, (execution) => execution.workflowId === workflowId,
); );
expect(matchingExecutions).to.have.length(2); expect(matchingExecutions).to.have.length(1);
workflowPage.actions.activateWorkflow(); cy.wait(1200);
workflowPage.getters.activatorSwitch().should('not.have.class', 'is-checked'); cy.request<ExecutionResponse>('GET', `${BACKEND_BASE_URL}/rest/executions`).then(
cy.visit(workflowsPage.url); (response1) => {
workflowsPage.actions.deleteWorkFlow('Schedule Trigger Workflow'); expect(response1.status).to.eq(200);
}); expect(response1.body.data.results.length).to.be.greaterThan(0);
}); const matchingExecutions1 = response1.body.data.results.filter(
(execution: any) => execution.workflowId === workflowId,
);
expect(matchingExecutions1).to.have.length(2);
workflowPage.actions.activateWorkflow();
workflowPage.getters.activatorSwitch().should('not.have.class', 'is-checked');
cy.visit(workflowsPage.url);
workflowsPage.actions.deleteWorkFlow('Schedule Trigger Workflow');
},
);
},
);
}); });
}); });
}); });

View file

@ -1,5 +1,5 @@
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
import { cowBase64 } from '../support/binaryTestFiles'; import { cowBase64 } from '../support/binaryTestFiles';
import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants'; import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
@ -75,7 +75,7 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
} }
}; };
describe('Webhook Trigger node', async () => { describe('Webhook Trigger node', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
}); });
@ -121,10 +121,12 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook); cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { cy.request<{ MyValue: number }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
expect(response.status).to.eq(200); (response) => {
expect(response.body.MyValue).to.eq(1234); expect(response.status).to.eq(200);
}); expect(response.body.MyValue).to.eq(1234);
},
);
}); });
it('should listen for a GET request and respond custom status code 201', () => { it('should listen for a GET request and respond custom status code 201', () => {
@ -161,10 +163,12 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook); cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { cy.request<{ MyValue: number }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
expect(response.status).to.eq(200); (response) => {
expect(response.body.MyValue).to.eq(1234); expect(response.status).to.eq(200);
}); expect(response.body.MyValue).to.eq(1234);
},
);
}); });
it('should listen for a GET request and respond with last node binary data', () => { it('should listen for a GET request and respond with last node binary data', () => {
@ -200,10 +204,12 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook); cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { cy.request<{ data: unknown }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
expect(response.status).to.eq(200); (response) => {
expect(Object.keys(response.body).includes('data')).to.be.true; expect(response.status).to.eq(200);
}); expect(Object.keys(response.body).includes('data')).to.be.true;
},
);
}); });
it('should listen for a GET request and respond with an empty body', () => { it('should listen for a GET request and respond with an empty body', () => {
@ -217,10 +223,12 @@ describe('Webhook Trigger node', async () => {
}); });
ndv.actions.execute(); ndv.actions.execute();
cy.wait(waitForWebhook); cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { cy.request<{ MyValue: unknown }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
expect(response.status).to.eq(200); (response) => {
expect(response.body.MyValue).to.be.undefined; expect(response.status).to.eq(200);
}); expect(response.body.MyValue).to.be.undefined;
},
);
}); });
it('should listen for a GET request with Basic Authentication', () => { it('should listen for a GET request with Basic Authentication', () => {

View file

@ -187,7 +187,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
workflowPage.getters.successToast().should('contain', 'User deleted'); workflowPage.getters.successToast().should('contain', 'User deleted');
}); });
it(`should allow user to change their personal data`, () => { it('should allow user to change their personal data', () => {
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password); personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.actions.updateFirstAndLastName( personalSettingsPage.actions.updateFirstAndLastName(
updatedPersonalData.newFirstName, updatedPersonalData.newFirstName,
@ -199,15 +199,15 @@ describe('User Management', { disableAutoLogin: true }, () => {
workflowPage.getters.successToast().should('contain', 'Personal details updated'); workflowPage.getters.successToast().should('contain', 'Personal details updated');
}); });
it(`shouldn't allow user to set weak password`, () => { it("shouldn't allow user to set weak password", () => {
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password); personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.getters.changePasswordLink().click(); personalSettingsPage.getters.changePasswordLink().click();
for (let weakPass of updatedPersonalData.invalidPasswords) { for (const weakPass of updatedPersonalData.invalidPasswords) {
personalSettingsPage.actions.tryToSetWeakPassword(INSTANCE_OWNER.password, weakPass); personalSettingsPage.actions.tryToSetWeakPassword(INSTANCE_OWNER.password, weakPass);
} }
}); });
it(`shouldn't allow user to change password if old password is wrong`, () => { it("shouldn't allow user to change password if old password is wrong", () => {
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password); personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.getters.changePasswordLink().click(); personalSettingsPage.getters.changePasswordLink().click();
personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword); personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword);
@ -217,7 +217,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
.should('contain', 'Provided current password is incorrect.'); .should('contain', 'Provided current password is incorrect.');
}); });
it(`should change current user password`, () => { it('should change current user password', () => {
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password); personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.getters.changePasswordLink().click(); personalSettingsPage.getters.changePasswordLink().click();
personalSettingsPage.actions.updatePassword( personalSettingsPage.actions.updatePassword(
@ -231,7 +231,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
); );
}); });
it(`shouldn't allow users to set invalid email`, () => { it("shouldn't allow users to set invalid email", () => {
personalSettingsPage.actions.loginAndVisit( personalSettingsPage.actions.loginAndVisit(
INSTANCE_OWNER.email, INSTANCE_OWNER.email,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,
@ -242,7 +242,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('.')[0]); personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('.')[0]);
}); });
it(`should change user email`, () => { it('should change user email', () => {
personalSettingsPage.actions.loginAndVisit( personalSettingsPage.actions.loginAndVisit(
INSTANCE_OWNER.email, INSTANCE_OWNER.email,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,

View file

@ -512,8 +512,9 @@ describe('Execution', () => {
expect(interception.request.body).to.have.property('runData').that.is.an('object'); expect(interception.request.body).to.have.property('runData').that.is.an('object');
const expectedKeys = ['When clicking Test workflow', 'fetch 5 random users']; const expectedKeys = ['When clicking Test workflow', 'fetch 5 random users'];
expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(expectedKeys.length); const { runData } = interception.request.body as Record<string, object>;
expect(interception.request.body.runData).to.include.all.keys(expectedKeys); expect(Object.keys(runData)).to.have.lengthOf(expectedKeys.length);
expect(runData).to.include.all.keys(expectedKeys);
}); });
}); });
@ -537,10 +538,9 @@ describe('Execution', () => {
expect(interception.request.body).to.have.property('pinData').that.is.an('object'); expect(interception.request.body).to.have.property('pinData').that.is.an('object');
const expectedPinnedDataKeys = ['Webhook']; const expectedPinnedDataKeys = ['Webhook'];
expect(Object.keys(interception.request.body.pinData)).to.have.lengthOf( const { pinData } = interception.request.body as Record<string, object>;
expectedPinnedDataKeys.length, expect(Object.keys(pinData)).to.have.lengthOf(expectedPinnedDataKeys.length);
); expect(pinData).to.include.all.keys(expectedPinnedDataKeys);
expect(interception.request.body.pinData).to.include.all.keys(expectedPinnedDataKeys);
}); });
workflowPage.getters.clearExecutionDataButton().should('be.visible'); workflowPage.getters.clearExecutionDataButton().should('be.visible');
@ -558,15 +558,12 @@ describe('Execution', () => {
const expectedPinnedDataKeys = ['Webhook']; const expectedPinnedDataKeys = ['Webhook'];
const expectedRunDataKeys = ['If', 'Webhook']; const expectedRunDataKeys = ['If', 'Webhook'];
expect(Object.keys(interception.request.body.pinData)).to.have.lengthOf( const { pinData, runData } = interception.request.body as Record<string, object>;
expectedPinnedDataKeys.length, expect(Object.keys(pinData)).to.have.lengthOf(expectedPinnedDataKeys.length);
); expect(pinData).to.include.all.keys(expectedPinnedDataKeys);
expect(interception.request.body.pinData).to.include.all.keys(expectedPinnedDataKeys);
expect(Object.keys(interception.request.body.runData)).to.have.lengthOf( expect(Object.keys(runData)).to.have.lengthOf(expectedRunDataKeys.length);
expectedRunDataKeys.length, expect(runData).to.include.all.keys(expectedRunDataKeys);
);
expect(interception.request.body.runData).to.include.all.keys(expectedRunDataKeys);
}); });
}); });
@ -617,6 +614,6 @@ describe('Execution', () => {
.within(() => cy.get('.fa-check')) .within(() => cy.get('.fa-check'))
.should('exist'); .should('exist');
workflowPage.getters.errorToast().should('contain', `Problem in node Telegram`); workflowPage.getters.errorToast().should('contain', 'Problem in node Telegram');
}); });
}); });

View file

@ -1,3 +1,4 @@
import type { ICredentialType } from 'n8n-workflow';
import { import {
GMAIL_NODE_NAME, GMAIL_NODE_NAME,
HTTP_REQUEST_NODE_NAME, HTTP_REQUEST_NODE_NAME,
@ -209,7 +210,7 @@ describe('Credentials', () => {
req.headers['cache-control'] = 'no-cache, no-store'; req.headers['cache-control'] = 'no-cache, no-store';
req.on('response', (res) => { req.on('response', (res) => {
const credentials = res.body || []; const credentials: ICredentialType[] = res.body || [];
const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api'); const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api');

View file

@ -1,6 +1,6 @@
import type { RouteHandler } from 'cypress/types/net-stubbing';
import { WorkflowPage } from '../pages'; import { WorkflowPage } from '../pages';
import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab'; import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
import type { RouteHandler } from 'cypress/types/net-stubbing';
import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json'; import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
@ -11,7 +11,7 @@ const executionsRefreshInterval = 4000;
describe('Current Workflow Executions', () => { describe('Current Workflow Executions', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`); cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
}); });
it('should render executions tab correctly', () => { it('should render executions tab correctly', () => {
@ -58,8 +58,8 @@ describe('Current Workflow Executions', () => {
}); });
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => { it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
const throttleResponse: RouteHandler = (req) => { const throttleResponse: RouteHandler = async (req) => {
return new Promise((resolve) => { return await new Promise((resolve) => {
setTimeout(() => resolve(req.continue()), 2000); setTimeout(() => resolve(req.continue()), 2000);
}); });
}; };
@ -89,6 +89,7 @@ describe('Current Workflow Executions', () => {
.should('be.visible') .should('be.visible')
.its('0.contentDocument.body') // Access the body of the iframe document .its('0.contentDocument.body') // Access the body of the iframe document
.should('not.be.empty') // Ensure the body is not empty .should('not.be.empty') // Ensure the body is not empty
// eslint-disable-next-line @typescript-eslint/unbound-method
.then(cy.wrap) .then(cy.wrap)
.find('.el-notification:has(.el-notification--error)') .find('.el-notification:has(.el-notification--error)')
.should('be.visible') .should('be.visible')

View file

@ -1,3 +1,4 @@
import type { ICredentialType } from 'n8n-workflow';
import { NodeCreator } from '../pages/features/node-creator'; import { NodeCreator } from '../pages/features/node-creator';
import CustomNodeFixture from '../fixtures/Custom_node.json'; import CustomNodeFixture from '../fixtures/Custom_node.json';
import { CredentialsModal, WorkflowPage } from '../pages'; import { CredentialsModal, WorkflowPage } from '../pages';
@ -33,9 +34,9 @@ describe('Community Nodes', () => {
req.headers['cache-control'] = 'no-cache, no-store'; req.headers['cache-control'] = 'no-cache, no-store';
req.on('response', (res) => { req.on('response', (res) => {
const credentials = res.body || []; const credentials: ICredentialType[] = res.body || [];
credentials.push(CustomCredential); credentials.push(CustomCredential as ICredentialType);
}); });
}); });

View file

@ -37,8 +37,9 @@ describe('ADO-2106 connections should be colored correctly for pinned data in ex
.should('be.visible') .should('be.visible')
.its('0.contentDocument.body') .its('0.contentDocument.body')
.should('not.be.empty') .should('not.be.empty')
// eslint-disable-next-line @typescript-eslint/unbound-method
.then(cy.wrap) .then(cy.wrap)
.find(`.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]`) .find('.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]')
.should('have.class', 'success') .should('have.class', 'success')
.should('have.class', 'has-run') .should('have.class', 'has-run')
.should('not.have.class', 'pinned'); .should('not.have.class', 'pinned');
@ -56,8 +57,9 @@ describe('ADO-2106 connections should be colored correctly for pinned data in ex
.should('be.visible') .should('be.visible')
.its('0.contentDocument.body') .its('0.contentDocument.body')
.should('not.be.empty') .should('not.be.empty')
// eslint-disable-next-line @typescript-eslint/unbound-method
.then(cy.wrap) .then(cy.wrap)
.find(`.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]`) .find('.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]')
.should('have.class', 'success') .should('have.class', 'success')
.should('have.class', 'has-run') .should('have.class', 'has-run')
.should('have.class', 'pinned'); .should('have.class', 'pinned');

View file

@ -7,7 +7,7 @@ describe('ADO-2230 NDV Pagination Reset', () => {
it('should reset pagaintion if data size changes to less than current page', () => { it('should reset pagaintion if data size changes to less than current page', () => {
// setup, load workflow with debughelper node with random seed // setup, load workflow with debughelper node with random seed
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('NDV-debug-generate-data.json', `Debug workflow`); cy.createFixtureWorkflow('NDV-debug-generate-data.json', 'Debug workflow');
workflowPage.actions.openNode('DebugHelper'); workflowPage.actions.openNode('DebugHelper');
// execute node outputting 10 pages, check output of first page // execute node outputting 10 pages, check output of first page

View file

@ -1,5 +1,5 @@
import { WorkflowPage, NDV } from '../pages';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { WorkflowPage, NDV } from '../pages';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();

View file

@ -1,7 +1,7 @@
import type { Interception } from 'cypress/types/net-stubbing';
import { META_KEY } from '../constants'; import { META_KEY } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { getPopper } from '../utils'; import { getPopper } from '../utils';
import { Interception } from 'cypress/types/net-stubbing';
const workflowPage = new WorkflowPageClass(); const workflowPage = new WorkflowPageClass();
@ -91,7 +91,7 @@ describe('Canvas Actions', () => {
getPopper().should('be.visible'); getPopper().should('be.visible');
workflowPage.actions.pickColor(2); workflowPage.actions.pickColor();
workflowPage.actions.toggleColorPalette(); workflowPage.actions.toggleColorPalette();
@ -301,15 +301,6 @@ function stickyShouldBePositionedCorrectly(position: Position) {
}); });
} }
function stickyShouldHaveCorrectSize(size: [number, number]) {
const yOffset = 0;
const xOffset = 0;
workflowPage.getters.stickies().should(($el) => {
expect($el).to.have.css('height', `${yOffset + size[0]}px`);
expect($el).to.have.css('width', `${xOffset + size[1]}px`);
});
}
function moveSticky(target: Position) { function moveSticky(target: Position) {
cy.drag('[data-test-id="sticky"]', [target.left, target.top], { abs: true }); cy.drag('[data-test-id="sticky"]', [target.left, target.top], { abs: true });
stickyShouldBePositionedCorrectly(target); stickyShouldBePositionedCorrectly(target);

View file

@ -1,9 +1,9 @@
import { MainSidebar } from './../pages/sidebar/main-sidebar'; import generateOTPToken from 'cypress-otp';
import { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../constants'; import { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../constants';
import { SigninPage } from '../pages'; import { SigninPage } from '../pages';
import { PersonalSettingsPage } from '../pages/settings-personal'; import { PersonalSettingsPage } from '../pages/settings-personal';
import { MfaLoginPage } from '../pages/mfa-login'; import { MfaLoginPage } from '../pages/mfa-login';
import generateOTPToken from 'cypress-otp'; import { MainSidebar } from './../pages/sidebar/main-sidebar';
const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD'; const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD';
@ -36,14 +36,14 @@ const mainSidebar = new MainSidebar();
describe('Two-factor authentication', () => { describe('Two-factor authentication', () => {
beforeEach(() => { beforeEach(() => {
Cypress.session.clearAllSavedSessions(); void Cypress.session.clearAllSavedSessions();
cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, { cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, {
owner: user, owner: user,
members: [], members: [],
admin, admin,
}); });
cy.on('uncaught:exception', (err, runnable) => { cy.on('uncaught:exception', (error) => {
expect(err.message).to.include('Not logged in'); expect(error.message).to.include('Not logged in');
return false; return false;
}); });
cy.intercept('GET', '/rest/mfa/qr').as('getMfaQrCode'); cy.intercept('GET', '/rest/mfa/qr').as('getMfaQrCode');

View file

@ -188,7 +188,7 @@ describe('Editor zoom should work after route changes', () => {
cy.enableFeature('workflowHistory'); cy.enableFeature('workflowHistory');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Lots_of_nodes.json', `Lots of nodes`); cy.createFixtureWorkflow('Lots_of_nodes.json', 'Lots of nodes');
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
}); });

View file

@ -1,17 +1,4 @@
import { import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
AGENT_NODE_NAME,
MANUAL_CHAT_TRIGGER_NODE_NAME,
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
MANUAL_TRIGGER_NODE_NAME,
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
AI_TOOL_CALCULATOR_NODE_NAME,
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
AI_TOOL_CODE_NODE_NAME,
AI_TOOL_WIKIPEDIA_NODE_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
} from './../constants';
import { createMockNodeExecutionData, runMockWorkflowExcution } from '../utils';
import { import {
addLanguageModelNodeToParent, addLanguageModelNodeToParent,
addMemoryNodeToParent, addMemoryNodeToParent,
@ -42,6 +29,19 @@ import {
getManualChatModalLogsTree, getManualChatModalLogsTree,
sendManualChatMessage, sendManualChatMessage,
} from '../composables/modals/chat-modal'; } from '../composables/modals/chat-modal';
import {
AGENT_NODE_NAME,
MANUAL_CHAT_TRIGGER_NODE_NAME,
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
MANUAL_TRIGGER_NODE_NAME,
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
AI_TOOL_CALCULATOR_NODE_NAME,
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
AI_TOOL_CODE_NODE_NAME,
AI_TOOL_WIKIPEDIA_NODE_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
} from './../constants';
describe('Langchain Integration', () => { describe('Langchain Integration', () => {
beforeEach(() => { beforeEach(() => {
@ -149,7 +149,7 @@ describe('Langchain Integration', () => {
const outputMessage = 'Hi there! How can I assist you today?'; const outputMessage = 'Hi there! How can I assist you today?';
clickExecuteNode(); clickExecuteNode();
runMockWorkflowExcution({ runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage), trigger: () => sendManualChatMessage(inputMessage),
runData: [ runData: [
createMockNodeExecutionData(BASIC_LLM_CHAIN_NODE_NAME, { createMockNodeExecutionData(BASIC_LLM_CHAIN_NODE_NAME, {
@ -189,7 +189,7 @@ describe('Langchain Integration', () => {
const outputMessage = 'Hi there! How can I assist you today?'; const outputMessage = 'Hi there! How can I assist you today?';
clickExecuteNode(); clickExecuteNode();
runMockWorkflowExcution({ runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage), trigger: () => sendManualChatMessage(inputMessage),
runData: [ runData: [
createMockNodeExecutionData(AGENT_NODE_NAME, { createMockNodeExecutionData(AGENT_NODE_NAME, {
@ -230,7 +230,7 @@ describe('Langchain Integration', () => {
const inputMessage = 'Hello!'; const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?'; const outputMessage = 'Hi there! How can I assist you today?';
runMockWorkflowExcution({ runMockWorkflowExecution({
trigger: () => { trigger: () => {
sendManualChatMessage(inputMessage); sendManualChatMessage(inputMessage);
}, },

View file

@ -6,7 +6,7 @@ const ndv = new NDV();
describe('Node IO Filter', () => { describe('Node IO Filter', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Node_IO_filter.json', `Node IO filter`); cy.createFixtureWorkflow('Node_IO_filter.json', 'Node IO filter');
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
}); });

View file

@ -1,4 +1,4 @@
import { WorkflowPage } from "../pages"; import { WorkflowPage } from '../pages';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
@ -27,7 +27,7 @@ const VALID_NAMES = [
]; ];
describe('Personal Settings', () => { describe('Personal Settings', () => {
it ('should allow to change first and last name', () => { it('should allow to change first and last name', () => {
cy.visit('/settings/personal'); cy.visit('/settings/personal');
VALID_NAMES.forEach((name) => { VALID_NAMES.forEach((name) => {
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name[0]); cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name[0]);

View file

@ -50,7 +50,7 @@ describe('Template credentials setup', () => {
clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram'); clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');
templateCredentialsSetupPage.getters templateCredentialsSetupPage.getters
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`) .title("Set up 'Promote new Shopify products on Twitter and Telegram' template")
.should('be.visible'); .should('be.visible');
}); });
@ -58,7 +58,7 @@ describe('Template credentials setup', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id); templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters templateCredentialsSetupPage.getters
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`) .title("Set up 'Promote new Shopify products on Twitter and Telegram' template")
.should('be.visible'); .should('be.visible');
templateCredentialsSetupPage.getters templateCredentialsSetupPage.getters

View file

@ -64,7 +64,7 @@ describe('Import workflow', () => {
workflowPage.getters.workflowMenuItemImportFromFile().click(); workflowPage.getters.workflowMenuItemImportFromFile().click();
workflowPage.getters workflowPage.getters
.workflowImportInput() .workflowImportInput()
.selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true }); .selectFile('fixtures/Test_workflow-actions_paste-data.json', { force: true });
cy.waitForLoad(false); cy.waitForLoad(false);
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.getters.canvasNodes().should('have.length', 5); workflowPage.getters.canvasNodes().should('have.length', 5);

View file

@ -1,4 +1,10 @@
import { INSTANCE_ADMIN, INSTANCE_MEMBERS, INSTANCE_OWNER, MANUAL_TRIGGER_NODE_NAME, NOTION_NODE_NAME } from '../constants'; import {
INSTANCE_ADMIN,
INSTANCE_MEMBERS,
INSTANCE_OWNER,
MANUAL_TRIGGER_NODE_NAME,
NOTION_NODE_NAME,
} from '../constants';
import { import {
WorkflowsPage, WorkflowsPage,
WorkflowPage, WorkflowPage,
@ -260,7 +266,9 @@ describe('Projects', () => {
credentialsModal.getters.newCredentialTypeSelect().should('be.visible'); credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click(); credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click(); credentialsModal.getters.newCredentialTypeButton().click();
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890'); credentialsModal.getters
.connectionParameter('Internal Integration Secret')
.type('1234567890');
credentialsModal.actions.setName('Notion account project 1'); credentialsModal.actions.setName('Notion account project 1');
cy.intercept('POST', '/rest/credentials').as('credentialSave'); cy.intercept('POST', '/rest/credentials').as('credentialSave');
@ -283,7 +291,9 @@ describe('Projects', () => {
credentialsModal.getters.newCredentialTypeSelect().should('be.visible'); credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click(); credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click(); credentialsModal.getters.newCredentialTypeButton().click();
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890'); credentialsModal.getters
.connectionParameter('Internal Integration Secret')
.type('1234567890');
credentialsModal.actions.setName('Notion account project 2'); credentialsModal.actions.setName('Notion account project 2');
credentialsModal.actions.save(); credentialsModal.actions.save();
@ -303,12 +313,14 @@ describe('Projects', () => {
credentialsModal.getters.newCredentialTypeSelect().should('be.visible'); credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click(); credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click(); credentialsModal.getters.newCredentialTypeButton().click();
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890'); credentialsModal.getters
.connectionParameter('Internal Integration Secret')
.type('1234567890');
credentialsModal.actions.setName('Notion account personal project'); credentialsModal.actions.setName('Notion account personal project');
cy.intercept('POST', '/rest/credentials').as('credentialSave'); cy.intercept('POST', '/rest/credentials').as('credentialSave');
credentialsModal.actions.save(); credentialsModal.actions.save();
cy.wait('@credentialSave') cy.wait('@credentialSave');
credentialsModal.actions.close(); credentialsModal.actions.close();
// Go to the first project and create a workflow // Go to the first project and create a workflow
@ -318,14 +330,22 @@ describe('Projects', () => {
workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect().find('li').should('have.length', 2).first().should('contain.text', 'Notion account project 1'); getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account project 1');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload(); cy.reload();
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick(); workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect().find('li').should('have.length', 2).first().should('contain.text', 'Notion account project 1'); getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account project 1');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
// Go to the second project and create a workflow // Go to the second project and create a workflow
@ -335,14 +355,22 @@ describe('Projects', () => {
workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect().find('li').should('have.length', 2).first().should('contain.text', 'Notion account project 2'); getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account project 2');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload(); cy.reload();
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick(); workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect().find('li').should('have.length', 2).first().should('contain.text', 'Notion account project 2'); getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account project 2');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
// Go to the Home project and create a workflow // Go to the Home project and create a workflow
@ -356,15 +384,22 @@ describe('Projects', () => {
workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect().find('li').should('have.length', 2).first().should('contain.text', 'Notion account personal project'); getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account personal project');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload(); cy.reload();
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick(); workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect().find('li').should('have.length', 2).first().should('contain.text', 'Notion account personal project'); getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account personal project');
}); });
}); });
}); });

View file

@ -12,7 +12,6 @@ describe('Editors', () => {
}); });
describe('SQL Editor', () => { describe('SQL Editor', () => {
it('should preserve changes when opening-closing Postgres node', () => { it('should preserve changes when opening-closing Postgres node', () => {
workflowPage.actions.addInitialNodeToCanvas('Postgres', { workflowPage.actions.addInitialNodeToCanvas('Postgres', {
action: 'Execute a SQL query', action: 'Execute a SQL query',
@ -26,7 +25,11 @@ describe('Editors', () => {
.type('{esc}'); .type('{esc}');
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.openNode('Postgres'); workflowPage.actions.openNode('Postgres');
ndv.getters.sqlEditorContainer().find('.cm-content').type('{end} LIMIT 10', { delay: TYPING_DELAY }).type('{esc}'); ndv.getters
.sqlEditorContainer()
.find('.cm-content')
.type('{end} LIMIT 10', { delay: TYPING_DELAY })
.type('{esc}');
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.openNode('Postgres'); workflowPage.actions.openNode('Postgres');
ndv.getters.sqlEditorContainer().should('contain', 'SELECT * FROM `testTable` LIMIT 10'); ndv.getters.sqlEditorContainer().should('contain', 'SELECT * FROM `testTable` LIMIT 10');
@ -126,7 +129,11 @@ describe('Editors', () => {
.type('{esc}'); .type('{esc}');
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.openNode('HTML'); workflowPage.actions.openNode('HTML');
ndv.getters.htmlEditorContainer().find('.cm-content').type(`{end}${TEST_ELEMENT_P}`, { delay: TYPING_DELAY, force: true }).type('{esc}'); ndv.getters
.htmlEditorContainer()
.find('.cm-content')
.type(`{end}${TEST_ELEMENT_P}`, { delay: TYPING_DELAY, force: true })
.type('{esc}');
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.openNode('HTML'); workflowPage.actions.openNode('HTML');
ndv.getters.htmlEditorContainer().should('contain', TEST_ELEMENT_H1); ndv.getters.htmlEditorContainer().should('contain', TEST_ELEMENT_H1);

View file

@ -67,7 +67,7 @@ describe('NDV', () => {
}); });
it('should disconect Switch outputs if rules order was changed', () => { it('should disconect Switch outputs if rules order was changed', () => {
cy.createFixtureWorkflow('NDV-test-switch_reorder.json', `NDV test switch reorder`); cy.createFixtureWorkflow('NDV-test-switch_reorder.json', 'NDV test switch reorder');
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
@ -305,7 +305,7 @@ describe('NDV', () => {
it('should display parameter hints correctly', () => { it('should display parameter hints correctly', () => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`); cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow');
workflowPage.actions.openNode('Set1'); workflowPage.actions.openNode('Set1');
ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions
@ -333,7 +333,7 @@ describe('NDV', () => {
} }
ndv.getters.parameterInput('name').click(); // remove focus from input, hide expression preview ndv.getters.parameterInput('name').click(); // remove focus from input, hide expression preview
ndv.actions.validateExpressionPreview('value', output || input); ndv.actions.validateExpressionPreview('value', output ?? input);
ndv.getters.parameterInput('value').clear(); ndv.getters.parameterInput('value').clear();
}); });
}); });
@ -436,7 +436,7 @@ describe('NDV', () => {
} }
it('should traverse floating nodes with mouse', () => { it('should traverse floating nodes with mouse', () => {
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`); cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes');
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist'); getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist'); getFloatingNodeByPosition('outputMain').should('exist');
@ -482,7 +482,7 @@ describe('NDV', () => {
}); });
it('should traverse floating nodes with keyboard', () => { it('should traverse floating nodes with keyboard', () => {
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`); cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes');
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist'); getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist'); getFloatingNodeByPosition('outputMain').should('exist');
@ -597,7 +597,7 @@ describe('NDV', () => {
}); });
it('Should render xml and html tags as strings and can search', () => { it('Should render xml and html tags as strings and can search', () => {
cy.createFixtureWorkflow('Test_workflow_xml_output.json', `test`); cy.createFixtureWorkflow('Test_workflow_xml_output.json', 'test');
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
@ -741,7 +741,7 @@ describe('NDV', () => {
it('should allow selecting item for expressions', () => { it('should allow selecting item for expressions', () => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`); cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow');
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions

View file

@ -338,7 +338,6 @@ describe('Workflow Actions', () => {
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('{enter}'); cy.get('body').type(META_KEY, { delay: 500, release: false }).type('{enter}');
WorkflowPage.getters.successToast().should('not.exist'); WorkflowPage.getters.successToast().should('not.exist');
}); });
}); });
describe('Menu entry Push To Git', () => { describe('Menu entry Push To Git', () => {

View file

@ -8,7 +8,7 @@ describe('Expression editor modal', () => {
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Schedule'); WorkflowPage.actions.addInitialNodeToCanvas('Schedule');
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError'); cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
}); });
describe('Static data', () => { describe('Static data', () => {

28
cypress/package.json Normal file
View file

@ -0,0 +1,28 @@
{
"name": "n8n-cypress",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"cypress:install": "cypress install",
"test:e2e:ui": "scripts/run-e2e.js ui",
"test:e2e:dev": "scripts/run-e2e.js dev",
"test:e2e:all": "scripts/run-e2e.js all",
"format": "prettier --write . --ignore-path ../.prettierignore",
"lint": "eslint . --quiet",
"lintfix": "eslint . --fix",
"start": "cd ..; pnpm start"
},
"devDependencies": {
"@types/uuid": "^8.3.2",
"n8n-workflow": "workspace:*"
},
"dependencies": {
"@ngneat/falso": "^6.4.0",
"cross-env": "^7.0.3",
"cypress": "^13.6.2",
"cypress-otp": "^1.0.3",
"cypress-real-events": "^1.11.0",
"start-server-and-test": "^2.0.3",
"uuid": "8.3.2"
}
}

View file

@ -4,5 +4,6 @@ export class BannerStack extends BasePage {
getters = { getters = {
banner: () => cy.getByTestId('banner-stack'), banner: () => cy.getByTestId('banner-stack'),
}; };
actions = {}; actions = {};
} }

View file

@ -1,6 +1,7 @@
import { IE2ETestPage, IE2ETestPageElement } from '../types'; import type { IE2ETestPage } from '../types';
export class BasePage implements IE2ETestPage { export class BasePage implements IE2ETestPage {
getters: Record<string, IE2ETestPageElement> = {}; getters = {};
actions: Record<string, (...args: any[]) => void> = {};
actions = {};
} }

View file

@ -2,6 +2,7 @@ import { BasePage } from './base';
export class CredentialsPage extends BasePage { export class CredentialsPage extends BasePage {
url = '/home/credentials'; url = '/home/credentials';
getters = { getters = {
emptyListCreateCredentialButton: () => cy.getByTestId('empty-resources-list').find('button'), emptyListCreateCredentialButton: () => cy.getByTestId('empty-resources-list').find('button'),
createCredentialButton: () => cy.getByTestId('resources-list-add'), createCredentialButton: () => cy.getByTestId('resources-list-add'),
@ -23,6 +24,7 @@ export class CredentialsPage extends BasePage {
filtersTrigger: () => cy.getByTestId('resources-list-filters-trigger'), filtersTrigger: () => cy.getByTestId('resources-list-filters-trigger'),
filtersDropdown: () => cy.getByTestId('resources-list-filters-dropdown'), filtersDropdown: () => cy.getByTestId('resources-list-filters-dropdown'),
}; };
actions = { actions = {
search: (searchString: string) => { search: (searchString: string) => {
const searchInput = this.getters.searchInput(); const searchInput = this.getters.searchInput();

View file

@ -7,15 +7,14 @@ export function vistDemoPage(theme?: 'dark' | 'light') {
cy.visit('/workflows/demo' + query); cy.visit('/workflows/demo' + query);
cy.waitForLoad(); cy.waitForLoad();
cy.window().then((win) => { cy.window().then((win) => {
// @ts-ignore
win.preventNodeViewBeforeUnload = true; win.preventNodeViewBeforeUnload = true;
}); });
} }
export function importWorkflow(workflow: object) { export function importWorkflow(workflow: object) {
const OPEN_WORKFLOW = {command: 'openWorkflow', workflow}; const OPEN_WORKFLOW = { command: 'openWorkflow', workflow };
cy.window().then($window => { cy.window().then(($window) => {
const message = JSON.stringify(OPEN_WORKFLOW); const message = JSON.stringify(OPEN_WORKFLOW);
$window.postMessage(message, '*') $window.postMessage(message, '*');
}); });
} }

View file

@ -1,8 +1,8 @@
import { BasePage } from '../base'; import { BasePage } from '../base';
import { INodeTypeDescription } from 'n8n-workflow';
export class NodeCreator extends BasePage { export class NodeCreator extends BasePage {
url = '/workflow/new'; url = '/workflow/new';
getters = { getters = {
plusButton: () => cy.getByTestId('node-creator-plus-button'), plusButton: () => cy.getByTestId('node-creator-plus-button'),
canvasAddButton: () => cy.getByTestId('canvas-add-button'), canvasAddButton: () => cy.getByTestId('canvas-add-button'),
@ -25,6 +25,7 @@ export class NodeCreator extends BasePage {
expandedCategories: () => expandedCategories: () =>
this.getters.creatorItem().find('>div').filter('.active').invoke('text'), this.getters.creatorItem().find('>div').filter('.active').invoke('text'),
}; };
actions = { actions = {
openNodeCreator: () => { openNodeCreator: () => {
this.getters.plusButton().click(); this.getters.plusButton().click();
@ -33,31 +34,5 @@ export class NodeCreator extends BasePage {
selectNode: (displayName: string) => { selectNode: (displayName: string) => {
this.getters.getCreatorItem(displayName).click(); this.getters.getCreatorItem(displayName).click();
}, },
toggleCategory: (category: string) => {
this.getters.getCreatorItem(category).click();
},
categorizeNodes: (nodes: INodeTypeDescription[]) => {
const categorizedNodes = nodes.reduce((acc, node) => {
const categories = (node?.codex?.categories || []).map((category: string) =>
category.trim(),
);
categories.forEach((category: { [key: string]: INodeTypeDescription[] }) => {
// Node creator should show only the latest version of a node
const newerVersion = nodes.find(
(n: INodeTypeDescription) =>
n.name === node.name && (n.version > node.version || Array.isArray(n.version)),
);
if (acc[category] === undefined) {
acc[category] = [];
}
acc[category].push(newerVersion ?? node);
});
return acc;
}, {});
return categorizedNodes;
},
}; };
} }

View file

@ -5,6 +5,7 @@ import { WorkflowsPage } from './workflows';
export class MfaLoginPage extends BasePage { export class MfaLoginPage extends BasePage {
url = '/mfa'; url = '/mfa';
getters = { getters = {
form: () => cy.getByTestId('mfa-login-form'), form: () => cy.getByTestId('mfa-login-form'),
token: () => cy.getByTestId('token'), token: () => cy.getByTestId('token'),

View file

@ -28,6 +28,7 @@ export class CredentialsModal extends BasePage {
usersSelect: () => cy.getByTestId('project-sharing-select').filter(':visible'), usersSelect: () => cy.getByTestId('project-sharing-select').filter(':visible'),
testSuccessTag: () => cy.getByTestId('credentials-config-container-test-success'), testSuccessTag: () => cy.getByTestId('credentials-config-container-test-success'),
}; };
actions = { actions = {
addUser: (email: string) => { addUser: (email: string) => {
this.getters.usersSelect().click(); this.getters.usersSelect().click();
@ -45,7 +46,7 @@ export class CredentialsModal extends BasePage {
if (test) cy.wait('@testCredential'); if (test) cy.wait('@testCredential');
this.getters.saveButton().should('contain.text', 'Saved'); this.getters.saveButton().should('contain.text', 'Saved');
}, },
saveSharing: (test = false) => { saveSharing: () => {
cy.intercept('PUT', '/rest/credentials/*/share').as('shareCredential'); cy.intercept('PUT', '/rest/credentials/*/share').as('shareCredential');
this.getters.saveButton().click({ force: true }); this.getters.saveButton().click({ force: true });
cy.wait('@shareCredential'); cy.wait('@shareCredential');

View file

@ -8,6 +8,7 @@ export class MessageBox extends BasePage {
confirm: () => this.getters.modal().find('.btn--confirm').first(), confirm: () => this.getters.modal().find('.btn--confirm').first(),
cancel: () => this.getters.modal().find('.btn--cancel').first(), cancel: () => this.getters.modal().find('.btn--cancel').first(),
}; };
actions = { actions = {
confirm: () => { confirm: () => {
this.getters.confirm().click({ force: true }); this.getters.confirm().click({ force: true });

View file

@ -7,6 +7,7 @@ export class WorkflowSharingModal extends BasePage {
saveButton: () => cy.getByTestId('workflow-sharing-modal-save-button'), saveButton: () => cy.getByTestId('workflow-sharing-modal-save-button'),
closeButton: () => this.getters.modal().find('.el-dialog__close').first(), closeButton: () => this.getters.modal().find('.el-dialog__close').first(),
}; };
actions = { actions = {
addUser: (email: string) => { addUser: (email: string) => {
this.getters.usersSelect().click(); this.getters.usersSelect().click();

View file

@ -1,5 +1,5 @@
import { BasePage } from './base';
import { getVisiblePopper, getVisibleSelect } from '../utils'; import { getVisiblePopper, getVisibleSelect } from '../utils';
import { BasePage } from './base';
export class NDV extends BasePage { export class NDV extends BasePage {
getters = { getters = {
@ -158,12 +158,9 @@ export class NDV extends BasePage {
this.getters.pinnedDataEditor().click(); this.getters.pinnedDataEditor().click();
this.getters this.getters
.pinnedDataEditor() .pinnedDataEditor()
.type( .type(`{selectall}{backspace}${pinnedData.replace(new RegExp('{', 'g'), '{{}')}`, {
`{selectall}{backspace}${pinnedData.replace(new RegExp('{', 'g'), '{{}')}`, delay: 0,
{ });
delay: 0,
},
);
this.actions.savePinnedData(); this.actions.savePinnedData();
}, },
@ -179,7 +176,7 @@ export class NDV extends BasePage {
this.actions.savePinnedData(); this.actions.savePinnedData();
}, },
clearParameterInput: (parameterName: string) => { clearParameterInput: (parameterName: string) => {
this.getters.parameterInput(parameterName).type(`{selectall}{backspace}`); this.getters.parameterInput(parameterName).type('{selectall}{backspace}');
}, },
typeIntoParameterInput: ( typeIntoParameterInput: (
parameterName: string, parameterName: string,
@ -188,7 +185,7 @@ export class NDV extends BasePage {
) => { ) => {
this.getters.parameterInput(parameterName).type(content, opts); this.getters.parameterInput(parameterName).type(content, opts);
}, },
selectOptionInParameterDropdown: (parameterName: string, content: string) => { selectOptionInParameterDropdown: (_: string, content: string) => {
getVisibleSelect().find('.option-headline').contains(content).click(); getVisibleSelect().find('.option-headline').contains(content).click();
}, },
rename: (newName: string) => { rename: (newName: string) => {
@ -286,7 +283,7 @@ export class NDV extends BasePage {
parseSpecialCharSequences: false, parseSpecialCharSequences: false,
delay, delay,
}); });
this.actions.validateExpressionPreview(fieldName, `node doesn't exist`); this.actions.validateExpressionPreview(fieldName, "node doesn't exist");
}, },
openSettings: () => { openSettings: () => {
this.getters.nodeSettingsTab().click(); this.getters.nodeSettingsTab().click();

View file

@ -1,8 +1,9 @@
import { BasePage } from './base';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import { BasePage } from './base';
export class SettingsLogStreamingPage extends BasePage { export class SettingsLogStreamingPage extends BasePage {
url = '/settings/log-streaming'; url = '/settings/log-streaming';
getters = { getters = {
getActionBoxUnlicensed: () => cy.getByTestId('action-box-unlicensed'), getActionBoxUnlicensed: () => cy.getByTestId('action-box-unlicensed'),
getActionBoxLicensed: () => cy.getByTestId('action-box-licensed'), getActionBoxLicensed: () => cy.getByTestId('action-box-licensed'),
@ -17,6 +18,7 @@ export class SettingsLogStreamingPage extends BasePage {
getDestinationDeleteButton: () => cy.getByTestId('destination-delete-button'), getDestinationDeleteButton: () => cy.getByTestId('destination-delete-button'),
getDestinationCards: () => cy.getByTestId('destination-card'), getDestinationCards: () => cy.getByTestId('destination-card'),
}; };
actions = { actions = {
clickContactUs: () => this.getters.getContactUsButton().click(), clickContactUs: () => this.getters.getContactUsButton().click(),
clickAddFirstDestination: () => this.getters.getAddFirstDestinationButton().click(), clickAddFirstDestination: () => this.getters.getAddFirstDestinationButton().click(),

View file

@ -1,13 +1,14 @@
import generateOTPToken from 'cypress-otp';
import { ChangePasswordModal } from './modals/change-password-modal'; import { ChangePasswordModal } from './modals/change-password-modal';
import { MfaSetupModal } from './modals/mfa-setup-modal'; import { MfaSetupModal } from './modals/mfa-setup-modal';
import { BasePage } from './base'; import { BasePage } from './base';
import generateOTPToken from 'cypress-otp';
const changePasswordModal = new ChangePasswordModal(); const changePasswordModal = new ChangePasswordModal();
const mfaSetupModal = new MfaSetupModal(); const mfaSetupModal = new MfaSetupModal();
export class PersonalSettingsPage extends BasePage { export class PersonalSettingsPage extends BasePage {
url = '/settings/personal'; url = '/settings/personal';
secret = ''; secret = '';
getters = { getters = {
@ -23,6 +24,7 @@ export class PersonalSettingsPage extends BasePage {
themeSelector: () => cy.getByTestId('theme-select'), themeSelector: () => cy.getByTestId('theme-select'),
selectOptionsVisible: () => cy.get('.el-select-dropdown:visible .el-select-dropdown__item'), selectOptionsVisible: () => cy.get('.el-select-dropdown:visible .el-select-dropdown__item'),
}; };
actions = { actions = {
changeTheme: (theme: 'System default' | 'Dark' | 'Light') => { changeTheme: (theme: 'System default' | 'Dark' | 'Light') => {
this.getters.themeSelector().click(); this.getters.themeSelector().click();

View file

@ -2,6 +2,8 @@ import { BasePage } from './base';
export class SettingsUsagePage extends BasePage { export class SettingsUsagePage extends BasePage {
url = '/settings/usage'; url = '/settings/usage';
getters = {}; getters = {};
actions = {}; actions = {};
} }

View file

@ -11,6 +11,7 @@ const settingsSidebar = new SettingsSidebar();
export class SettingsUsersPage extends BasePage { export class SettingsUsersPage extends BasePage {
url = '/settings/users'; url = '/settings/users';
getters = { getters = {
setUpOwnerButton: () => cy.getByTestId('action-box').find('button').first(), setUpOwnerButton: () => cy.getByTestId('action-box').find('button').first(),
inviteButton: () => cy.getByTestId('settings-users-invite-button').last(), inviteButton: () => cy.getByTestId('settings-users-invite-button').last(),
@ -34,6 +35,7 @@ export class SettingsUsersPage extends BasePage {
deleteUserButton: () => this.getters.confirmDeleteModal().find('button:contains("Delete")'), deleteUserButton: () => this.getters.confirmDeleteModal().find('button:contains("Delete")'),
deleteDataInput: () => cy.getByTestId('delete-data-input').find('input').first(), deleteDataInput: () => cy.getByTestId('delete-data-input').find('input').first(),
}; };
actions = { actions = {
goToOwnerSetup: () => this.getters.setUpOwnerButton().click(), goToOwnerSetup: () => this.getters.setUpOwnerButton().click(),
loginAndVisit: (email: string, password: string, isOwner: boolean) => { loginAndVisit: (email: string, password: string, isOwner: boolean) => {

View file

@ -2,8 +2,10 @@ import { BasePage } from './base';
export class SettingsPage extends BasePage { export class SettingsPage extends BasePage {
url = '/settings'; url = '/settings';
getters = { getters = {
menuItems: () => cy.getByTestId('menu-item'), menuItems: () => cy.getByTestId('menu-item'),
}; };
actions = {}; actions = {};
} }

View file

@ -1,8 +1,6 @@
import { BasePage } from '../base'; import { BasePage } from '../base';
import { WorkflowsPage } from '../workflows'; import { WorkflowsPage } from '../workflows';
const workflowsPage = new WorkflowsPage();
export class MainSidebar extends BasePage { export class MainSidebar extends BasePage {
getters = { getters = {
menuItem: (id: string) => cy.getByTestId('menu-item').get('#' + id), menuItem: (id: string) => cy.getByTestId('menu-item').get('#' + id),
@ -16,6 +14,7 @@ export class MainSidebar extends BasePage {
userMenu: () => cy.get('div[class="action-dropdown-container"]'), userMenu: () => cy.get('div[class="action-dropdown-container"]'),
logo: () => cy.getByTestId('n8n-logo'), logo: () => cy.getByTestId('n8n-logo'),
}; };
actions = { actions = {
goToSettings: () => { goToSettings: () => {
this.getters.settings().should('be.visible'); this.getters.settings().should('be.visible');

View file

@ -6,6 +6,7 @@ export class SettingsSidebar extends BasePage {
users: () => this.getters.menuItem('settings-users'), users: () => this.getters.menuItem('settings-users'),
back: () => cy.getByTestId('settings-back'), back: () => cy.getByTestId('settings-back'),
}; };
actions = { actions = {
goToUsers: () => { goToUsers: () => {
this.getters.users().should('be.visible'); this.getters.users().should('be.visible');

View file

@ -4,6 +4,7 @@ import { WorkflowsPage } from './workflows';
export class SigninPage extends BasePage { export class SigninPage extends BasePage {
url = '/signin'; url = '/signin';
getters = { getters = {
form: () => cy.getByTestId('auth-form'), form: () => cy.getByTestId('auth-form'),
email: () => cy.getByTestId('email'), email: () => cy.getByTestId('email'),

View file

@ -1,6 +1,6 @@
import { CredentialsModal, MessageBox } from './modals';
import * as formStep from '../composables/setup-template-form-step'; import * as formStep from '../composables/setup-template-form-step';
import { overrideFeatureFlag } from '../composables/featureFlags'; import { overrideFeatureFlag } from '../composables/featureFlags';
import { CredentialsModal, MessageBox } from './modals';
export type TemplateTestData = { export type TemplateTestData = {
id: number; id: number;

View file

@ -17,15 +17,18 @@ export class TemplateWorkflowPage extends BasePage {
this.getters.useTemplateButton().click(); this.getters.useTemplateButton().click();
}, },
openTemplate: (template: { openTemplate: (
workflow: { template: {
id: number; workflow: {
name: string; id: number;
description: string; name: string;
user: { username: string }; description: string;
image: { id: number; url: string }[]; user: { username: string };
}; image: Array<{ id: number; url: string }>;
}, templateHost: string) => { };
},
templateHost: string,
) => {
cy.intercept('GET', `${templateHost}/api/templates/workflows/${template.workflow.id}`, { cy.intercept('GET', `${templateHost}/api/templates/workflows/${template.workflow.id}`, {
statusCode: 200, statusCode: 200,
body: template, body: template,

View file

@ -3,6 +3,7 @@ import Chainable = Cypress.Chainable;
export class VariablesPage extends BasePage { export class VariablesPage extends BasePage {
url = '/variables'; url = '/variables';
getters = { getters = {
unavailableResourcesList: () => cy.getByTestId('unavailable-resources-list'), unavailableResourcesList: () => cy.getByTestId('unavailable-resources-list'),
emptyResourcesList: () => cy.getByTestId('empty-resources-list'), emptyResourcesList: () => cy.getByTestId('empty-resources-list'),
@ -14,7 +15,7 @@ export class VariablesPage extends BasePage {
createVariableButton: () => cy.getByTestId('resources-list-add'), createVariableButton: () => cy.getByTestId('resources-list-add'),
variablesRows: () => cy.getByTestId('variables-row'), variablesRows: () => cy.getByTestId('variables-row'),
variablesEditableRows: () => variablesEditableRows: () =>
cy.getByTestId('variables-row').filter((index, row) => !!row.querySelector('input')), cy.getByTestId('variables-row').filter((_, row) => !!row.querySelector('input')),
variableRow: (key: string) => variableRow: (key: string) =>
this.getters.variablesRows().contains(key).parents('[data-test-id="variables-row"]'), this.getters.variablesRows().contains(key).parents('[data-test-id="variables-row"]'),
editableRowCancelButton: (row: Chainable<JQuery<HTMLElement>>) => editableRowCancelButton: (row: Chainable<JQuery<HTMLElement>>) =>

View file

@ -2,6 +2,7 @@ import { BasePage } from './base';
export class WorkerViewPage extends BasePage { export class WorkerViewPage extends BasePage {
url = '/settings/workers'; url = '/settings/workers';
getters = { getters = {
workerCards: () => cy.getByTestId('worker-card'), workerCards: () => cy.getByTestId('worker-card'),
workerCard: (workerId: string) => this.getters.workerCards().contains(workerId), workerCard: (workerId: string) => this.getters.workerCards().contains(workerId),

View file

@ -26,6 +26,7 @@ export class WorkflowExecutionsTab extends BasePage {
executionDebugButton: () => cy.getByTestId('execution-debug-button'), executionDebugButton: () => cy.getByTestId('execution-debug-button'),
workflowExecutionPreviewIframe: () => cy.getByTestId('workflow-preview-iframe'), workflowExecutionPreviewIframe: () => cy.getByTestId('workflow-preview-iframe'),
}; };
actions = { actions = {
toggleNodeEnabled: (nodeName: string) => { toggleNodeEnabled: (nodeName: string) => {
workflowPage.getters.canvasNodeByName(nodeName).click(); workflowPage.getters.canvasNodeByName(nodeName).click();

View file

@ -1,7 +1,7 @@
import { BasePage } from "./base"; import { BasePage } from './base';
export class WorkflowHistoryPage extends BasePage { export class WorkflowHistoryPage extends BasePage {
getters = { getters = {
workflowHistoryCloseButton: () => cy.getByTestId('workflow-history-close-button'), workflowHistoryCloseButton: () => cy.getByTestId('workflow-history-close-button'),
} };
} }

View file

@ -1,6 +1,6 @@
import { META_KEY } from '../constants'; import { META_KEY } from '../constants';
import { BasePage } from './base';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import { BasePage } from './base';
import { NodeCreator } from './features/node-creator'; import { NodeCreator } from './features/node-creator';
type CyGetOptions = Parameters<(typeof cy)['get']>[1]; type CyGetOptions = Parameters<(typeof cy)['get']>[1];
@ -8,6 +8,7 @@ type CyGetOptions = Parameters<(typeof cy)['get']>[1];
const nodeCreator = new NodeCreator(); const nodeCreator = new NodeCreator();
export class WorkflowPage extends BasePage { export class WorkflowPage extends BasePage {
url = '/workflow/new'; url = '/workflow/new';
getters = { getters = {
workflowNameInputContainer: () => cy.getByTestId('workflow-name-input', { timeout: 5000 }), workflowNameInputContainer: () => cy.getByTestId('workflow-name-input', { timeout: 5000 }),
workflowNameInput: () => workflowNameInput: () =>
@ -134,12 +135,12 @@ export class WorkflowPage extends BasePage {
colors: () => cy.getByTestId('color'), colors: () => cy.getByTestId('color'),
contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`), contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`),
}; };
actions = { actions = {
visit: (preventNodeViewUnload = true) => { visit: (preventNodeViewUnload = true) => {
cy.visit(this.url); cy.visit(this.url);
cy.waitForLoad(); cy.waitForLoad();
cy.window().then((win) => { cy.window().then((win) => {
// @ts-ignore
win.preventNodeViewBeforeUnload = preventNodeViewUnload; win.preventNodeViewBeforeUnload = preventNodeViewUnload;
}); });
}, },
@ -329,15 +330,17 @@ export class WorkflowPage extends BasePage {
cy.getByTestId('zoom-to-fit').click(); cy.getByTestId('zoom-to-fit').click();
}, },
pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => { pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => {
// Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling) cy.window().then((win) => {
this.getters.nodeViewBackground().trigger('wheel', { // Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling)
force: true, this.getters.nodeViewBackground().trigger('wheel', {
bubbles: true, force: true,
ctrlKey: true, bubbles: true,
pageX: cy.window().innerWidth / 2, ctrlKey: true,
pageY: cy.window().innerHeight / 2, pageX: win.innerWidth / 2,
deltaMode: 1, pageY: win.innerHeight / 2,
deltaY: mode === 'zoomOut' ? steps : -steps, deltaMode: 1,
deltaY: mode === 'zoomOut' ? steps : -steps,
});
}); });
}, },
hitUndo: () => { hitUndo: () => {
@ -388,11 +391,7 @@ export class WorkflowPage extends BasePage {
this.actions.addNodeToCanvas(newNodeName, false, false, action); this.actions.addNodeToCanvas(newNodeName, false, false, action);
}, },
deleteNodeBetweenNodes: ( deleteNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string) => {
sourceNodeName: string,
targetNodeName: string,
newNodeName: string,
) => {
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover(); this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
this.getters this.getters
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName) .getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
@ -415,7 +414,7 @@ export class WorkflowPage extends BasePage {
.find('[data-test-id="change-sticky-color"]') .find('[data-test-id="change-sticky-color"]')
.click({ force: true }); .click({ force: true });
}, },
pickColor: (index: number) => { pickColor: () => {
this.getters.colors().eq(1).click(); this.getters.colors().eq(1).click();
}, },
editSticky: (content: string) => { editSticky: (content: string) => {

View file

@ -2,6 +2,7 @@ import { BasePage } from './base';
export class WorkflowsPage extends BasePage { export class WorkflowsPage extends BasePage {
url = '/home/workflows'; url = '/home/workflows';
getters = { getters = {
newWorkflowButtonCard: () => cy.getByTestId('new-workflow-card'), newWorkflowButtonCard: () => cy.getByTestId('new-workflow-card'),
newWorkflowTemplateCard: () => cy.getByTestId('new-workflow-template-card'), newWorkflowTemplateCard: () => cy.getByTestId('new-workflow-template-card'),

View file

@ -16,9 +16,7 @@ Cypress.Commands.add('createFixtureWorkflow', (fixtureKey, workflowName) => {
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
// We need to force the click because the input is hidden // We need to force the click because the input is hidden
workflowPage.getters workflowPage.getters.workflowImportInput().selectFile(`fixtures/${fixtureKey}`, { force: true });
.workflowImportInput()
.selectFile(`cypress/fixtures/${fixtureKey}`, { force: true });
cy.waitForLoad(false); cy.waitForLoad(false);
workflowPage.actions.setWorkflowName(workflowName); workflowPage.actions.setWorkflowName(workflowName);
@ -46,7 +44,7 @@ Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => {
}); });
Cypress.Commands.add('signin', ({ email, password }) => { Cypress.Commands.add('signin', ({ email, password }) => {
Cypress.session.clearAllSavedSessions(); void Cypress.session.clearAllSavedSessions();
cy.session([email, password], () => cy.session([email, password], () =>
cy.request({ cy.request({
method: 'POST', method: 'POST',
@ -128,7 +126,7 @@ Cypress.Commands.add('paste', { prevSubject: true }, (selector, pastePayload) =>
}); });
Cypress.Commands.add('drag', (selector, pos, options) => { Cypress.Commands.add('drag', (selector, pos, options) => {
const index = options?.index || 0; const index = options?.index ?? 0;
const [xDiff, yDiff] = pos; const [xDiff, yDiff] = pos;
const element = typeof selector === 'string' ? cy.get(selector).eq(index) : selector; const element = typeof selector === 'string' ? cy.get(selector).eq(index) : selector;
element.should('exist'); element.should('exist');

View file

@ -4,8 +4,8 @@ import './commands';
before(() => { before(() => {
cy.resetDatabase(); cy.resetDatabase();
Cypress.on('uncaught:exception', (err) => { Cypress.on('uncaught:exception', (error) => {
return !err.message.includes('ResizeObserver'); return !error.message.includes('ResizeObserver');
}); });
}); });

View file

@ -1,7 +1,7 @@
// Load type definitions that come with Cypress module // Load type definitions that come with Cypress module
/// <reference types="cypress" /> /// <reference types="cypress" />
import { Interception } from 'cypress/types/net-stubbing'; import type { Interception } from 'cypress/types/net-stubbing';
interface SigninPayload { interface SigninPayload {
email: string; email: string;
@ -18,7 +18,7 @@ declare global {
config(key: keyof SuiteConfigOverrides): boolean; config(key: keyof SuiteConfigOverrides): boolean;
getByTestId( getByTestId(
selector: string, selector: string,
...args: (Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined)[] ...args: Array<Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined>
): Chainable<JQuery<HTMLElement>>; ): Chainable<JQuery<HTMLElement>>;
findChildByTestId(childTestId: string): Chainable<JQuery<HTMLElement>>; findChildByTestId(childTestId: string): Chainable<JQuery<HTMLElement>>;
createFixtureWorkflow(fixtureKey: string, workflowName: string): void; createFixtureWorkflow(fixtureKey: string, workflowName: string): void;
@ -36,7 +36,7 @@ declare global {
readClipboard(): Chainable<string>; readClipboard(): Chainable<string>;
paste(pastePayload: string): void; paste(pastePayload: string): void;
drag( drag(
selector: string | Cypress.Chainable<JQuery<HTMLElement>>, selector: string | Chainable<JQuery<HTMLElement>>,
target: [number, number], target: [number, number],
options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean }, options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean },
): void; ): void;
@ -45,8 +45,11 @@ declare global {
shouldNotHaveConsoleErrors(): void; shouldNotHaveConsoleErrors(): void;
window(): Chainable< window(): Chainable<
AUTWindow & { AUTWindow & {
innerWidth: number;
innerHeight: number;
preventNodeViewBeforeUnload?: boolean;
featureFlags: { featureFlags: {
override: (feature: string, value: any) => void; override: (feature: string, value: unknown) => void;
}; };
} }
>; >;

View file

@ -7,5 +7,6 @@
"types": ["cypress", "node"] "types": ["cypress", "node"]
}, },
"include": ["**/*.ts"], "include": ["**/*.ts"],
"exclude": ["**/dist/**/*", "**/node_modules/**/*"] "exclude": ["**/dist/**/*", "**/node_modules/**/*"],
"references": [{ "path": "../packages/workflow/tsconfig.build.json" }]
} }

View file

@ -1,12 +1,24 @@
export type IE2ETestPageElement = ( export type IE2ETestPageElement = (
...args: any[] ...args: unknown[]
) => ) =>
| Cypress.Chainable<JQuery<HTMLElement>> | Cypress.Chainable<JQuery<HTMLElement>>
| Cypress.Chainable<JQuery<HTMLInputElement>> | Cypress.Chainable<JQuery<HTMLInputElement>>
| Cypress.Chainable<JQuery<HTMLButtonElement>>; | Cypress.Chainable<JQuery<HTMLButtonElement>>;
type Getter = IE2ETestPageElement | ((key: string | number) => IE2ETestPageElement);
export interface IE2ETestPage { export interface IE2ETestPage {
url?: string; url?: string;
getters: Record<string, IE2ETestPageElement>; getters: Record<string, Getter>;
actions: Record<string, (...args: any[]) => void>; actions: Record<string, (...args: unknown[]) => void>;
}
interface Execution {
workflowId: string;
}
export interface ExecutionResponse {
data: {
results: Execution[];
};
} }

View file

@ -1,5 +1,4 @@
import { ITaskData } from '../../packages/workflow/src'; import type { IDataObject, IPinData, ITaskData, ITaskDataConnections } from 'n8n-workflow';
import { IPinData } from '../../packages/workflow';
import { clickExecuteWorkflowButton } from '../composables/workflow'; import { clickExecuteWorkflowButton } from '../composables/workflow';
export function createMockNodeExecutionData( export function createMockNodeExecutionData(
@ -10,7 +9,7 @@ export function createMockNodeExecutionData(
executionStatus = 'success', executionStatus = 'success',
jsonData, jsonData,
...rest ...rest
}: Partial<ITaskData> & { jsonData?: Record<string, object> }, }: Partial<ITaskData> & { jsonData?: Record<string, IDataObject> },
): Record<string, ITaskData> { ): Record<string, ITaskData> {
return { return {
[name]: { [name]: {
@ -29,7 +28,7 @@ export function createMockNodeExecutionData(
]; ];
return acc; return acc;
}, {}) }, {} as ITaskDataConnections)
: data, : data,
source: [null], source: [null],
...rest, ...rest,
@ -75,7 +74,7 @@ export function createMockWorkflowExecutionData({
}; };
} }
export function runMockWorkflowExcution({ export function runMockWorkflowExecution({
trigger, trigger,
lastNodeExecuted, lastNodeExecuted,
runData, runData,
@ -105,7 +104,7 @@ export function runMockWorkflowExcution({
cy.wait('@runWorkflow'); cy.wait('@runWorkflow');
const resolvedRunData = {}; const resolvedRunData: Record<string, ITaskData> = {};
runData.forEach((nodeExecution) => { runData.forEach((nodeExecution) => {
const nodeName = Object.keys(nodeExecution)[0]; const nodeName = Object.keys(nodeExecution)[0];
const nodeRunData = nodeExecution[nodeName]; const nodeRunData = nodeExecution[nodeName];

View file

@ -31,23 +31,13 @@
"test:frontend": "pnpm --filter=@n8n/chat --filter=@n8n/codemirror-lang --filter=n8n-design-system --filter=n8n-editor-ui test", "test:frontend": "pnpm --filter=@n8n/chat --filter=@n8n/codemirror-lang --filter=n8n-design-system --filter=n8n-editor-ui test",
"watch": "turbo run watch --parallel", "watch": "turbo run watch --parallel",
"webhook": "./packages/cli/bin/n8n webhook", "webhook": "./packages/cli/bin/n8n webhook",
"worker": "./packages/cli/bin/n8n worker", "worker": "./packages/cli/bin/n8n worker"
"cypress:install": "cypress install",
"cypress:open": "CYPRESS_BASE_URL=http://localhost:8080 cypress open",
"test:e2e:ui": "scripts/run-e2e.js ui",
"test:e2e:dev": "scripts/run-e2e.js dev",
"test:e2e:all": "scripts/run-e2e.js all"
}, },
"devDependencies": { "devDependencies": {
"@n8n_io/eslint-config": "workspace:*", "@n8n_io/eslint-config": "workspace:*",
"@ngneat/falso": "^6.4.0",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^1.6.0", "@vitest/coverage-v8": "^1.6.0",
"cross-env": "^7.0.3",
"cypress": "^13.6.2",
"cypress-otp": "^1.0.3",
"cypress-real-events": "^1.11.0",
"jest": "^29.6.2", "jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2", "jest-environment-jsdom": "^29.6.2",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
@ -58,7 +48,6 @@
"p-limit": "^3.1.0", "p-limit": "^3.1.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"run-script-os": "^1.0.7", "run-script-os": "^1.0.7",
"start-server-and-test": "^2.0.3",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"tsc-alias": "^1.8.7", "tsc-alias": "^1.8.7",

View file

@ -50,9 +50,6 @@ importers:
'@n8n_io/eslint-config': '@n8n_io/eslint-config':
specifier: workspace:* specifier: workspace:*
version: link:packages/@n8n_io/eslint-config version: link:packages/@n8n_io/eslint-config
'@ngneat/falso':
specifier: ^6.4.0
version: 6.4.0
'@types/jest': '@types/jest':
specifier: ^29.5.3 specifier: ^29.5.3
version: 29.5.3 version: 29.5.3
@ -62,18 +59,6 @@ importers:
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.6.0(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) version: 1.6.0(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))
cross-env:
specifier: ^7.0.3
version: 7.0.3
cypress:
specifier: ^13.6.2
version: 13.6.2
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)
jest: jest:
specifier: ^29.6.2 specifier: ^29.6.2
version: 29.6.2(@types/node@18.16.16) version: 29.6.2(@types/node@18.16.16)
@ -104,9 +89,6 @@ importers:
run-script-os: run-script-os:
specifier: ^1.0.7 specifier: ^1.0.7
version: 1.1.6 version: 1.1.6
start-server-and-test:
specifier: ^2.0.3
version: 2.0.3
supertest: supertest:
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.0.0 version: 7.0.0
@ -138,6 +120,37 @@ importers:
specifier: ^2.0.19 specifier: ^2.0.19
version: 2.0.19(typescript@5.4.2) version: 2.0.19(typescript@5.4.2)
cypress:
dependencies:
'@ngneat/falso':
specifier: ^6.4.0
version: 6.4.0
cross-env:
specifier: ^7.0.3
version: 7.0.3
cypress:
specifier: ^13.6.2
version: 13.6.2
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)
start-server-and-test:
specifier: ^2.0.3
version: 2.0.3
uuid:
specifier: 8.3.2
version: 8.3.2
devDependencies:
'@types/uuid':
specifier: ^8.3.2
version: 8.3.4
n8n-workflow:
specifier: workspace:*
version: link:../packages/workflow
packages/@n8n/chat: packages/@n8n/chat:
dependencies: dependencies:
highlight.js: highlight.js:
@ -5733,9 +5746,6 @@ packages:
'@types/uuid@8.3.4': '@types/uuid@8.3.4':
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
'@types/uuid@9.0.0':
resolution: {integrity: sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==}
'@types/uuid@9.0.7': '@types/uuid@9.0.7':
resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==} resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==}
@ -13156,10 +13166,6 @@ packages:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true hasBin: true
uuid@9.0.0:
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
hasBin: true
uuid@9.0.1: uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true hasBin: true
@ -13341,6 +13347,9 @@ packages:
vue-component-type-helpers@2.0.19: vue-component-type-helpers@2.0.19:
resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==} resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==}
vue-component-type-helpers@2.0.21:
resolution: {integrity: sha512-3NaicyZ7N4B6cft4bfb7dOnPbE9CjLcx+6wZWAg5zwszfO4qXRh+U52dN5r5ZZfc6iMaxKCEcoH9CmxxoFZHLg==}
vue-demi@0.14.5: vue-demi@0.14.5:
resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -18969,7 +18978,7 @@ snapshots:
ts-dedent: 2.2.0 ts-dedent: 2.2.0
type-fest: 2.19.0 type-fest: 2.19.0
vue: 3.4.21(typescript@5.4.2) vue: 3.4.21(typescript@5.4.2)
vue-component-type-helpers: 2.0.19 vue-component-type-helpers: 2.0.21
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
- prettier - prettier
@ -19573,8 +19582,6 @@ snapshots:
'@types/uuid@8.3.4': {} '@types/uuid@8.3.4': {}
'@types/uuid@9.0.0': {}
'@types/uuid@9.0.7': {} '@types/uuid@9.0.7': {}
'@types/validator@13.7.10': {} '@types/validator@13.7.10': {}
@ -21459,7 +21466,7 @@ snapshots:
cli-table3: 0.6.3 cli-table3: 0.6.3
commander: 6.2.1 commander: 6.2.1
common-tags: 1.8.2 common-tags: 1.8.2
dayjs: 1.11.6 dayjs: 1.11.10
debug: 4.3.4(supports-color@8.1.1) debug: 4.3.4(supports-color@8.1.1)
enquirer: 2.3.6 enquirer: 2.3.6
eventemitter2: 6.4.7 eventemitter2: 6.4.7
@ -24542,11 +24549,11 @@ snapshots:
dependencies: dependencies:
'@types/asn1': 0.2.0 '@types/asn1': 0.2.0
'@types/node': 18.16.16 '@types/node': 18.16.16
'@types/uuid': 9.0.0 '@types/uuid': 9.0.7
asn1: 0.2.6 asn1: 0.2.6
debug: 4.3.4(supports-color@8.1.1) debug: 4.3.4(supports-color@8.1.1)
strict-event-emitter-types: 2.0.0 strict-event-emitter-types: 2.0.0
uuid: 9.0.0 uuid: 9.0.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -28334,8 +28341,6 @@ snapshots:
uuid@8.3.2: {} uuid@8.3.2: {}
uuid@9.0.0: {}
uuid@9.0.1: {} uuid@9.0.1: {}
v3-infinite-loading@1.2.2: {} v3-infinite-loading@1.2.2: {}
@ -28515,6 +28520,8 @@ snapshots:
vue-component-type-helpers@2.0.19: {} vue-component-type-helpers@2.0.19: {}
vue-component-type-helpers@2.0.21: {}
vue-demi@0.14.5(vue@3.4.21(typescript@5.4.2)): vue-demi@0.14.5(vue@3.4.21(typescript@5.4.2)):
dependencies: dependencies:
vue: 3.4.21(typescript@5.4.2) vue: 3.4.21(typescript@5.4.2)

View file

@ -2,3 +2,4 @@ packages:
- packages/* - packages/*
- packages/@n8n/* - packages/@n8n/*
- packages/@n8n_io/* - packages/@n8n_io/*
- cypress