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

3
.gitignore vendored
View file

@ -18,9 +18,6 @@ nodelinter.config.json
packages/**/.turbo
.turbo
*.tsbuildinfo
cypress/videos/*
cypress/screenshots/*
cypress/downloads/*
*.swp
CHANGELOG-*.md
*.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
export const interceptCtaRequestWithResponse = (becomeCreator: boolean) => {
return cy.intercept('GET', `/rest/cta/become-creator`, {
return cy.intercept('GET', '/rest/cta/become-creator', {
body: becomeCreator,
});
};

View file

@ -42,7 +42,7 @@ export function closeCredentialModal() {
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]) => {
setCredentialConnectionParameterInputByName(key, value);
});

View file

@ -2,7 +2,7 @@
* Getters
*/
import { getVisibleSelect } from "../utils";
import { getVisibleSelect } from '../utils';
export function getCredentialSelect(eq = 0) {
return cy.getByTestId('node-credentials-select').eq(eq);
@ -75,7 +75,7 @@ export function setParameterInputByName(name: string, value: 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) {

View file

@ -2,4 +2,4 @@
* 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) {
const target = getNodeByName(name);
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) {

View file

@ -18,6 +18,12 @@ module.exports = defineConfig({
screenshotOnRunFailure: true,
experimentalInteractiveRunEvents: true,
experimentalSessionAndOrigin: true,
specPattern: 'e2e/**/*.ts',
supportFile: 'support/e2e.ts',
fixturesFolder: 'fixtures',
downloadsFolder: 'downloads',
screenshotsFolder: 'screenshots',
videosFolder: 'videos',
},
env: {
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 { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { v4 as uuid } from 'uuid';
const WorkflowsPage = new WorkflowsPageClass();
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 { SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import {
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 { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
import { NDV } from '../pages/ndv';
@ -338,8 +342,8 @@ describe('Undo/Redo', () => {
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.css', 'left', `637px`)
.should('have.css', 'top', `501px`);
.should('have.css', 'left', '637px')
.should('have.css', 'top', '501px');
cy.fixture('Test_workflow_form_switch.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
@ -353,8 +357,8 @@ describe('Undo/Redo', () => {
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.css', 'left', `637px`)
.should('have.css', 'top', `501px`);
.should('have.css', 'left', '637px')
.should('have.css', 'top', '501px');
});
it('should not undo/redo when NDV or a modal is open', () => {

View file

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

View file

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

View file

@ -1,3 +1,5 @@
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV, WorkflowExecutionsTab } from '../pages';
import {
MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
@ -7,8 +9,6 @@ import {
SWITCH_NODE_NAME,
MERGE_NODE_NAME,
} from './../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV, WorkflowExecutionsTab } from '../pages';
const WorkflowPage = new WorkflowPageClass();
const ExecutionsTab = new WorkflowExecutionsTab();
@ -258,7 +258,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
// 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.getters

View file

@ -136,7 +136,7 @@ describe('Data pinning', () => {
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
@ -151,10 +151,8 @@ describe('Data pinning', () => {
ndv.getters.pinDataButton().should('not.exist');
ndv.getters.editPinnedDataButton().should('be.visible');
ndv.actions.setPinnedData('[ { "name": "First item", "code": 2dsa }]')
workflowPage.getters
.errorToast()
.should('contain', 'Unable to save due to invalid JSON');
ndv.actions.setPinnedData('[ { "name": "First item", "code": 2dsa }]');
workflowPage.getters.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', () => {
@ -168,6 +166,7 @@ describe('Data pinning', () => {
ndv.actions.close();
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`);
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 { getVisibleSelect } from '../utils';
const wf = new WorkflowPage();
const ndv = new NDV();

View file

@ -1,10 +1,10 @@
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
import {
MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
SCHEDULE_TRIGGER_NODE_NAME,
} from './../constants';
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
@ -170,7 +170,7 @@ describe('Data mapping', () => {
});
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.openNode('Set1');

View file

@ -1,12 +1,13 @@
import { WorkflowPage, WorkflowsPage, NDV } from '../pages';
import { BACKEND_BASE_URL } from '../constants';
import { getVisibleSelect } from '../utils';
import type { ExecutionResponse } from '../types';
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('Schedule Trigger node', async () => {
describe('Schedule Trigger node', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
@ -37,30 +38,34 @@ describe('Schedule Trigger node', async () => {
const workflowId = url.split('/').pop();
cy.wait(1200);
cy.request('GET', `${BACKEND_BASE_URL}/rest/executions`).then((response) => {
expect(response.status).to.eq(200);
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) => {
cy.request<ExecutionResponse>('GET', `${BACKEND_BASE_URL}/rest/executions`).then(
(response) => {
expect(response.status).to.eq(200);
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,
(execution) => execution.workflowId === workflowId,
);
expect(matchingExecutions).to.have.length(2);
expect(matchingExecutions).to.have.length(1);
workflowPage.actions.activateWorkflow();
workflowPage.getters.activatorSwitch().should('not.have.class', 'is-checked');
cy.visit(workflowsPage.url);
workflowsPage.actions.deleteWorkFlow('Schedule Trigger Workflow');
});
});
cy.wait(1200);
cy.request<ExecutionResponse>('GET', `${BACKEND_BASE_URL}/rest/executions`).then(
(response1) => {
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 { WorkflowPage, NDV, CredentialsModal } from '../pages';
import { cowBase64 } from '../support/binaryTestFiles';
import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { getVisibleSelect } from '../utils';
@ -75,7 +75,7 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
}
};
describe('Webhook Trigger node', async () => {
describe('Webhook Trigger node', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
@ -121,10 +121,12 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.MyValue).to.eq(1234);
});
cy.request<{ MyValue: number }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
(response) => {
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', () => {
@ -161,10 +163,12 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.MyValue).to.eq(1234);
});
cy.request<{ MyValue: number }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
(response) => {
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', () => {
@ -200,10 +204,12 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
expect(response.status).to.eq(200);
expect(Object.keys(response.body).includes('data')).to.be.true;
});
cy.request<{ data: unknown }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
(response) => {
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', () => {
@ -217,10 +223,12 @@ describe('Webhook Trigger node', async () => {
});
ndv.actions.execute();
cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.MyValue).to.be.undefined;
});
cy.request<{ MyValue: unknown }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
(response) => {
expect(response.status).to.eq(200);
expect(response.body.MyValue).to.be.undefined;
},
);
});
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');
});
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.updateFirstAndLastName(
updatedPersonalData.newFirstName,
@ -199,15 +199,15 @@ describe('User Management', { disableAutoLogin: true }, () => {
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.getters.changePasswordLink().click();
for (let weakPass of updatedPersonalData.invalidPasswords) {
for (const weakPass of updatedPersonalData.invalidPasswords) {
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.getters.changePasswordLink().click();
personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword);
@ -217,7 +217,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
.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.getters.changePasswordLink().click();
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(
INSTANCE_OWNER.email,
updatedPersonalData.newPassword,
@ -242,7 +242,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('.')[0]);
});
it(`should change user email`, () => {
it('should change user email', () => {
personalSettingsPage.actions.loginAndVisit(
INSTANCE_OWNER.email,
updatedPersonalData.newPassword,

View file

@ -512,8 +512,9 @@ describe('Execution', () => {
expect(interception.request.body).to.have.property('runData').that.is.an('object');
const expectedKeys = ['When clicking Test workflow', 'fetch 5 random users'];
expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(expectedKeys.length);
expect(interception.request.body.runData).to.include.all.keys(expectedKeys);
const { runData } = interception.request.body as Record<string, object>;
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');
const expectedPinnedDataKeys = ['Webhook'];
expect(Object.keys(interception.request.body.pinData)).to.have.lengthOf(
expectedPinnedDataKeys.length,
);
expect(interception.request.body.pinData).to.include.all.keys(expectedPinnedDataKeys);
const { pinData } = interception.request.body as Record<string, object>;
expect(Object.keys(pinData)).to.have.lengthOf(expectedPinnedDataKeys.length);
expect(pinData).to.include.all.keys(expectedPinnedDataKeys);
});
workflowPage.getters.clearExecutionDataButton().should('be.visible');
@ -558,15 +558,12 @@ describe('Execution', () => {
const expectedPinnedDataKeys = ['Webhook'];
const expectedRunDataKeys = ['If', 'Webhook'];
expect(Object.keys(interception.request.body.pinData)).to.have.lengthOf(
expectedPinnedDataKeys.length,
);
expect(interception.request.body.pinData).to.include.all.keys(expectedPinnedDataKeys);
const { pinData, runData } = interception.request.body as Record<string, object>;
expect(Object.keys(pinData)).to.have.lengthOf(expectedPinnedDataKeys.length);
expect(pinData).to.include.all.keys(expectedPinnedDataKeys);
expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(
expectedRunDataKeys.length,
);
expect(interception.request.body.runData).to.include.all.keys(expectedRunDataKeys);
expect(Object.keys(runData)).to.have.lengthOf(expectedRunDataKeys.length);
expect(runData).to.include.all.keys(expectedRunDataKeys);
});
});
@ -617,6 +614,6 @@ describe('Execution', () => {
.within(() => cy.get('.fa-check'))
.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 {
GMAIL_NODE_NAME,
HTTP_REQUEST_NODE_NAME,
@ -209,7 +210,7 @@ describe('Credentials', () => {
req.headers['cache-control'] = 'no-cache, no-store';
req.on('response', (res) => {
const credentials = res.body || [];
const credentials: ICredentialType[] = res.body || [];
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 { 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';
const workflowPage = new WorkflowPage();
@ -11,7 +11,7 @@ const executionsRefreshInterval = 4000;
describe('Current Workflow Executions', () => {
beforeEach(() => {
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', () => {
@ -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', () => {
const throttleResponse: RouteHandler = (req) => {
return new Promise((resolve) => {
const throttleResponse: RouteHandler = async (req) => {
return await new Promise((resolve) => {
setTimeout(() => resolve(req.continue()), 2000);
});
};
@ -89,6 +89,7 @@ describe('Current Workflow Executions', () => {
.should('be.visible')
.its('0.contentDocument.body') // Access the body of the iframe document
.should('not.be.empty') // Ensure the body is not empty
// eslint-disable-next-line @typescript-eslint/unbound-method
.then(cy.wrap)
.find('.el-notification:has(.el-notification--error)')
.should('be.visible')

View file

@ -1,3 +1,4 @@
import type { ICredentialType } from 'n8n-workflow';
import { NodeCreator } from '../pages/features/node-creator';
import CustomNodeFixture from '../fixtures/Custom_node.json';
import { CredentialsModal, WorkflowPage } from '../pages';
@ -33,9 +34,9 @@ describe('Community Nodes', () => {
req.headers['cache-control'] = 'no-cache, no-store';
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')
.its('0.contentDocument.body')
.should('not.be.empty')
// eslint-disable-next-line @typescript-eslint/unbound-method
.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', 'has-run')
.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')
.its('0.contentDocument.body')
.should('not.be.empty')
// eslint-disable-next-line @typescript-eslint/unbound-method
.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', 'has-run')
.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', () => {
// setup, load workflow with debughelper node with random seed
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');
// 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 { WorkflowPage, NDV } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();

View file

@ -1,7 +1,7 @@
import type { Interception } from 'cypress/types/net-stubbing';
import { META_KEY } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { getPopper } from '../utils';
import { Interception } from 'cypress/types/net-stubbing';
const workflowPage = new WorkflowPageClass();
@ -91,7 +91,7 @@ describe('Canvas Actions', () => {
getPopper().should('be.visible');
workflowPage.actions.pickColor(2);
workflowPage.actions.pickColor();
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) {
cy.drag('[data-test-id="sticky"]', [target.left, target.top], { abs: true });
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 { SigninPage } from '../pages';
import { PersonalSettingsPage } from '../pages/settings-personal';
import { MfaLoginPage } from '../pages/mfa-login';
import generateOTPToken from 'cypress-otp';
import { MainSidebar } from './../pages/sidebar/main-sidebar';
const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD';
@ -36,14 +36,14 @@ const mainSidebar = new MainSidebar();
describe('Two-factor authentication', () => {
beforeEach(() => {
Cypress.session.clearAllSavedSessions();
void Cypress.session.clearAllSavedSessions();
cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, {
owner: user,
members: [],
admin,
});
cy.on('uncaught:exception', (err, runnable) => {
expect(err.message).to.include('Not logged in');
cy.on('uncaught:exception', (error) => {
expect(error.message).to.include('Not logged in');
return false;
});
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.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
workflowPage.actions.visit();
cy.createFixtureWorkflow('Lots_of_nodes.json', `Lots of nodes`);
cy.createFixtureWorkflow('Lots_of_nodes.json', 'Lots of nodes');
workflowPage.actions.saveWorkflowOnButtonClick();
});

View file

@ -1,17 +1,4 @@
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';
import { createMockNodeExecutionData, runMockWorkflowExcution } from '../utils';
import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
import {
addLanguageModelNodeToParent,
addMemoryNodeToParent,
@ -42,6 +29,19 @@ import {
getManualChatModalLogsTree,
sendManualChatMessage,
} 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', () => {
beforeEach(() => {
@ -149,7 +149,7 @@ describe('Langchain Integration', () => {
const outputMessage = 'Hi there! How can I assist you today?';
clickExecuteNode();
runMockWorkflowExcution({
runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage),
runData: [
createMockNodeExecutionData(BASIC_LLM_CHAIN_NODE_NAME, {
@ -189,7 +189,7 @@ describe('Langchain Integration', () => {
const outputMessage = 'Hi there! How can I assist you today?';
clickExecuteNode();
runMockWorkflowExcution({
runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage),
runData: [
createMockNodeExecutionData(AGENT_NODE_NAME, {
@ -230,7 +230,7 @@ describe('Langchain Integration', () => {
const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?';
runMockWorkflowExcution({
runMockWorkflowExecution({
trigger: () => {
sendManualChatMessage(inputMessage);
},

View file

@ -6,7 +6,7 @@ const ndv = new NDV();
describe('Node IO Filter', () => {
beforeEach(() => {
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.executeWorkflow();
});

View file

@ -1,4 +1,4 @@
import { WorkflowPage } from "../pages";
import { WorkflowPage } from '../pages';
const workflowPage = new WorkflowPage();
@ -27,7 +27,7 @@ const VALID_NAMES = [
];
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');
VALID_NAMES.forEach((name) => {
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');
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');
});
@ -58,7 +58,7 @@ describe('Template credentials setup', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
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');
templateCredentialsSetupPage.getters

View file

@ -64,7 +64,7 @@ describe('Import workflow', () => {
workflowPage.getters.workflowMenuItemImportFromFile().click();
workflowPage.getters
.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);
workflowPage.actions.zoomToFit();
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 {
WorkflowsPage,
WorkflowPage,
@ -260,7 +266,9 @@ describe('Projects', () => {
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').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');
cy.intercept('POST', '/rest/credentials').as('credentialSave');
@ -283,7 +291,9 @@ describe('Projects', () => {
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').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.save();
@ -303,12 +313,14 @@ describe('Projects', () => {
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').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');
cy.intercept('POST', '/rest/credentials').as('credentialSave');
credentialsModal.actions.save();
cy.wait('@credentialSave')
cy.wait('@credentialSave');
credentialsModal.actions.close();
// 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(NOTION_NODE_NAME, true, true);
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();
workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload();
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
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();
// 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(NOTION_NODE_NAME, true, true);
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();
workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload();
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
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();
// 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(NOTION_NODE_NAME, true, true);
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();
workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload();
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
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', () => {
it('should preserve changes when opening-closing Postgres node', () => {
workflowPage.actions.addInitialNodeToCanvas('Postgres', {
action: 'Execute a SQL query',
@ -26,7 +25,11 @@ describe('Editors', () => {
.type('{esc}');
ndv.actions.close();
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();
workflowPage.actions.openNode('Postgres');
ndv.getters.sqlEditorContainer().should('contain', 'SELECT * FROM `testTable` LIMIT 10');
@ -126,7 +129,11 @@ describe('Editors', () => {
.type('{esc}');
ndv.actions.close();
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();
workflowPage.actions.openNode('HTML');
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', () => {
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.executeWorkflow();
@ -305,7 +305,7 @@ describe('NDV', () => {
it('should display parameter hints correctly', () => {
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');
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.actions.validateExpressionPreview('value', output || input);
ndv.actions.validateExpressionPreview('value', output ?? input);
ndv.getters.parameterInput('value').clear();
});
});
@ -436,7 +436,7 @@ describe('NDV', () => {
}
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();
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist');
@ -482,7 +482,7 @@ describe('NDV', () => {
});
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();
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist');
@ -597,7 +597,7 @@ describe('NDV', () => {
});
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();
@ -741,7 +741,7 @@ describe('NDV', () => {
it('should allow selecting item for expressions', () => {
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');
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}');
WorkflowPage.getters.successToast().should('not.exist');
});
});
describe('Menu entry Push To Git', () => {

View file

@ -8,7 +8,7 @@ describe('Expression editor modal', () => {
beforeEach(() => {
WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Schedule');
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError');
cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
});
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 = {
banner: () => cy.getByTestId('banner-stack'),
};
actions = {};
}

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import { BasePage } from '../base';
import { INodeTypeDescription } from 'n8n-workflow';
export class NodeCreator extends BasePage {
url = '/workflow/new';
getters = {
plusButton: () => cy.getByTestId('node-creator-plus-button'),
canvasAddButton: () => cy.getByTestId('canvas-add-button'),
@ -25,6 +25,7 @@ export class NodeCreator extends BasePage {
expandedCategories: () =>
this.getters.creatorItem().find('>div').filter('.active').invoke('text'),
};
actions = {
openNodeCreator: () => {
this.getters.plusButton().click();
@ -33,31 +34,5 @@ export class NodeCreator extends BasePage {
selectNode: (displayName: string) => {
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 {
url = '/mfa';
getters = {
form: () => cy.getByTestId('mfa-login-form'),
token: () => cy.getByTestId('token'),

View file

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

View file

@ -8,6 +8,7 @@ export class MessageBox extends BasePage {
confirm: () => this.getters.modal().find('.btn--confirm').first(),
cancel: () => this.getters.modal().find('.btn--cancel').first(),
};
actions = {
confirm: () => {
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'),
closeButton: () => this.getters.modal().find('.el-dialog__close').first(),
};
actions = {
addUser: (email: string) => {
this.getters.usersSelect().click();

View file

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

View file

@ -1,8 +1,9 @@
import { BasePage } from './base';
import { getVisibleSelect } from '../utils';
import { BasePage } from './base';
export class SettingsLogStreamingPage extends BasePage {
url = '/settings/log-streaming';
getters = {
getActionBoxUnlicensed: () => cy.getByTestId('action-box-unlicensed'),
getActionBoxLicensed: () => cy.getByTestId('action-box-licensed'),
@ -17,6 +18,7 @@ export class SettingsLogStreamingPage extends BasePage {
getDestinationDeleteButton: () => cy.getByTestId('destination-delete-button'),
getDestinationCards: () => cy.getByTestId('destination-card'),
};
actions = {
clickContactUs: () => this.getters.getContactUsButton().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 { MfaSetupModal } from './modals/mfa-setup-modal';
import { BasePage } from './base';
import generateOTPToken from 'cypress-otp';
const changePasswordModal = new ChangePasswordModal();
const mfaSetupModal = new MfaSetupModal();
export class PersonalSettingsPage extends BasePage {
url = '/settings/personal';
secret = '';
getters = {
@ -23,6 +24,7 @@ export class PersonalSettingsPage extends BasePage {
themeSelector: () => cy.getByTestId('theme-select'),
selectOptionsVisible: () => cy.get('.el-select-dropdown:visible .el-select-dropdown__item'),
};
actions = {
changeTheme: (theme: 'System default' | 'Dark' | 'Light') => {
this.getters.themeSelector().click();

View file

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

View file

@ -11,6 +11,7 @@ const settingsSidebar = new SettingsSidebar();
export class SettingsUsersPage extends BasePage {
url = '/settings/users';
getters = {
setUpOwnerButton: () => cy.getByTestId('action-box').find('button').first(),
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")'),
deleteDataInput: () => cy.getByTestId('delete-data-input').find('input').first(),
};
actions = {
goToOwnerSetup: () => this.getters.setUpOwnerButton().click(),
loginAndVisit: (email: string, password: string, isOwner: boolean) => {

View file

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

View file

@ -1,8 +1,6 @@
import { BasePage } from '../base';
import { WorkflowsPage } from '../workflows';
const workflowsPage = new WorkflowsPage();
export class MainSidebar extends BasePage {
getters = {
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"]'),
logo: () => cy.getByTestId('n8n-logo'),
};
actions = {
goToSettings: () => {
this.getters.settings().should('be.visible');

View file

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

View file

@ -4,6 +4,7 @@ import { WorkflowsPage } from './workflows';
export class SigninPage extends BasePage {
url = '/signin';
getters = {
form: () => cy.getByTestId('auth-form'),
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 { overrideFeatureFlag } from '../composables/featureFlags';
import { CredentialsModal, MessageBox } from './modals';
export type TemplateTestData = {
id: number;

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import { BasePage } from './base';
export class WorkerViewPage extends BasePage {
url = '/settings/workers';
getters = {
workerCards: () => cy.getByTestId('worker-card'),
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'),
workflowExecutionPreviewIframe: () => cy.getByTestId('workflow-preview-iframe'),
};
actions = {
toggleNodeEnabled: (nodeName: string) => {
workflowPage.getters.canvasNodeByName(nodeName).click();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,5 +7,6 @@
"types": ["cypress", "node"]
},
"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 = (
...args: any[]
...args: unknown[]
) =>
| Cypress.Chainable<JQuery<HTMLElement>>
| Cypress.Chainable<JQuery<HTMLInputElement>>
| Cypress.Chainable<JQuery<HTMLButtonElement>>;
type Getter = IE2ETestPageElement | ((key: string | number) => IE2ETestPageElement);
export interface IE2ETestPage {
url?: string;
getters: Record<string, IE2ETestPageElement>;
actions: Record<string, (...args: any[]) => void>;
getters: Record<string, Getter>;
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 { IPinData } from '../../packages/workflow';
import type { IDataObject, IPinData, ITaskData, ITaskDataConnections } from 'n8n-workflow';
import { clickExecuteWorkflowButton } from '../composables/workflow';
export function createMockNodeExecutionData(
@ -10,7 +9,7 @@ export function createMockNodeExecutionData(
executionStatus = 'success',
jsonData,
...rest
}: Partial<ITaskData> & { jsonData?: Record<string, object> },
}: Partial<ITaskData> & { jsonData?: Record<string, IDataObject> },
): Record<string, ITaskData> {
return {
[name]: {
@ -29,7 +28,7 @@ export function createMockNodeExecutionData(
];
return acc;
}, {})
}, {} as ITaskDataConnections)
: data,
source: [null],
...rest,
@ -75,7 +74,7 @@ export function createMockWorkflowExecutionData({
};
}
export function runMockWorkflowExcution({
export function runMockWorkflowExecution({
trigger,
lastNodeExecuted,
runData,
@ -105,7 +104,7 @@ export function runMockWorkflowExcution({
cy.wait('@runWorkflow');
const resolvedRunData = {};
const resolvedRunData: Record<string, ITaskData> = {};
runData.forEach((nodeExecution) => {
const nodeName = Object.keys(nodeExecution)[0];
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",
"watch": "turbo run watch --parallel",
"webhook": "./packages/cli/bin/n8n webhook",
"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"
"worker": "./packages/cli/bin/n8n worker"
},
"devDependencies": {
"@n8n_io/eslint-config": "workspace:*",
"@ngneat/falso": "^6.4.0",
"@types/jest": "^29.5.3",
"@types/supertest": "^6.0.2",
"@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-environment-jsdom": "^29.6.2",
"jest-expect-message": "^1.1.3",
@ -58,7 +48,6 @@
"p-limit": "^3.1.0",
"rimraf": "^5.0.1",
"run-script-os": "^1.0.7",
"start-server-and-test": "^2.0.3",
"supertest": "^7.0.0",
"ts-jest": "^29.1.1",
"tsc-alias": "^1.8.7",

View file

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

View file

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