From af3ac2db2850237663cf2f24a4466ae4a0fc6f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 10 Jun 2024 15:49:50 +0200 Subject: [PATCH] refactor: Set up Cypress as pnpm workspace (no-changelog) (#6049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- .github/workflows/e2e-reusable.yml | 8 +- .gitignore | 3 - cypress/.eslintrc.js | 24 ++++++ cypress/.gitignore | 3 + cypress/augmentation.d.ts | 4 + .../composables/becomeTemplateCreatorCta.ts | 2 +- .../composables/modals/credential-modal.ts | 2 +- cypress/composables/ndv.ts | 4 +- .../setup-workflow-credentials-button.ts | 2 +- cypress/composables/workflow.ts | 2 +- .../cypress.config.js | 6 ++ cypress/e2e/1-workflows.cy.ts | 2 +- cypress/e2e/10-undo-redo.cy.ts | 16 ++-- cypress/e2e/11-inline-expression-editor.cy.ts | 2 +- cypress/e2e/12-canvas-actions.cy.ts | 2 +- cypress/e2e/12-canvas.cy.ts | 6 +- cypress/e2e/13-pinning.cy.ts | 9 +-- .../14-data-transformation-expressions.cy.ts | 2 +- cypress/e2e/14-mapping.cy.ts | 6 +- cypress/e2e/15-scheduler-node.cy.ts | 45 +++++++----- cypress/e2e/16-webhook-node.cy.ts | 44 ++++++----- cypress/e2e/18-user-management.cy.ts | 14 ++-- cypress/e2e/19-execution.cy.ts | 27 +++---- cypress/e2e/2-credentials.cy.ts | 3 +- cypress/e2e/20-workflow-executions.cy.ts | 9 ++- cypress/e2e/21-community-nodes.cy.ts | 5 +- ...06-ADO-pinned-data-execution-preview.cy.ts | 6 +- .../2230-ADO-ndv-reset-data-pagination.cy.ts | 2 +- cypress/e2e/24-ndv-paired-item.cy.ts | 2 +- cypress/e2e/25-stickies.cy.ts | 13 +--- .../e2e/27-two-factor-authentication.cy.ts | 10 +-- .../e2e/30-editor-after-route-changes.cy.ts | 2 +- cypress/e2e/30-langchain.cy.ts | 34 ++++----- cypress/e2e/32-node-io-filter.cy.ts | 2 +- cypress/e2e/33-settings-personal.cy.ts | 4 +- .../e2e/34-template-credentials-setup.cy.ts | 4 +- cypress/e2e/39-import-workflow.cy.ts | 2 +- cypress/e2e/39-projects.cy.ts | 59 ++++++++++++--- cypress/e2e/41-editors.cy.ts | 13 +++- cypress/e2e/5-ndv.cy.ts | 14 ++-- cypress/e2e/7-workflow-actions.cy.ts | 1 - cypress/e2e/9-expression-editor-modal.cy.ts | 2 +- cypress/package.json | 28 +++++++ cypress/pages/bannerStack.ts | 1 + cypress/pages/base.ts | 7 +- cypress/pages/credentials.ts | 2 + cypress/pages/demo.ts | 7 +- cypress/pages/features/node-creator.ts | 29 +------- cypress/pages/mfa-login.ts | 1 + cypress/pages/modals/credentials-modal.ts | 3 +- cypress/pages/modals/message-box.ts | 1 + .../pages/modals/workflow-sharing-modal.ts | 1 + cypress/pages/ndv.ts | 17 ++--- cypress/pages/settings-log-streaming.ts | 4 +- cypress/pages/settings-personal.ts | 4 +- cypress/pages/settings-usage.ts | 2 + cypress/pages/settings-users.ts | 2 + cypress/pages/settings.ts | 2 + cypress/pages/sidebar/main-sidebar.ts | 3 +- cypress/pages/sidebar/settings-sidebar.ts | 1 + cypress/pages/signin.ts | 1 + cypress/pages/template-credential-setup.ts | 2 +- cypress/pages/template-workflow.ts | 21 +++--- cypress/pages/variables.ts | 3 +- cypress/pages/workerView.ts | 1 + cypress/pages/workflow-executions-tab.ts | 1 + cypress/pages/workflow-history.ts | 8 +- cypress/pages/workflow.ts | 33 ++++----- cypress/pages/workflows.ts | 1 + {scripts => cypress/scripts}/run-e2e.js | 0 cypress/support/commands.ts | 8 +- cypress/support/e2e.ts | 4 +- cypress/support/index.ts | 11 ++- cypress/tsconfig.json | 3 +- cypress/types.ts | 18 ++++- cypress/utils/executions.ts | 11 ++- package.json | 13 +--- pnpm-lock.yaml | 73 ++++++++++--------- pnpm-workspace.yaml | 1 + 79 files changed, 435 insertions(+), 315 deletions(-) create mode 100644 cypress/.eslintrc.js create mode 100644 cypress/.gitignore create mode 100644 cypress/augmentation.d.ts rename cypress.config.js => cypress/cypress.config.js (76%) create mode 100644 cypress/package.json rename {scripts => cypress/scripts}/run-e2e.js (100%) diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index b5cc8a38c6..771dfab35a 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 592a8894f3..e36cf95823 100644 --- a/.gitignore +++ b/.gitignore @@ -18,9 +18,6 @@ nodelinter.config.json packages/**/.turbo .turbo *.tsbuildinfo -cypress/videos/* -cypress/screenshots/* -cypress/downloads/* *.swp CHANGELOG-*.md *.mdx diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js new file mode 100644 index 0000000000..dc6abe3a71 --- /dev/null +++ b/cypress/.eslintrc.js @@ -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', + }, +}; diff --git a/cypress/.gitignore b/cypress/.gitignore new file mode 100644 index 0000000000..a1d14ddebb --- /dev/null +++ b/cypress/.gitignore @@ -0,0 +1,3 @@ +videos/ +screenshots/ +downloads/ diff --git a/cypress/augmentation.d.ts b/cypress/augmentation.d.ts new file mode 100644 index 0000000000..334bc0e9f4 --- /dev/null +++ b/cypress/augmentation.d.ts @@ -0,0 +1,4 @@ +declare module 'cypress-otp' { + // eslint-disable-next-line import/no-default-export + export default function generateOTPToken(secret: string): string; +} diff --git a/cypress/composables/becomeTemplateCreatorCta.ts b/cypress/composables/becomeTemplateCreatorCta.ts index 55fc985c74..ca35e611d9 100644 --- a/cypress/composables/becomeTemplateCreatorCta.ts +++ b/cypress/composables/becomeTemplateCreatorCta.ts @@ -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, }); }; diff --git a/cypress/composables/modals/credential-modal.ts b/cypress/composables/modals/credential-modal.ts index bfcbf89251..8ce6a86049 100644 --- a/cypress/composables/modals/credential-modal.ts +++ b/cypress/composables/modals/credential-modal.ts @@ -42,7 +42,7 @@ export function closeCredentialModal() { getCredentialModalCloseButton().click(); } -export function setCredentialValues(values: Record, save = true) { +export function setCredentialValues(values: Record, save = true) { Object.entries(values).forEach(([key, value]) => { setCredentialConnectionParameterInputByName(key, value); }); diff --git a/cypress/composables/ndv.ts b/cypress/composables/ndv.ts index e2fc03d7af..c3fab73f8c 100644 --- a/cypress/composables/ndv.ts +++ b/cypress/composables/ndv.ts @@ -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) { diff --git a/cypress/composables/setup-workflow-credentials-button.ts b/cypress/composables/setup-workflow-credentials-button.ts index 6b1b9b69d4..8285454d83 100644 --- a/cypress/composables/setup-workflow-credentials-button.ts +++ b/cypress/composables/setup-workflow-credentials-button.ts @@ -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")'); diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index 1aa469b194..b3d6f20c28 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -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) { diff --git a/cypress.config.js b/cypress/cypress.config.js similarity index 76% rename from cypress.config.js rename to cypress/cypress.config.js index f01672c6f9..c82f039994 100644 --- a/cypress.config.js +++ b/cypress/cypress.config.js @@ -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 diff --git a/cypress/e2e/1-workflows.cy.ts b/cypress/e2e/1-workflows.cy.ts index 25f4f3cb0a..d14506d17e 100644 --- a/cypress/e2e/1-workflows.cy.ts +++ b/cypress/e2e/1-workflows.cy.ts @@ -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(); diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts index 3190987541..19465ed749 100644 --- a/cypress/e2e/10-undo-redo.cy.ts +++ b/cypress/e2e/10-undo-redo.cy.ts @@ -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', () => { diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts index 45fb7752ed..a7fadd196f 100644 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ b/cypress/e2e/11-inline-expression-editor.cy.ts @@ -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', () => { diff --git a/cypress/e2e/12-canvas-actions.cy.ts b/cypress/e2e/12-canvas-actions.cy.ts index 3c517b6c98..4cdfe6808e 100644 --- a/cypress/e2e/12-canvas-actions.cy.ts +++ b/cypress/e2e/12-canvas-actions.cy.ts @@ -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', () => { diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index 13fef5b10c..db6b38d53a 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -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 diff --git a/cypress/e2e/13-pinning.cy.ts b/cypress/e2e/13-pinning.cy.ts index a27b830ce6..0929e8cfae 100644 --- a/cypress/e2e/13-pinning.cy.ts +++ b/cypress/e2e/13-pinning.cy.ts @@ -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}}]'; diff --git a/cypress/e2e/14-data-transformation-expressions.cy.ts b/cypress/e2e/14-data-transformation-expressions.cy.ts index 21c958d691..e823e31d5e 100644 --- a/cypress/e2e/14-data-transformation-expressions.cy.ts +++ b/cypress/e2e/14-data-transformation-expressions.cy.ts @@ -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(); diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index f8711db226..365efbd0d3 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -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'); diff --git a/cypress/e2e/15-scheduler-node.cy.ts b/cypress/e2e/15-scheduler-node.cy.ts index 0021455619..fecaef038a 100644 --- a/cypress/e2e/15-scheduler-node.cy.ts +++ b/cypress/e2e/15-scheduler-node.cy.ts @@ -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('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('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'); + }, + ); + }, + ); }); }); }); diff --git a/cypress/e2e/16-webhook-node.cy.ts b/cypress/e2e/16-webhook-node.cy.ts index 560fc41056..c28046dc17 100644 --- a/cypress/e2e/16-webhook-node.cy.ts +++ b/cypress/e2e/16-webhook-node.cy.ts @@ -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', () => { diff --git a/cypress/e2e/18-user-management.cy.ts b/cypress/e2e/18-user-management.cy.ts index fdd78cf1f2..0332525ab9 100644 --- a/cypress/e2e/18-user-management.cy.ts +++ b/cypress/e2e/18-user-management.cy.ts @@ -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, diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index 84b71e0885..921dc8bbca 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -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; + 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; + 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; + 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‘'); }); }); diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index 1a9776d280..cc10513ba7 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -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'); diff --git a/cypress/e2e/20-workflow-executions.cy.ts b/cypress/e2e/20-workflow-executions.cy.ts index bf5ecb17b3..1d807695fd 100644 --- a/cypress/e2e/20-workflow-executions.cy.ts +++ b/cypress/e2e/20-workflow-executions.cy.ts @@ -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') diff --git a/cypress/e2e/21-community-nodes.cy.ts b/cypress/e2e/21-community-nodes.cy.ts index 39f572ba5c..b9d10b30f2 100644 --- a/cypress/e2e/21-community-nodes.cy.ts +++ b/cypress/e2e/21-community-nodes.cy.ts @@ -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); }); }); diff --git a/cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts b/cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts index 386d8762aa..6c69f4f79d 100644 --- a/cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts +++ b/cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts @@ -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'); diff --git a/cypress/e2e/2230-ADO-ndv-reset-data-pagination.cy.ts b/cypress/e2e/2230-ADO-ndv-reset-data-pagination.cy.ts index 5fce177848..849b520c4c 100644 --- a/cypress/e2e/2230-ADO-ndv-reset-data-pagination.cy.ts +++ b/cypress/e2e/2230-ADO-ndv-reset-data-pagination.cy.ts @@ -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 diff --git a/cypress/e2e/24-ndv-paired-item.cy.ts b/cypress/e2e/24-ndv-paired-item.cy.ts index 1b2b4f1efe..3d57a33a3b 100644 --- a/cypress/e2e/24-ndv-paired-item.cy.ts +++ b/cypress/e2e/24-ndv-paired-item.cy.ts @@ -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(); diff --git a/cypress/e2e/25-stickies.cy.ts b/cypress/e2e/25-stickies.cy.ts index 4cbad810f9..35416ebd3e 100644 --- a/cypress/e2e/25-stickies.cy.ts +++ b/cypress/e2e/25-stickies.cy.ts @@ -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); diff --git a/cypress/e2e/27-two-factor-authentication.cy.ts b/cypress/e2e/27-two-factor-authentication.cy.ts index 91f6ca57a2..7a1ee28f22 100644 --- a/cypress/e2e/27-two-factor-authentication.cy.ts +++ b/cypress/e2e/27-two-factor-authentication.cy.ts @@ -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'); diff --git a/cypress/e2e/30-editor-after-route-changes.cy.ts b/cypress/e2e/30-editor-after-route-changes.cy.ts index a502d3577c..423c92110b 100644 --- a/cypress/e2e/30-editor-after-route-changes.cy.ts +++ b/cypress/e2e/30-editor-after-route-changes.cy.ts @@ -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(); }); diff --git a/cypress/e2e/30-langchain.cy.ts b/cypress/e2e/30-langchain.cy.ts index 9536b3cf60..c1409a34f3 100644 --- a/cypress/e2e/30-langchain.cy.ts +++ b/cypress/e2e/30-langchain.cy.ts @@ -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); }, diff --git a/cypress/e2e/32-node-io-filter.cy.ts b/cypress/e2e/32-node-io-filter.cy.ts index 3f1ffdf005..bc39c1b611 100644 --- a/cypress/e2e/32-node-io-filter.cy.ts +++ b/cypress/e2e/32-node-io-filter.cy.ts @@ -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(); }); diff --git a/cypress/e2e/33-settings-personal.cy.ts b/cypress/e2e/33-settings-personal.cy.ts index 9257bee22d..58e0f05237 100644 --- a/cypress/e2e/33-settings-personal.cy.ts +++ b/cypress/e2e/33-settings-personal.cy.ts @@ -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]); diff --git a/cypress/e2e/34-template-credentials-setup.cy.ts b/cypress/e2e/34-template-credentials-setup.cy.ts index e112cd926b..7553a55a7b 100644 --- a/cypress/e2e/34-template-credentials-setup.cy.ts +++ b/cypress/e2e/34-template-credentials-setup.cy.ts @@ -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 diff --git a/cypress/e2e/39-import-workflow.cy.ts b/cypress/e2e/39-import-workflow.cy.ts index 831228fba3..5b78b7c65d 100644 --- a/cypress/e2e/39-import-workflow.cy.ts +++ b/cypress/e2e/39-import-workflow.cy.ts @@ -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); diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts index 21685dd58c..96e98a0031 100644 --- a/cypress/e2e/39-projects.cy.ts +++ b/cypress/e2e/39-projects.cy.ts @@ -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'); }); }); }); diff --git a/cypress/e2e/41-editors.cy.ts b/cypress/e2e/41-editors.cy.ts index 0c44c51185..f7ad8129b3 100644 --- a/cypress/e2e/41-editors.cy.ts +++ b/cypress/e2e/41-editors.cy.ts @@ -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); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 0ebd859174..7623e2de97 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -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 diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index c0875d93f7..0ba151f4cc 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -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', () => { diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts index ddf4ac3090..30b6a84957 100644 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ b/cypress/e2e/9-expression-editor-modal.cy.ts @@ -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', () => { diff --git a/cypress/package.json b/cypress/package.json new file mode 100644 index 0000000000..aabcc929c5 --- /dev/null +++ b/cypress/package.json @@ -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" + } +} diff --git a/cypress/pages/bannerStack.ts b/cypress/pages/bannerStack.ts index dce3222126..c4936891ae 100644 --- a/cypress/pages/bannerStack.ts +++ b/cypress/pages/bannerStack.ts @@ -4,5 +4,6 @@ export class BannerStack extends BasePage { getters = { banner: () => cy.getByTestId('banner-stack'), }; + actions = {}; } diff --git a/cypress/pages/base.ts b/cypress/pages/base.ts index 06c832591c..abd7a210a8 100644 --- a/cypress/pages/base.ts +++ b/cypress/pages/base.ts @@ -1,6 +1,7 @@ -import { IE2ETestPage, IE2ETestPageElement } from '../types'; +import type { IE2ETestPage } from '../types'; export class BasePage implements IE2ETestPage { - getters: Record = {}; - actions: Record void> = {}; + getters = {}; + + actions = {}; } diff --git a/cypress/pages/credentials.ts b/cypress/pages/credentials.ts index 7ae2d0f3b4..a356dc1421 100644 --- a/cypress/pages/credentials.ts +++ b/cypress/pages/credentials.ts @@ -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(); diff --git a/cypress/pages/demo.ts b/cypress/pages/demo.ts index 0590fb8def..691066ce05 100644 --- a/cypress/pages/demo.ts +++ b/cypress/pages/demo.ts @@ -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, '*'); }); } diff --git a/cypress/pages/features/node-creator.ts b/cypress/pages/features/node-creator.ts index 3e6a819443..a0d3995160 100644 --- a/cypress/pages/features/node-creator.ts +++ b/cypress/pages/features/node-creator.ts @@ -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; - }, }; } diff --git a/cypress/pages/mfa-login.ts b/cypress/pages/mfa-login.ts index 50ca5adab7..ae4d916ba9 100644 --- a/cypress/pages/mfa-login.ts +++ b/cypress/pages/mfa-login.ts @@ -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'), diff --git a/cypress/pages/modals/credentials-modal.ts b/cypress/pages/modals/credentials-modal.ts index 2275ea5e4c..9492f59bbe 100644 --- a/cypress/pages/modals/credentials-modal.ts +++ b/cypress/pages/modals/credentials-modal.ts @@ -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'); diff --git a/cypress/pages/modals/message-box.ts b/cypress/pages/modals/message-box.ts index b54e375ef6..a40c2d1a88 100644 --- a/cypress/pages/modals/message-box.ts +++ b/cypress/pages/modals/message-box.ts @@ -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 }); diff --git a/cypress/pages/modals/workflow-sharing-modal.ts b/cypress/pages/modals/workflow-sharing-modal.ts index fc4ba8dada..02e183fc81 100644 --- a/cypress/pages/modals/workflow-sharing-modal.ts +++ b/cypress/pages/modals/workflow-sharing-modal.ts @@ -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(); diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 0cb7609fb2..651c58feb3 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -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(); diff --git a/cypress/pages/settings-log-streaming.ts b/cypress/pages/settings-log-streaming.ts index 2d056a4444..cc1ea1250d 100644 --- a/cypress/pages/settings-log-streaming.ts +++ b/cypress/pages/settings-log-streaming.ts @@ -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(), diff --git a/cypress/pages/settings-personal.ts b/cypress/pages/settings-personal.ts index 716625beb5..69227603db 100644 --- a/cypress/pages/settings-personal.ts +++ b/cypress/pages/settings-personal.ts @@ -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(); diff --git a/cypress/pages/settings-usage.ts b/cypress/pages/settings-usage.ts index cd3dfd3596..85300fe05f 100644 --- a/cypress/pages/settings-usage.ts +++ b/cypress/pages/settings-usage.ts @@ -2,6 +2,8 @@ import { BasePage } from './base'; export class SettingsUsagePage extends BasePage { url = '/settings/usage'; + getters = {}; + actions = {}; } diff --git a/cypress/pages/settings-users.ts b/cypress/pages/settings-users.ts index a16eb4ab6f..d188896225 100644 --- a/cypress/pages/settings-users.ts +++ b/cypress/pages/settings-users.ts @@ -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) => { diff --git a/cypress/pages/settings.ts b/cypress/pages/settings.ts index 264b525dee..74c3b0fe76 100644 --- a/cypress/pages/settings.ts +++ b/cypress/pages/settings.ts @@ -2,8 +2,10 @@ import { BasePage } from './base'; export class SettingsPage extends BasePage { url = '/settings'; + getters = { menuItems: () => cy.getByTestId('menu-item'), }; + actions = {}; } diff --git a/cypress/pages/sidebar/main-sidebar.ts b/cypress/pages/sidebar/main-sidebar.ts index 7b30de7a4e..1ce7eb54d7 100644 --- a/cypress/pages/sidebar/main-sidebar.ts +++ b/cypress/pages/sidebar/main-sidebar.ts @@ -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'); diff --git a/cypress/pages/sidebar/settings-sidebar.ts b/cypress/pages/sidebar/settings-sidebar.ts index 886a0a3c1e..17d43b65e7 100644 --- a/cypress/pages/sidebar/settings-sidebar.ts +++ b/cypress/pages/sidebar/settings-sidebar.ts @@ -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'); diff --git a/cypress/pages/signin.ts b/cypress/pages/signin.ts index 1b2b35c22f..22d0fd163a 100644 --- a/cypress/pages/signin.ts +++ b/cypress/pages/signin.ts @@ -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'), diff --git a/cypress/pages/template-credential-setup.ts b/cypress/pages/template-credential-setup.ts index d673261fdf..0910d47632 100644 --- a/cypress/pages/template-credential-setup.ts +++ b/cypress/pages/template-credential-setup.ts @@ -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; diff --git a/cypress/pages/template-workflow.ts b/cypress/pages/template-workflow.ts index d1e8630a12..84464d0ae6 100644 --- a/cypress/pages/template-workflow.ts +++ b/cypress/pages/template-workflow.ts @@ -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, diff --git a/cypress/pages/variables.ts b/cypress/pages/variables.ts index 6d9e9eb134..c74624686e 100644 --- a/cypress/pages/variables.ts +++ b/cypress/pages/variables.ts @@ -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>) => diff --git a/cypress/pages/workerView.ts b/cypress/pages/workerView.ts index e14bfd36a2..f442468c52 100644 --- a/cypress/pages/workerView.ts +++ b/cypress/pages/workerView.ts @@ -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), diff --git a/cypress/pages/workflow-executions-tab.ts b/cypress/pages/workflow-executions-tab.ts index 474a00745e..27285d28b8 100644 --- a/cypress/pages/workflow-executions-tab.ts +++ b/cypress/pages/workflow-executions-tab.ts @@ -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(); diff --git a/cypress/pages/workflow-history.ts b/cypress/pages/workflow-history.ts index 18cd6ed999..1b9d7328b1 100644 --- a/cypress/pages/workflow-history.ts +++ b/cypress/pages/workflow-history.ts @@ -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'), + }; } diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index fa8867ce4e..2a03c80c51 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -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) => { diff --git a/cypress/pages/workflows.ts b/cypress/pages/workflows.ts index 370a2ae0e3..356b9ca5ec 100644 --- a/cypress/pages/workflows.ts +++ b/cypress/pages/workflows.ts @@ -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'), diff --git a/scripts/run-e2e.js b/cypress/scripts/run-e2e.js similarity index 100% rename from scripts/run-e2e.js rename to cypress/scripts/run-e2e.js diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index bd33a8f21f..3bb28e2df8 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -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'); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 69bb74ec88..0cf29b09a4 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -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'); }); }); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 411b732250..f25104f883 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -1,7 +1,7 @@ // Load type definitions that come with Cypress module /// -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 | undefined)[] + ...args: Array | undefined> ): Chainable>; findChildByTestId(childTestId: string): Chainable>; createFixtureWorkflow(fixtureKey: string, workflowName: string): void; @@ -36,7 +36,7 @@ declare global { readClipboard(): Chainable; paste(pastePayload: string): void; drag( - selector: string | Cypress.Chainable>, + selector: string | Chainable>, 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; }; } >; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 26a5da716b..dc823f5e5c 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -7,5 +7,6 @@ "types": ["cypress", "node"] }, "include": ["**/*.ts"], - "exclude": ["**/dist/**/*", "**/node_modules/**/*"] + "exclude": ["**/dist/**/*", "**/node_modules/**/*"], + "references": [{ "path": "../packages/workflow/tsconfig.build.json" }] } diff --git a/cypress/types.ts b/cypress/types.ts index d7e776d49a..6186c4201d 100644 --- a/cypress/types.ts +++ b/cypress/types.ts @@ -1,12 +1,24 @@ export type IE2ETestPageElement = ( - ...args: any[] + ...args: unknown[] ) => | Cypress.Chainable> | Cypress.Chainable> | Cypress.Chainable>; +type Getter = IE2ETestPageElement | ((key: string | number) => IE2ETestPageElement); + export interface IE2ETestPage { url?: string; - getters: Record; - actions: Record void>; + getters: Record; + actions: Record void>; +} + +interface Execution { + workflowId: string; +} + +export interface ExecutionResponse { + data: { + results: Execution[]; + }; } diff --git a/cypress/utils/executions.ts b/cypress/utils/executions.ts index d88b58ea9b..4a40eff8fe 100644 --- a/cypress/utils/executions.ts +++ b/cypress/utils/executions.ts @@ -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 & { jsonData?: Record }, + }: Partial & { jsonData?: Record }, ): Record { 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 = {}; runData.forEach((nodeExecution) => { const nodeName = Object.keys(nodeExecution)[0]; const nodeRunData = nodeExecution[nodeName]; diff --git a/package.json b/package.json index d82bf6247b..124b672571 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2ddd7d6da..e0a71cdaed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ce18506b38..add43b6c00 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - packages/* - packages/@n8n/* - packages/@n8n_io/* + - cypress