Merge branch 'master' of github.com:n8n-io/n8n into highlevel-node-update

This commit is contained in:
Jonathan Bennetts 2025-01-03 10:48:00 +00:00
commit 7dbe96017e
No known key found for this signature in database
3245 changed files with 126000 additions and 40976 deletions

View file

@ -11,3 +11,8 @@
# refactor: Run lintfix (no-changelog) (#7537)
62c096710fab2f7e886518abdbded34b55e93f62
# refactor: Move test files alongside tested files (#11504)
7e58fc4fec468aca0b45d5bfe6150e1af632acbc
f32b13c6ed078be042a735bc8621f27e00dc3116

View file

@ -1,18 +1,14 @@
version: '3.9'
services:
mysql:
image: mysql:5.7
mariadb:
image: mariadb:10.9
environment:
- MYSQL_DATABASE=n8n
- MYSQL_ROOT_PASSWORD=password
- MARIADB_DATABASE=n8n
- MARIADB_ROOT_PASSWORD=password
- MARIADB_MYSQL_LOCALHOST_USER=true
ports:
- 3306:3306
ulimits:
nproc: 65535
nofile:
soft: 26677
hard: 46677
tmpfs:
- /var/lib/mysql
postgres:
image: postgres:16
@ -23,3 +19,5 @@ services:
- POSTGRES_PASSWORD=password
ports:
- 5432:5432
tmpfs:
- /var/lib/postgresql/data

View file

@ -11,6 +11,8 @@ Photos and videos are recommended.
Include links to **Linear ticket** or Github issue or Community forum post.
Important in order to close *automatically* and provide context to reviewers.
-->
<!-- Use "closes #<issue-number>", "fixes #<issue-number>", or "resolves #<issue-number>" to automatically close issues when the PR is merged. -->
## Review / Merge checklist

View file

@ -1,103 +0,0 @@
import { readFile } from 'fs/promises';
import path from 'path';
import util from 'util';
import { exec } from 'child_process';
import { glob } from 'glob';
import ts from 'typescript';
const execAsync = util.promisify(exec);
const filterAsync = async (asyncPredicate, arr) => {
const filterResults = await Promise.all(
arr.map(async (item) => ({
item,
shouldKeep: await asyncPredicate(item),
})),
);
return filterResults.filter(({ shouldKeep }) => shouldKeep).map(({ item }) => item);
};
const isAbstractClass = (node) => {
if (ts.isClassDeclaration(node)) {
return (
node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword) || false
);
}
return false;
};
const isAbstractMethod = (node) => {
return (
ts.isMethodDeclaration(node) &&
Boolean(node.modifiers?.find((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword))
);
};
// Function to check if a file has a function declaration, function expression, object method or class
const hasFunctionOrClass = async (filePath) => {
const fileContent = await readFile(filePath, 'utf-8');
const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);
let hasFunctionOrClass = false;
const visit = (node) => {
if (
ts.isFunctionDeclaration(node) ||
ts.isFunctionExpression(node) ||
ts.isArrowFunction(node) ||
(ts.isMethodDeclaration(node) && !isAbstractMethod(node)) ||
(ts.isClassDeclaration(node) && !isAbstractClass(node))
) {
hasFunctionOrClass = true;
}
node.forEachChild(visit);
};
visit(sourceFile);
return hasFunctionOrClass;
};
const main = async () => {
// Run a git command to get a list of all changed files in the branch (branch has to be up to date with master)
const changedFiles = await execAsync(
'git diff --name-only --diff-filter=d origin/master..HEAD',
).then(({ stdout }) => stdout.trim().split('\n').filter(Boolean));
// Get all .spec.ts and .test.ts files from the packages
const specAndTestTsFiles = await glob('packages/*/**/{test,__tests__}/**/*.{spec,test}.ts');
const specAndTestTsFilesNames = specAndTestTsFiles.map((file) =>
path.parse(file).name.replace(/\.(test|spec)/, ''),
);
// Filter out the .ts and .vue files from the changed files
const changedVueFiles = changedFiles.filter((file) => file.endsWith('.vue'));
// .ts files with any kind of function declaration or class and not in any of the test folders
const changedTsFilesWithFunction = await filterAsync(
async (filePath) =>
filePath.endsWith('.ts') &&
!(await glob('packages/*/**/{test,__tests__}/*.ts')).includes(filePath) &&
(await hasFunctionOrClass(filePath)),
changedFiles,
);
// For each .ts or .vue file, check if there's a corresponding .test.ts or .spec.ts file in the repository
const missingTests = changedVueFiles
.concat(changedTsFilesWithFunction)
.reduce((filesList, nextFile) => {
const fileName = path.parse(nextFile).name;
if (!specAndTestTsFilesNames.includes(fileName)) {
filesList.push(nextFile);
}
return filesList;
}, []);
if (missingTests.length) {
console.error(`Missing tests for:\n${missingTests.join('\n')}`);
process.exit(1);
}
};
main();

View file

@ -7,7 +7,6 @@
"p-limit": "3.1.0",
"picocolors": "1.0.1",
"semver": "7.5.4",
"tempfile": "5.0.0",
"typescript": "*"
"tempfile": "5.0.0"
}
}

View file

@ -1,30 +0,0 @@
name: Check Test Files
on:
pull_request:
branches:
- '**'
- '!release/*'
pull_request_target:
branches:
- master
jobs:
check-tests:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
- name: Use Node.js
uses: actions/setup-node@v4.0.2
with:
node-version: 20.x
- run: npm install --prefix=.github/scripts --no-package-lock
- name: Check for test files
run: node .github/scripts/check-tests.mjs

View file

@ -1,6 +1,8 @@
name: Chromatic
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
pull_request_review:
types: [submitted]
@ -65,11 +67,12 @@ jobs:
continue-on-error: true
with:
workingDir: packages/design-system
onlyChanged: true
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: false
- name: Success comment
if: steps.chromatic_tests.outcome == 'success'
if: steps.chromatic_tests.outcome == 'success' && github.ref != 'refs/heads/master'
uses: peter-evans/create-or-update-comment@v4.0.0
with:
issue-number: ${{ github.event.pull_request.number }}
@ -79,7 +82,7 @@ jobs:
:white_check_mark: No visual regressions found.
- name: Fail comment
if: steps.chromatic_tests.outcome != 'success'
if: steps.chromatic_tests.outcome != 'success' && github.ref != 'refs/heads/master'
uses: peter-evans/create-or-update-comment@v4.0.0
with:
issue-number: ${{ github.event.pull_request.number }}

View file

@ -8,6 +8,7 @@ on:
paths:
- packages/cli/src/databases/**
- .github/workflows/ci-postgres-mysql.yml
- .github/docker-compose.yml
pull_request_review:
types: [submitted]
@ -71,8 +72,8 @@ jobs:
working-directory: packages/cli
run: pnpm jest
mysql:
name: MySQL
mariadb:
name: MariaDB
runs-on: ubuntu-latest
needs: build
timeout-minutes: 20
@ -96,16 +97,16 @@ jobs:
path: ./packages/**/dist
key: ${{ github.sha }}:db-tests
- name: Start MySQL
- name: Start MariaDB
uses: isbang/compose-action@v2.0.0
with:
compose-file: ./.github/docker-compose.yml
services: |
mysql
mariadb
- name: Test MySQL
- name: Test MariaDB
working-directory: packages/cli
run: pnpm test:mysql --testTimeout 20000
run: pnpm test:mariadb --testTimeout 30000
postgres:
name: Postgres
@ -147,7 +148,7 @@ jobs:
notify-on-failure:
name: Notify Slack on failure
runs-on: ubuntu-latest
needs: [mysql, postgres]
needs: [mariadb, postgres]
steps:
- name: Notify Slack on failure
uses: act10ns/slack@v2.0.0
@ -156,4 +157,4 @@ jobs:
status: ${{ job.status }}
channel: '#alerts-build'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: Postgres or MySQL tests failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
message: Postgres or MariaDB tests failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})

View file

@ -49,6 +49,9 @@ jobs:
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
cacheKey: ${{ github.sha }}-base:build
collectCoverage: true
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
lint:
name: Lint

View file

@ -41,6 +41,11 @@ on:
description: 'PR number to run tests for.'
required: false
type: number
node_view_version:
description: 'Node View version to run tests with.'
required: false
default: '1'
type: string
secrets:
CYPRESS_RECORD_KEY:
description: 'Cypress record key.'
@ -160,6 +165,7 @@ jobs:
spec: '${{ inputs.spec }}'
env:
NODE_OPTIONS: --dns-result-order=ipv4first
CYPRESS_NODE_VIEW_VERSION: ${{ inputs.node_view_version }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
E2E_TESTS: true

View file

@ -27,6 +27,11 @@ on:
description: 'URL to call after workflow is done.'
required: false
default: ''
node_view_version:
description: 'Node View version to run tests with.'
required: false
default: '1'
type: string
jobs:
calls-start-url:
@ -46,6 +51,7 @@ jobs:
branch: ${{ github.event.inputs.branch || 'master' }}
user: ${{ github.event.inputs.user || 'PR User' }}
spec: ${{ github.event.inputs.spec || 'e2e/*' }}
node_view_version: ${{ github.event.inputs.node_view_version || '1' }}
secrets:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

View file

@ -38,6 +38,12 @@ jobs:
- name: Build
run: pnpm build
- name: Cache build artifacts
uses: actions/cache/save@v4.0.0
with:
path: ./packages/**/dist
key: ${{ github.sha }}-release:build
- name: Dry-run publishing
run: pnpm publish -r --no-git-checks --dry-run
@ -119,6 +125,40 @@ jobs:
makeLatest: false
body: ${{github.event.pull_request.body}}
create-sentry-release:
name: Create a Sentry Release
needs: [publish-to-npm, publish-to-docker-hub]
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
timeout-minutes: 5
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
steps:
- uses: actions/checkout@v4.1.1
- name: Restore cached build artifacts
uses: actions/cache/restore@v4.0.0
with:
path: ./packages/**/dist
key: ${{ github.sha }}-release:build
- name: Create a frontend release
uses: getsentry/action-release@v1.7.0
continue-on-error: true
with:
projects: ${{ secrets.SENTRY_FRONTEND_PROJECT }}
version: ${{ needs.publish-to-npm.outputs.release }}
sourcemaps: packages/editor-ui/dist
- name: Create a backend release
uses: getsentry/action-release@v1.7.0
continue-on-error: true
with:
projects: ${{ secrets.SENTRY_BACKEND_PROJECT }}
version: ${{ needs.publish-to-npm.outputs.release }}
sourcemaps: packages/cli/dist packages/core/dist packages/nodes-base/dist packages/@n8n/n8n-nodes-langchain/dist
trigger-release-note:
name: Trigger a release note
needs: [publish-to-npm, create-github-release]

View file

@ -4,18 +4,70 @@ on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
pull_request:
paths:
- packages/core/package.json
- packages/nodes-base/package.json
- packages/@n8n/nodes-langchain/package.json
- .github/workflows/test-workflows.yml
pull_request_review:
types: [submitted]
jobs:
run-test-workflows:
build:
name: Install & Build
runs-on: ubuntu-latest
timeout-minutes: 30
if: github.event_name != 'pull_request_review' || startsWith(github.event.pull_request.base.ref, 'release/')
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- uses: actions/checkout@v4.1.1
- run: corepack enable
- uses: actions/setup-node@v4.0.2
with:
path: n8n
node-version: 20.x
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build Backend
run: pnpm build:backend
- name: Cache build artifacts
uses: actions/cache/save@v4.0.0
with:
path: ./packages/**/dist
key: ${{ github.sha }}:workflow-tests
run-test-workflows:
name: Workflow Tests
runs-on: ubuntu-latest
needs: build
timeout-minutes: 10
steps:
- uses: actions/checkout@v4.1.1
- run: corepack enable
- uses: actions/setup-node@v4.0.2
with:
node-version: 20.x
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Restore cached build artifacts
uses: actions/cache/restore@v4.0.0
with:
path: ./packages/**/dist
key: ${{ github.sha }}:workflow-tests
- name: Install OS dependencies
run: |
sudo apt update -y
echo 'tzdata tzdata/Areas select Europe' | sudo debconf-set-selections
echo 'tzdata tzdata/Zones/Europe select Paris' | sudo debconf-set-selections
DEBIAN_FRONTEND="noninteractive" sudo apt-get install -y graphicsmagick
- name: Checkout workflows repo
uses: actions/checkout@v4.1.1
@ -23,77 +75,34 @@ jobs:
repository: n8n-io/test-workflows
path: test-workflows
- run: corepack enable
working-directory: n8n
- uses: actions/setup-node@v4.0.2
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'n8n/pnpm-lock.yaml'
- name: Install dependencies
run: |
sudo apt update -y
echo 'tzdata tzdata/Areas select Europe' | sudo debconf-set-selections
echo 'tzdata tzdata/Zones/Europe select Paris' | sudo debconf-set-selections
DEBIAN_FRONTEND="noninteractive" sudo apt-get install -y graphicsmagick
shell: bash
- name: pnpm install and build
working-directory: n8n
run: |
pnpm install --frozen-lockfile
pnpm build:backend
shell: bash
- name: Import credentials
run: n8n/packages/cli/bin/n8n import:credentials --input=test-workflows/credentials.json
shell: bash
run: packages/cli/bin/n8n import:credentials --input=test-workflows/credentials.json
env:
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
- name: Import workflows
run: n8n/packages/cli/bin/n8n import:workflow --separate --input=test-workflows/workflows
shell: bash
run: packages/cli/bin/n8n import:workflow --separate --input=test-workflows/workflows
env:
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
- name: Copy static assets
run: |
cp n8n/assets/n8n-logo.png /tmp/n8n-logo.png
cp n8n/assets/n8n-screenshot.png /tmp/n8n-screenshot.png
cp assets/n8n-logo.png /tmp/n8n-logo.png
cp assets/n8n-screenshot.png /tmp/n8n-screenshot.png
cp test-workflows/testData/pdfs/*.pdf /tmp/
shell: bash
- name: Run tests
run: n8n/packages/cli/bin/n8n executeBatch --shallow --skipList=test-workflows/skipList.txt --githubWorkflow --shortOutput --concurrency=16 --compare=test-workflows/snapshots
shell: bash
run: packages/cli/bin/n8n executeBatch --shallow --skipList=test-workflows/skipList.txt --githubWorkflow --shortOutput --concurrency=16 --compare=test-workflows/snapshots
id: tests
env:
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
SKIP_STATISTICS_EVENTS: true
DB_SQLITE_POOL_SIZE: 4
# -
# name: Export credentials
# if: always()
# run: n8n/packages/cli/bin/n8n export:credentials --output=test-workflows/credentials.json --all --pretty
# shell: bash
# env:
# N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
# -
# name: Commit and push credential changes
# if: always()
# run: |
# cd test-workflows
# git config --global user.name 'n8n test bot'
# git config --global user.email 'n8n-test-bot@users.noreply.github.com'
# git commit -am "Automated credential update"
# git push --force --quiet "https://janober:${{ secrets.TOKEN }}@github.com/n8n-io/test-workflows.git" main:main
N8N_SENTRY_DSN: ${{secrets.CI_SENTRY_DSN}}
- name: Notify Slack on failure
uses: act10ns/slack@v2.0.0
if: failure()
if: failure() && github.ref == 'refs/heads/master'
with:
status: ${{ job.status }}
channel: '#alerts-build'

View file

@ -49,7 +49,6 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Setup build cache
if: inputs.collectCoverage != true
uses: rharkor/caching-for-turbo@v1.5
- name: Build

View file

@ -7,6 +7,7 @@
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"mjmlio.vscode-mjml",
"Vue.volar"
"Vue.volar",
"vitest.explorer"
]
}

4
.vscode/launch.json vendored
View file

@ -47,9 +47,7 @@
"request": "launch",
"skipFiles": ["<node_internals>/**"],
"type": "node",
"env": {
// "N8N_PORT": "5679",
},
"envFile": "${workspaceFolder}/.env",
"outputCapture": "std",
"killBehavior": "polite"
},

View file

@ -1,3 +1,423 @@
# [1.73.0](https://github.com/n8n-io/n8n/compare/n8n@1.72.0...n8n@1.73.0) (2024-12-19)
### Bug Fixes
* **core:** Ensure runners do not throw on unsupported console methods ([#12167](https://github.com/n8n-io/n8n/issues/12167)) ([57c6a61](https://github.com/n8n-io/n8n/commit/57c6a6167dd2b30f0082a416daefce994ecad33a))
* **core:** Fix `$getWorkflowStaticData` on task runners ([#12153](https://github.com/n8n-io/n8n/issues/12153)) ([b479f14](https://github.com/n8n-io/n8n/commit/b479f14ef5012551b823bea5d2ffbddedfd50a77))
* **core:** Fix binary data helpers (like `prepareBinaryData`) with task runner ([#12259](https://github.com/n8n-io/n8n/issues/12259)) ([0f1461f](https://github.com/n8n-io/n8n/commit/0f1461f2d5d7ec34236ed7fcec3e2f9ee7eb73c4))
* **core:** Fix race condition in AI tool invocation with multiple items from the parent ([#12169](https://github.com/n8n-io/n8n/issues/12169)) ([dce0c58](https://github.com/n8n-io/n8n/commit/dce0c58f8605c33fc50ec8aa422f0fb5eee07637))
* **core:** Fix serialization of circular json with task runner ([#12288](https://github.com/n8n-io/n8n/issues/12288)) ([a99d726](https://github.com/n8n-io/n8n/commit/a99d726f42d027b64f94eda0d385b597c5d5be2e))
* **core:** Upgrade nanoid to address CVE-2024-55565 ([#12171](https://github.com/n8n-io/n8n/issues/12171)) ([8c0bd02](https://github.com/n8n-io/n8n/commit/8c0bd0200c386b122f495c453ccc97a001e4729c))
* **editor:** Add new create first project CTA ([#12189](https://github.com/n8n-io/n8n/issues/12189)) ([878b419](https://github.com/n8n-io/n8n/commit/878b41904d76eda3ee230f850127b4d56993de24))
* **editor:** Fix canvas ready opacity transition on new canvas ([#12264](https://github.com/n8n-io/n8n/issues/12264)) ([5d33a6b](https://github.com/n8n-io/n8n/commit/5d33a6ba8a2bccea097402fd04c0e2b00e423e76))
* **editor:** Fix rendering of code-blocks in sticky notes ([#12227](https://github.com/n8n-io/n8n/issues/12227)) ([9b59035](https://github.com/n8n-io/n8n/commit/9b5903524b95bd21d5915908780942790cf88d27))
* **editor:** Fix sticky color picker getting covered by nodes on new canvas ([#12263](https://github.com/n8n-io/n8n/issues/12263)) ([27bd3c8](https://github.com/n8n-io/n8n/commit/27bd3c85b3a4ddcf763a543b232069bb108130cf))
* **editor:** Improve commit modal user facing messaging ([#12161](https://github.com/n8n-io/n8n/issues/12161)) ([ad39243](https://github.com/n8n-io/n8n/commit/ad392439826b17bd0b84f981e0958d88f09e7fe9))
* **editor:** Prevent connection line from showing when clicking the plus button of a node ([#12265](https://github.com/n8n-io/n8n/issues/12265)) ([9180b46](https://github.com/n8n-io/n8n/commit/9180b46b52302b203eecf3bb81c3f2132527a1e6))
* **editor:** Prevent stickies from being edited in preview mode in the new canvas ([#12222](https://github.com/n8n-io/n8n/issues/12222)) ([6706dcd](https://github.com/n8n-io/n8n/commit/6706dcdf72d54f33c1cf4956602c3a64a1578826))
* **editor:** Reduce cases for Auto-Add of ChatTrigger for AI Agents ([#12154](https://github.com/n8n-io/n8n/issues/12154)) ([365e82d](https://github.com/n8n-io/n8n/commit/365e82d2008dff2f9c91664ee04d7a78363a8b30))
* **editor:** Remove invalid connections after node handles change ([#12247](https://github.com/n8n-io/n8n/issues/12247)) ([6330bec](https://github.com/n8n-io/n8n/commit/6330bec4db0175b558f2747837323fdbb25b634a))
* **editor:** Set dangerouslyUseHTMLString in composable ([#12280](https://github.com/n8n-io/n8n/issues/12280)) ([6ba91b5](https://github.com/n8n-io/n8n/commit/6ba91b5e1ed197c67146347a6f6e663ecdf3de48))
* **editor:** Set RunData outputIndex based on incoming data ([#12182](https://github.com/n8n-io/n8n/issues/12182)) ([dc4261a](https://github.com/n8n-io/n8n/commit/dc4261ae7eca6cf277404cd514c90fad42f14ae0))
* **editor:** Update the universal create button interaction ([#12105](https://github.com/n8n-io/n8n/issues/12105)) ([5300e0a](https://github.com/n8n-io/n8n/commit/5300e0ac45bf832b3d2957198a49a1c687f3fe1f))
* **Elasticsearch Node:** Fix issue stopping search queries being sent ([#11464](https://github.com/n8n-io/n8n/issues/11464)) ([388a83d](https://github.com/n8n-io/n8n/commit/388a83dfbdc6ac301e4df704666df9f09fb7d0b3))
* **Extract from File Node:** Detect file encoding ([#12081](https://github.com/n8n-io/n8n/issues/12081)) ([92af245](https://github.com/n8n-io/n8n/commit/92af245d1aab5bfad8618fda69b2405f5206875d))
* **Github Node:** Fix fetch of file names with ? character ([#12206](https://github.com/n8n-io/n8n/issues/12206)) ([39462ab](https://github.com/n8n-io/n8n/commit/39462abe1fde7e82b5e5b8f3ceebfcadbfd7c925))
* **Invoice Ninja Node:** Fix actions for bank transactions ([#11511](https://github.com/n8n-io/n8n/issues/11511)) ([80eea49](https://github.com/n8n-io/n8n/commit/80eea49cf0bf9db438eb85af7cd22aeb11fbfed2))
* **Linear Node:** Fix issue with error handling ([#12191](https://github.com/n8n-io/n8n/issues/12191)) ([b8eae5f](https://github.com/n8n-io/n8n/commit/b8eae5f28a7d523195f4715cd8da77b3a884ae4c))
* **MongoDB Node:** Fix checks on projection feature call ([#10563](https://github.com/n8n-io/n8n/issues/10563)) ([58bab46](https://github.com/n8n-io/n8n/commit/58bab461c4c5026b2ca5ea143cbcf98bf3a4ced8))
* **Postgres Node:** Allow users to wrap strings with $$ ([#12034](https://github.com/n8n-io/n8n/issues/12034)) ([0c15e30](https://github.com/n8n-io/n8n/commit/0c15e30778cc5cb10ed368df144d6fbb2504ec70))
* **Redis Node:** Add support for username auth ([#12274](https://github.com/n8n-io/n8n/issues/12274)) ([64c0414](https://github.com/n8n-io/n8n/commit/64c0414ef28acf0f7ec42b4b0bb21cbf2921ebe7))
### Features
* Add solarwinds ipam credentials ([#12005](https://github.com/n8n-io/n8n/issues/12005)) ([882484e](https://github.com/n8n-io/n8n/commit/882484e8ee7d1841d5d600414ca48e9915abcfa8))
* Add SolarWinds Observability node credentials ([#11805](https://github.com/n8n-io/n8n/issues/11805)) ([e8a5db5](https://github.com/n8n-io/n8n/commit/e8a5db5beb572edbb61dd9100b70827ccc4cca58))
* **AI Agent Node:** Update descriptions and titles for Chat Trigger options in AI Agents and Memory ([#12155](https://github.com/n8n-io/n8n/issues/12155)) ([07a6ae1](https://github.com/n8n-io/n8n/commit/07a6ae11b3291c1805553d55ba089fe8dd919fd8))
* **API:** Exclude pinned data from workflows ([#12261](https://github.com/n8n-io/n8n/issues/12261)) ([e0dc385](https://github.com/n8n-io/n8n/commit/e0dc385f8bc8ee13fbc5bbf35e07654e52b193e9))
* **editor:** Params pane collection improvements ([#11607](https://github.com/n8n-io/n8n/issues/11607)) ([6e44c71](https://github.com/n8n-io/n8n/commit/6e44c71c9ca82cce20eb55bb9003930bbf66a16c))
* **editor:** Support adding nodes via drag and drop from node creator on new canvas ([#12197](https://github.com/n8n-io/n8n/issues/12197)) ([1bfd9c0](https://github.com/n8n-io/n8n/commit/1bfd9c0e913f3eefc4593f6c344db1ae1f6e4df4))
* **Facebook Graph API Node:** Update node to support API v21.0 ([#12116](https://github.com/n8n-io/n8n/issues/12116)) ([14c33f6](https://github.com/n8n-io/n8n/commit/14c33f666fe92f7173e4f471fb478e629e775c62))
* **Linear Trigger Node:** Add support for admin scope ([#12211](https://github.com/n8n-io/n8n/issues/12211)) ([410ea9a](https://github.com/n8n-io/n8n/commit/410ea9a2ef2e14b5e8e4493e5db66cfc2290d8f6))
* **MailerLite Node:** Update node to support new api ([#11933](https://github.com/n8n-io/n8n/issues/11933)) ([d6b8e65](https://github.com/n8n-io/n8n/commit/d6b8e65abeb411f86538c1630dcce832ee0846a9))
* Send and wait operation - freeText and customForm response types ([#12106](https://github.com/n8n-io/n8n/issues/12106)) ([e98c7f1](https://github.com/n8n-io/n8n/commit/e98c7f160b018243dc88490d46fb1047a4d7fcdc))
### Performance Improvements
* **editor:** SchemaView performance improvement by ≈90% 🚀 ([#12180](https://github.com/n8n-io/n8n/issues/12180)) ([6a58309](https://github.com/n8n-io/n8n/commit/6a5830959f5fb493a4119869b8298d8ed702c84a))
# [1.72.0](https://github.com/n8n-io/n8n/compare/n8n@1.71.0...n8n@1.72.0) (2024-12-11)
### Bug Fixes
* Allow disabling MFA with recovery codes ([#12014](https://github.com/n8n-io/n8n/issues/12014)) ([95d56fe](https://github.com/n8n-io/n8n/commit/95d56fee8d0168b75fca6dcf41702d2f10c930a8))
* Chat triggers don't work with the new partial execution flow ([#11952](https://github.com/n8n-io/n8n/issues/11952)) ([2b6a72f](https://github.com/n8n-io/n8n/commit/2b6a72f1289c01145edf2b88e5027d2b9b2ed624))
* **core:** Execute nodes after loops correctly with the new partial execution flow ([#11978](https://github.com/n8n-io/n8n/issues/11978)) ([891dd7f](https://github.com/n8n-io/n8n/commit/891dd7f995c78a2355a049b7ced981a5f6b1c40c))
* **core:** Fix support for multiple invocation of AI tools ([#12141](https://github.com/n8n-io/n8n/issues/12141)) ([c572c06](https://github.com/n8n-io/n8n/commit/c572c0648ca5b644b222157b3cabac9c05704a84))
* **core:** Make sure task runner exits ([#12123](https://github.com/n8n-io/n8n/issues/12123)) ([c5effca](https://github.com/n8n-io/n8n/commit/c5effca7d47a713f157eea21d7892002e9ab7283))
* **core:** Remove run data of nodes unrelated to the current partial execution ([#12099](https://github.com/n8n-io/n8n/issues/12099)) ([c4e4d37](https://github.com/n8n-io/n8n/commit/c4e4d37a8785d1a4bcd376cb1c49b82a80aa4391))
* **core:** Return homeProject when filtering workflows by project id ([#12077](https://github.com/n8n-io/n8n/issues/12077)) ([efafeed](https://github.com/n8n-io/n8n/commit/efafeed33482100a23fa0163a53b9ce93cd6b2c3))
* **editor:** Don't reset all Parameter Inputs when switched to read-only ([#12063](https://github.com/n8n-io/n8n/issues/12063)) ([706702d](https://github.com/n8n-io/n8n/commit/706702dff8da3c2e949e2c98dd5b34b299a1f17c))
* **editor:** Fix canvas panning using `Control` + `Left Mouse Button` on Windows ([#12104](https://github.com/n8n-io/n8n/issues/12104)) ([43009b6](https://github.com/n8n-io/n8n/commit/43009b6aa820f24b9e6f519e7a45592aa21db03e))
* **editor:** Fix Nodeview.v2 reinitialise based on route changes ([#12062](https://github.com/n8n-io/n8n/issues/12062)) ([b1f8663](https://github.com/n8n-io/n8n/commit/b1f866326574974eb2936e6b02771346e83e7137))
* **editor:** Fix svg background pattern rendering on safari ([#12079](https://github.com/n8n-io/n8n/issues/12079)) ([596f221](https://github.com/n8n-io/n8n/commit/596f22103c01e14063ebb2388c4dabf4714d37c6))
* **editor:** Fix switching from v2 to v1 ([#12050](https://github.com/n8n-io/n8n/issues/12050)) ([5c76de3](https://github.com/n8n-io/n8n/commit/5c76de324c2e25b0d8b74cdab79f04aa616d8c4f))
* **editor:** Improvements to the commit modal ([#12031](https://github.com/n8n-io/n8n/issues/12031)) ([4fe1952](https://github.com/n8n-io/n8n/commit/4fe1952e2fb3379d95da42a7bb531851af6d0094))
* **editor:** Load node types in demo and preview modes ([#12048](https://github.com/n8n-io/n8n/issues/12048)) ([4ac5f95](https://github.com/n8n-io/n8n/commit/4ac5f9527bbec382a65ed3f1d9c41d6948c154e3))
* **editor:** Polyfill crypto.randomUUID ([#12052](https://github.com/n8n-io/n8n/issues/12052)) ([0537524](https://github.com/n8n-io/n8n/commit/0537524c3e45d7633415c7a9175a3857ad52cd58))
* **editor:** Redirect Settings to the proper sub page depending on the instance type (cloud or not) ([#12053](https://github.com/n8n-io/n8n/issues/12053)) ([a16d006](https://github.com/n8n-io/n8n/commit/a16d006f893cac927d674fa447b08c1205b67c54))
* **editor:** Render sanitized HTML content in toast messages ([#12139](https://github.com/n8n-io/n8n/issues/12139)) ([0468945](https://github.com/n8n-io/n8n/commit/0468945c99f083577c4cc71f671b4b950f6aeb86))
* **editor:** Universal button snags ([#11974](https://github.com/n8n-io/n8n/issues/11974)) ([956b11a](https://github.com/n8n-io/n8n/commit/956b11a560528336a74be40f722fa05bf3cca94d))
* **editor:** Update concurrency UI considering different types of instances ([#12068](https://github.com/n8n-io/n8n/issues/12068)) ([fa572bb](https://github.com/n8n-io/n8n/commit/fa572bbca4397b1cc42668530497444630ed17eb))
* **FTP Node:** Fix issue with creating folders on rename ([#9340](https://github.com/n8n-io/n8n/issues/9340)) ([eb7d593](https://github.com/n8n-io/n8n/commit/eb7d5934ef8bc6e999d6de4c0b8025ce175df5dd))
* **n8n Form Node:** Completion page display if EXECUTIONS_DATA_SAVE_ON_SUCCESS=none ([#11869](https://github.com/n8n-io/n8n/issues/11869)) ([f4c2523](https://github.com/n8n-io/n8n/commit/f4c252341985fe03927a2fd5d60ba846ec3dfc77))
* **OpenAI Node:** Allow updating assistant files ([#12042](https://github.com/n8n-io/n8n/issues/12042)) ([7b20f8a](https://github.com/n8n-io/n8n/commit/7b20f8aaa8befd19dbad0af3bf1b881342c1fca5))
### Features
* **AI Transform Node:** Reduce payload size ([#11965](https://github.com/n8n-io/n8n/issues/11965)) ([d8ca8de](https://github.com/n8n-io/n8n/commit/d8ca8de13a4cbb856696873bdb56c66b12a5b027))
* **core:** Add option to filter for empty variables ([#12112](https://github.com/n8n-io/n8n/issues/12112)) ([a63f0e8](https://github.com/n8n-io/n8n/commit/a63f0e878e21da9924451e2679939209b34b6583))
* **core:** Cancel runner task on timeout in external mode ([#12101](https://github.com/n8n-io/n8n/issues/12101)) ([addb4fa](https://github.com/n8n-io/n8n/commit/addb4fa352c88d856e463bb2b7001173c4fd6a7d))
* **core:** Parent workflows should wait for sub-workflows to finish ([#11985](https://github.com/n8n-io/n8n/issues/11985)) ([60b3dcc](https://github.com/n8n-io/n8n/commit/60b3dccf9317da6f3013be35a78ce21d0416ad80))
* **editor:** Implementing the `Easy AI Workflow` experiment ([#12043](https://github.com/n8n-io/n8n/issues/12043)) ([67ed1d2](https://github.com/n8n-io/n8n/commit/67ed1d2c3c2e69d5a96daf7de2795c02f5d8f15b))
* **Redis Node:** Add support for continue on fail / error output branch ([#11714](https://github.com/n8n-io/n8n/issues/11714)) ([ed35958](https://github.com/n8n-io/n8n/commit/ed359586c88a7662f4d94d58c5a87cf91d027ab9))
# [1.71.0](https://github.com/n8n-io/n8n/compare/n8n@1.70.0...n8n@1.71.0) (2024-12-04)
### Bug Fixes
* **core:** Fix push for waiting executions ([#11984](https://github.com/n8n-io/n8n/issues/11984)) ([8d71307](https://github.com/n8n-io/n8n/commit/8d71307da0398e7e39bf53e8e1cfa21ac1ceaf69))
* **core:** Improve header parameter parsing on http client responses ([#11953](https://github.com/n8n-io/n8n/issues/11953)) ([41e9e39](https://github.com/n8n-io/n8n/commit/41e9e39b5b53ecd9d8d1b385df65a26ecb9bccd8))
* **core:** Opt-out from optimizations if `$item` is used ([#12036](https://github.com/n8n-io/n8n/issues/12036)) ([872535a](https://github.com/n8n-io/n8n/commit/872535a40c85dcfad3a4b27c57c026ae003f562f))
* **core:** Use the configured timezone in task runner ([#12032](https://github.com/n8n-io/n8n/issues/12032)) ([2e6845a](https://github.com/n8n-io/n8n/commit/2e6845afcbc30dff73c3f3f15f21278cab397387))
* **core:** Validate node name when creating `NodeOperationErrror` ([#11999](https://github.com/n8n-io/n8n/issues/11999)) ([e68c9da](https://github.com/n8n-io/n8n/commit/e68c9da30c31cd5f994cb01ce759192562bfbd40))
* **editor:** Add execution concurrency info and paywall ([#11847](https://github.com/n8n-io/n8n/issues/11847)) ([57d3269](https://github.com/n8n-io/n8n/commit/57d3269e400ee4e7e3636614870ebdfdb0aa8c1d))
* **editor:** Fix bug causing connection lines to disappear when hovering stickies ([#11950](https://github.com/n8n-io/n8n/issues/11950)) ([439a1cc](https://github.com/n8n-io/n8n/commit/439a1cc4f39243e91715b21a84b8e7266ce872cd))
* **editor:** Fix canvas keybindings using splitter keys such as zooming using `+` key ([#12022](https://github.com/n8n-io/n8n/issues/12022)) ([6af9c82](https://github.com/n8n-io/n8n/commit/6af9c82af6020e99d61e442ee9c2d40761baf027))
* **editor:** Fix community check ([#11979](https://github.com/n8n-io/n8n/issues/11979)) ([af0398a](https://github.com/n8n-io/n8n/commit/af0398a5e3a8987c01c7112e6f689b35e1ef92fe))
* **editor:** Fix copy/paste keyboard events in canvas chat ([#12004](https://github.com/n8n-io/n8n/issues/12004)) ([967340a](https://github.com/n8n-io/n8n/commit/967340a2938a79c89319121bf57a8d654f88e06c))
* **editor:** Fix node showing as successful if errors exists on subsequent runs ([#12019](https://github.com/n8n-io/n8n/issues/12019)) ([8616b17](https://github.com/n8n-io/n8n/commit/8616b17cc6c305da69bbb54fd56ab7cb34213f7c))
* **editor:** Fix pin data showing up in production executions on new canvas ([#11951](https://github.com/n8n-io/n8n/issues/11951)) ([5f6f8a1](https://github.com/n8n-io/n8n/commit/5f6f8a1bddfd76b586c08da821e8b59070f449fc))
* **editor:** Handle source control initialization to prevent UI form crashing ([#11776](https://github.com/n8n-io/n8n/issues/11776)) ([6be8e86](https://github.com/n8n-io/n8n/commit/6be8e86c45bd64d000bc95d2ef2d68220e930c02))
* **editor:** Implement dirty nodes for partial executions ([#11739](https://github.com/n8n-io/n8n/issues/11739)) ([b8da4ff](https://github.com/n8n-io/n8n/commit/b8da4ff9edb0fbb0093c4c41fe11f8e67b696ca3))
* **editor:** Resolve going back from Settings ([#11958](https://github.com/n8n-io/n8n/issues/11958)) ([d74423c](https://github.com/n8n-io/n8n/commit/d74423c75198d38d0d99a1879051b5e964ecae74))
* **editor:** Unify executions card label color ([#11949](https://github.com/n8n-io/n8n/issues/11949)) ([fc79718](https://github.com/n8n-io/n8n/commit/fc797188d63e87df34b3a153eb4a0d0b7361b3f5))
* **editor:** Use optional chaining for all members in execution data when using the debug feature ([#12024](https://github.com/n8n-io/n8n/issues/12024)) ([67aa0c9](https://github.com/n8n-io/n8n/commit/67aa0c9107bda16b1cb6d273e17c3cde77035f51))
* **GraphQL Node:** Throw error if GraphQL variables are not objects or strings ([#11904](https://github.com/n8n-io/n8n/issues/11904)) ([85f30b2](https://github.com/n8n-io/n8n/commit/85f30b27ae282da58a25186d13ff17196dcd7d9c))
* **HTTP Request Node:** Use iconv-lite to decode http responses, to support more encoding types ([#11930](https://github.com/n8n-io/n8n/issues/11930)) ([461b39c](https://github.com/n8n-io/n8n/commit/461b39c5df5dd446cb8ceef469b204c7c5111229))
* Load workflows with unconnected Switch outputs ([#12020](https://github.com/n8n-io/n8n/issues/12020)) ([abc851c](https://github.com/n8n-io/n8n/commit/abc851c0cff298607a0dc2f2882aa17136898f45))
* **n8n Form Node:** Use https to load google fonts ([#11948](https://github.com/n8n-io/n8n/issues/11948)) ([eccd924](https://github.com/n8n-io/n8n/commit/eccd924f5e8dbe59e37099d1a6fbe8866fef55bf))
* **Telegram Trigger Node:** Fix header secret check ([#12018](https://github.com/n8n-io/n8n/issues/12018)) ([f16de4d](https://github.com/n8n-io/n8n/commit/f16de4db01c0496205635a3203a44098e7908453))
* **Webflow Node:** Fix issue with pagination in v2 node ([#11934](https://github.com/n8n-io/n8n/issues/11934)) ([1eb94bc](https://github.com/n8n-io/n8n/commit/1eb94bcaf54d9e581856ce0b87253e1c28fa68e2))
* **Webflow Node:** Fix issue with publishing items ([#11982](https://github.com/n8n-io/n8n/issues/11982)) ([0a8a57e](https://github.com/n8n-io/n8n/commit/0a8a57e4ec8081ab1a53f36d686b3d5dcaae2476))
### Features
* **AI Transform Node:** Node Prompt improvements ([#11611](https://github.com/n8n-io/n8n/issues/11611)) ([40a7445](https://github.com/n8n-io/n8n/commit/40a7445f0873af2cdbd10b12bd691c07a43e27cc))
* **Code Node:** Warning if pairedItem absent or could not be auto mapped ([#11737](https://github.com/n8n-io/n8n/issues/11737)) ([3a5bd12](https://github.com/n8n-io/n8n/commit/3a5bd129459272cbac960ae2754db3028943f87e))
* **editor:** Canvas chat UI & UX improvements ([#11924](https://github.com/n8n-io/n8n/issues/11924)) ([1e25774](https://github.com/n8n-io/n8n/commit/1e25774541461c86da5c4af8efec792e2814eeb1))
* **editor:** Persist user's preferred display modes on localStorage ([#11929](https://github.com/n8n-io/n8n/issues/11929)) ([bd69316](https://github.com/n8n-io/n8n/commit/bd693162b86a21c90880bab2c2e67aab733095ff))
### Performance Improvements
* **editor:** Virtualize SchemaView ([#11694](https://github.com/n8n-io/n8n/issues/11694)) ([9c6def9](https://github.com/n8n-io/n8n/commit/9c6def91975764522fa52cdf21e9cb5bdb4d721d))
# [1.70.0](https://github.com/n8n-io/n8n/compare/n8n@1.69.0...n8n@1.70.0) (2024-11-27)
### Bug Fixes
* **AI Agent Node:** Add binary message before scratchpad to prevent tool calling loops ([#11845](https://github.com/n8n-io/n8n/issues/11845)) ([5c80cb5](https://github.com/n8n-io/n8n/commit/5c80cb57cf709a1097a38e0394aad6fce5330eba))
* CodeNodeEditor walk cannot read properties of null ([#11129](https://github.com/n8n-io/n8n/issues/11129)) ([d99e0a7](https://github.com/n8n-io/n8n/commit/d99e0a7c979a1ee96b2eea1b9011d5bce375289a))
* **core:** Bring back execution data on the `executionFinished` push message ([#11821](https://github.com/n8n-io/n8n/issues/11821)) ([0313570](https://github.com/n8n-io/n8n/commit/03135702f18e750ba44840dccfec042270629a2b))
* **core:** Correct invalid WS status code on removing connection ([#11901](https://github.com/n8n-io/n8n/issues/11901)) ([1d80225](https://github.com/n8n-io/n8n/commit/1d80225d26ba01f78934a455acdcca7b83be7205))
* **core:** Don't use unbound context methods in code sandboxes ([#11914](https://github.com/n8n-io/n8n/issues/11914)) ([f6c0d04](https://github.com/n8n-io/n8n/commit/f6c0d045e9683cd04ee849f37b96697097c5b41d))
* **core:** Fix broken execution query when using projectId ([#11852](https://github.com/n8n-io/n8n/issues/11852)) ([a061dbc](https://github.com/n8n-io/n8n/commit/a061dbca07ad686c563e85c56081bc1a7830259b))
* **core:** Fix validation of items returned in the task runner ([#11897](https://github.com/n8n-io/n8n/issues/11897)) ([a535e88](https://github.com/n8n-io/n8n/commit/a535e88f1aec8fbbf2eb9397d38748f49773de2d))
* **editor:** Add missing trigger waiting tooltip on new canvas ([#11918](https://github.com/n8n-io/n8n/issues/11918)) ([a8df221](https://github.com/n8n-io/n8n/commit/a8df221bfbb5428d93d03f539bcfdaf29ee20c21))
* **editor:** Don't re-render input panel after node finishes executing ([#11813](https://github.com/n8n-io/n8n/issues/11813)) ([b3a99a2](https://github.com/n8n-io/n8n/commit/b3a99a2351079c37ed6d83f43920ba80f3832234))
* **editor:** Fix AI assistant loading message layout ([#11819](https://github.com/n8n-io/n8n/issues/11819)) ([89b4807](https://github.com/n8n-io/n8n/commit/89b48072432753137b498c338af7777036fdde7a))
* **editor:** Fix new canvas discovery tooltip position after adding github stars button ([#11898](https://github.com/n8n-io/n8n/issues/11898)) ([f4ab5c7](https://github.com/n8n-io/n8n/commit/f4ab5c7b9244b8fdde427c12c1a152fbaaba0c34))
* **editor:** Fix node position not getting set when dragging selection on new canvas ([#11871](https://github.com/n8n-io/n8n/issues/11871)) ([595de81](https://github.com/n8n-io/n8n/commit/595de81c03b3e488ab41fb8d1d316c3db6a8372a))
* **editor:** Restore workers view ([#11876](https://github.com/n8n-io/n8n/issues/11876)) ([3aa72f6](https://github.com/n8n-io/n8n/commit/3aa72f613f64c16d7dff67ffe66037894e45aa7c))
* **editor:** Turn NPS survey into a modal and make sure it shows above the Ask AI button ([#11814](https://github.com/n8n-io/n8n/issues/11814)) ([ca169f3](https://github.com/n8n-io/n8n/commit/ca169f3f3455fa39ce9120b30d7b409bade6561e))
* **editor:** Use `crypto.randomUUID()` to initialize node id if missing on new canvas ([#11873](https://github.com/n8n-io/n8n/issues/11873)) ([bc4857a](https://github.com/n8n-io/n8n/commit/bc4857a1b3d6ea389f11fb8246a1cee33b8a008e))
* **n8n Form Node:** Duplicate popup in manual mode ([#11925](https://github.com/n8n-io/n8n/issues/11925)) ([2c34bf4](https://github.com/n8n-io/n8n/commit/2c34bf4ea6137fb0fb321969684ffa621da20fa3))
* **n8n Form Node:** Redirect if completion page to trigger ([#11822](https://github.com/n8n-io/n8n/issues/11822)) ([1a8fb7b](https://github.com/n8n-io/n8n/commit/1a8fb7bdc428c6a23c8708e2dcf924f1f10b47a9))
* **OpenAI Node:** Remove preview chatInput parameter for `Assistant:Messsage` operation ([#11825](https://github.com/n8n-io/n8n/issues/11825)) ([4dde287](https://github.com/n8n-io/n8n/commit/4dde287cde3af7c9c0e57248e96b8f1270da9332))
* Retain execution data between partial executions (new flow) ([#11828](https://github.com/n8n-io/n8n/issues/11828)) ([3320436](https://github.com/n8n-io/n8n/commit/3320436a6fdf8472b3843b9fe8d4de7af7f5ef5c))
### Features
* Add SharePoint credentials ([#11570](https://github.com/n8n-io/n8n/issues/11570)) ([05c6109](https://github.com/n8n-io/n8n/commit/05c61091db9bdd62fdcca910ead50d0bd512966a))
* Add Zabbix credential only node ([#11489](https://github.com/n8n-io/n8n/issues/11489)) ([fbd1ecf](https://github.com/n8n-io/n8n/commit/fbd1ecfb29461fee393914bc200ec72c654d8944))
* **AI Transform Node:** Support for drag and drop ([#11276](https://github.com/n8n-io/n8n/issues/11276)) ([2c252b0](https://github.com/n8n-io/n8n/commit/2c252b0b2d5282f4a87bce76f93c4c02dd8ff5e3))
* **editor:** Drop `response` wrapper requirement from Subworkflow Tool output ([#11785](https://github.com/n8n-io/n8n/issues/11785)) ([cd3598a](https://github.com/n8n-io/n8n/commit/cd3598aaab6cefe58a4cb9df7d93fb501415e9d3))
* **editor:** Improve node and edge bring-to-front mechanism on new canvas ([#11793](https://github.com/n8n-io/n8n/issues/11793)) ([b89ca9d](https://github.com/n8n-io/n8n/commit/b89ca9d482faa5cb542898f3973fb6e7c9a8437a))
* **editor:** Make new canvas connections go underneath node when looping backwards ([#11833](https://github.com/n8n-io/n8n/issues/11833)) ([91d1bd8](https://github.com/n8n-io/n8n/commit/91d1bd8d333454f3971605df73c3703102d2a9e9))
* **editor:** Make the left sidebar in Expressions editor draggable ([#11838](https://github.com/n8n-io/n8n/issues/11838)) ([a713b3e](https://github.com/n8n-io/n8n/commit/a713b3ed25feb1790412fc320cf41a0967635263))
* **editor:** Migrate existing users to new canvas and set new canvas as default ([#11896](https://github.com/n8n-io/n8n/issues/11896)) ([caa7447](https://github.com/n8n-io/n8n/commit/caa744785a2cc5063a5fb9d269c0ea53ea432298))
* **Slack Node:** Update wait for approval to use markdown ([#11754](https://github.com/n8n-io/n8n/issues/11754)) ([40dd02f](https://github.com/n8n-io/n8n/commit/40dd02f360d0d8752fe89c4304c18cac9858c530))
# [1.69.0](https://github.com/n8n-io/n8n/compare/n8n@1.68.0...n8n@1.69.0) (2024-11-20)
### Bug Fixes
* Add supported versions warning to Zep memory node ([#11803](https://github.com/n8n-io/n8n/issues/11803)) ([9cc5bc1](https://github.com/n8n-io/n8n/commit/9cc5bc1aef974fe6c2511c1597b90c8b54ba6b9c))
* **AI Agent Node:** Escape curly brackets in tools description for non Tool agents ([#11772](https://github.com/n8n-io/n8n/issues/11772)) ([83abdfa](https://github.com/n8n-io/n8n/commit/83abdfaf027a0533824a3ac3e4bab3cad971821a))
* **Anthropic Chat Model Node:** Update credentials test endpoint ([#11756](https://github.com/n8n-io/n8n/issues/11756)) ([6cf0aba](https://github.com/n8n-io/n8n/commit/6cf0abab5bcddb407571271b9f174e66bb209790))
* **core:** Add missing env vars to task runner config ([#11810](https://github.com/n8n-io/n8n/issues/11810)) ([870c576](https://github.com/n8n-io/n8n/commit/870c576ed9d7ce4ef005db9c8bedd78e91084c9c))
* **core:** Allow Azure's SAML metadata XML containing WS-Federation nodes to pass validation ([#11724](https://github.com/n8n-io/n8n/issues/11724)) ([3b62bd5](https://github.com/n8n-io/n8n/commit/3b62bd58c264be0225a74ae0eb35c4761c419b79))
* **core:** Delete binary data parent folder when pruning executions ([#11790](https://github.com/n8n-io/n8n/issues/11790)) ([17ef2c6](https://github.com/n8n-io/n8n/commit/17ef2c63f69b811bdd28006df3b6edd446837971))
* **core:** Fix `diagnostics.enabled` default value ([#11809](https://github.com/n8n-io/n8n/issues/11809)) ([5fa72b0](https://github.com/n8n-io/n8n/commit/5fa72b0512b00bdc6a1065b7b604c9640f469454))
* **core:** Improve the security on OAuth callback endpoints ([#11593](https://github.com/n8n-io/n8n/issues/11593)) ([274fcf4](https://github.com/n8n-io/n8n/commit/274fcf45d393d8db1d2fb5ae1e774a4c9198a178))
* **core:** Restore old names for pruning config keys ([#11782](https://github.com/n8n-io/n8n/issues/11782)) ([d15b8d0](https://github.com/n8n-io/n8n/commit/d15b8d05092d2ed9dd45fcfa34b4177f60469ebd))
* **core:** Unload any existing version of a community nodes package before upgrading it ([#11727](https://github.com/n8n-io/n8n/issues/11727)) ([1d8fd13](https://github.com/n8n-io/n8n/commit/1d8fd13d841b73466ba5f8044d17d7199da7e856))
* **editor:** Add documentation link to insufficient quota message ([#11777](https://github.com/n8n-io/n8n/issues/11777)) ([1987363](https://github.com/n8n-io/n8n/commit/1987363f7941285c51fda849a4ac92832368b25a))
* **editor:** Add project header subtitle ([#11797](https://github.com/n8n-io/n8n/issues/11797)) ([ff4261c](https://github.com/n8n-io/n8n/commit/ff4261c16845c7de1790fdf0eaa9f57b37822289))
* **editor:** Change Home label to Overview ([#11736](https://github.com/n8n-io/n8n/issues/11736)) ([1a78360](https://github.com/n8n-io/n8n/commit/1a783606b4ef22d85e173a2a780d5c49ff208932))
* **editor:** Fix executions sorting ([#11808](https://github.com/n8n-io/n8n/issues/11808)) ([cd5ad65](https://github.com/n8n-io/n8n/commit/cd5ad65e90a3be4d67b51521772e0fceb7f4abc7))
* **editor:** Fix partial executions not working due to broken push message queue and race conditions ([#11798](https://github.com/n8n-io/n8n/issues/11798)) ([b05d435](https://github.com/n8n-io/n8n/commit/b05d43519994abdd34a65462d14184c779d0b667))
* **editor:** Fix reordered switch connections when copying nodes on new canvas ([#11788](https://github.com/n8n-io/n8n/issues/11788)) ([6c2dad7](https://github.com/n8n-io/n8n/commit/6c2dad79143f5b0c255ab8c97c3255314834c458))
* **editor:** Fix the issue with RMC Values to Send collection disappears ([#11710](https://github.com/n8n-io/n8n/issues/11710)) ([7bb9002](https://github.com/n8n-io/n8n/commit/7bb9002cbc10cf58550f53a30c6fd7151f8e7355))
* **editor:** Improve formatting of expired trial error message ([#11708](https://github.com/n8n-io/n8n/issues/11708)) ([8a0ad0f](https://github.com/n8n-io/n8n/commit/8a0ad0f910feeada6d0c63e81c3e97a1a6e44de7))
* **editor:** Optimize application layout ([#11769](https://github.com/n8n-io/n8n/issues/11769)) ([91f9390](https://github.com/n8n-io/n8n/commit/91f9390b90a68d064ea00d10505bf3767ddec1d4))
* **Google Sheets Trigger Node:** Fix issue with regex showing correct sheet as invalid ([#11770](https://github.com/n8n-io/n8n/issues/11770)) ([d5ba1a0](https://github.com/n8n-io/n8n/commit/d5ba1a059b7a67154f17f8ad3fcfe66c5c031059))
* **HTTP Request Node:** Continue using error ([#11733](https://github.com/n8n-io/n8n/issues/11733)) ([d1bae1a](https://github.com/n8n-io/n8n/commit/d1bae1ace062dd5b64087e0313e78599b5994355))
* **n8n Form Node:** Support expressions in completion page ([#11781](https://github.com/n8n-io/n8n/issues/11781)) ([1099167](https://github.com/n8n-io/n8n/commit/10991675fe2e6913e8f03d565b670257941f18e5))
* Prevent workflow to run if active and single webhook service ([#11752](https://github.com/n8n-io/n8n/issues/11752)) ([bcb9a20](https://github.com/n8n-io/n8n/commit/bcb9a2078186ff80e03ca3b8532d3585c307d86b))
* **Read/Write Files from Disk Node:** Escape parenthesis when reading file ([#11753](https://github.com/n8n-io/n8n/issues/11753)) ([285534e](https://github.com/n8n-io/n8n/commit/285534e6d0ceb60290bd0a928054e494252148fe))
* **YouTube Node:** Issue in published before and after dates filters ([#11741](https://github.com/n8n-io/n8n/issues/11741)) ([7381c28](https://github.com/n8n-io/n8n/commit/7381c28af00148b329690021b921267a48a6eaa3))
### Features
* **core:** Improve debugging of sub-workflows ([#11602](https://github.com/n8n-io/n8n/issues/11602)) ([fd3254d](https://github.com/n8n-io/n8n/commit/fd3254d5874a03b57421246b77a519787536a93e))
* **core:** Improve handling of manual executions with wait nodes ([#11750](https://github.com/n8n-io/n8n/issues/11750)) ([61696c3](https://github.com/n8n-io/n8n/commit/61696c3db313cdc97925af728ff5c68415f9b6b2))
* **editor:** Add Info Note to NDV Output Panel if no existing Tools were used during Execution ([#11672](https://github.com/n8n-io/n8n/issues/11672)) ([de0e861](https://github.com/n8n-io/n8n/commit/de0e86150f4d0615481e5ec3869465cfd1ce822f))
* **editor:** Add option to create sub workflow from workflows list in `Execute Workflow` node ([#11706](https://github.com/n8n-io/n8n/issues/11706)) ([c265d44](https://github.com/n8n-io/n8n/commit/c265d44841eb147115563ce24c56666b1e321433))
* **editor:** Add selection navigation using the keyboard on new canvas ([#11679](https://github.com/n8n-io/n8n/issues/11679)) ([6cd9b99](https://github.com/n8n-io/n8n/commit/6cd9b996af0406caf65941503276524de9e2add4))
* **editor:** Add universal Create Resource Menu ([#11564](https://github.com/n8n-io/n8n/issues/11564)) ([b38ce14](https://github.com/n8n-io/n8n/commit/b38ce14ec94d74aa1c9780a0572804ff6266588d))
* **Embeddings Azure OpenAI Node, Azure OpenAI Chat Model Node:** Add support for basePath url in Azure Open AI nodes ([#11784](https://github.com/n8n-io/n8n/issues/11784)) ([e298ebe](https://github.com/n8n-io/n8n/commit/e298ebe90d69f466ee897855472eaa7be1d96aba))
* **Embeddings OpenAI Node, Embeddings Azure OpenAI Node:** Add dimensions option ([#11773](https://github.com/n8n-io/n8n/issues/11773)) ([de01a8a](https://github.com/n8n-io/n8n/commit/de01a8a01d37f33ab8bcbc65588cafebda969922))
* GitHub stars dismiss button ([#11794](https://github.com/n8n-io/n8n/issues/11794)) ([8fbad74](https://github.com/n8n-io/n8n/commit/8fbad74ab685c2ba0395c30cee0ddf9498fb8984))
# [1.68.0](https://github.com/n8n-io/n8n/compare/n8n@1.67.0...n8n@1.68.0) (2024-11-13)
### Bug Fixes
* **AI Agent Node:** Throw better errors for non-tool agents when using structured tools ([#11582](https://github.com/n8n-io/n8n/issues/11582)) ([9b6123d](https://github.com/n8n-io/n8n/commit/9b6123dfb2648f880c7829211fa07666611ad0ea))
* **Auto-fixing Output Parser Node:** Only run retry chain on parsing errors ([#11569](https://github.com/n8n-io/n8n/issues/11569)) ([21b31e4](https://github.com/n8n-io/n8n/commit/21b31e488ff6ab0bcf3c79edcd17b9e37d4c64a4))
* **core:** Continue with error output reverse items in success branch ([#11684](https://github.com/n8n-io/n8n/issues/11684)) ([6d5ee83](https://github.com/n8n-io/n8n/commit/6d5ee832966fab96043b0d65697c059ced61d334))
* **core:** Ensure task runner server closes websocket connection correctly ([#11633](https://github.com/n8n-io/n8n/issues/11633)) ([b496bf3](https://github.com/n8n-io/n8n/commit/b496bf3147d2cd873d24371be02cb7ea5dbd8621))
* **core:** Handle websocket connection error more gracefully in task runners ([#11635](https://github.com/n8n-io/n8n/issues/11635)) ([af7d6e6](https://github.com/n8n-io/n8n/commit/af7d6e68d0436ff8a3d4e8410dc8ee4f3a035c44))
* **core:** Improve model sub-nodes error handling ([#11418](https://github.com/n8n-io/n8n/issues/11418)) ([57467d0](https://github.com/n8n-io/n8n/commit/57467d0285d67509322630c4c01130022f274a41))
* **core:** Make push work for waiting webhooks ([#11678](https://github.com/n8n-io/n8n/issues/11678)) ([600479b](https://github.com/n8n-io/n8n/commit/600479bf36ba8870d4aecacad19a2dc5f2d97959))
* **core:** Revert all the context helpers changes ([#11616](https://github.com/n8n-io/n8n/issues/11616)) ([20fd38f](https://github.com/n8n-io/n8n/commit/20fd38f3517f7ef35604ba16abb4d951270b4d50))
* **core:** Set the authentication methad to `email` during startup if the SAML configuration in the database has been corrupted ([#11600](https://github.com/n8n-io/n8n/issues/11600)) ([6439291](https://github.com/n8n-io/n8n/commit/6439291738dec16261979d6d835acbc63743d51a))
* **core:** Use cached value in retrieval of personal project owner ([#11533](https://github.com/n8n-io/n8n/issues/11533)) ([04029d8](https://github.com/n8n-io/n8n/commit/04029d82a11b52990890380ba7094055b18e7c1f))
* Credentials save button is hidden unless you make changes to the ([#11492](https://github.com/n8n-io/n8n/issues/11492)) ([835fbfe](https://github.com/n8n-io/n8n/commit/835fbfe337dd8dc0d0b0318c7227e174484e1328))
* **editor:** Add stickies to node insert position conflict check allowlist ([#11624](https://github.com/n8n-io/n8n/issues/11624)) ([fc39e3c](https://github.com/n8n-io/n8n/commit/fc39e3ca16231c176957e2504d55df6b416874fe))
* **editor:** Adjust Scrollbar Width of RunData Header Row ([#11561](https://github.com/n8n-io/n8n/issues/11561)) ([d17d76a](https://github.com/n8n-io/n8n/commit/d17d76a85d5425bc091d29fc84605ffbccbca984))
* **editor:** Cap NDV Output View Tab Index to prevent rare edge case ([#11614](https://github.com/n8n-io/n8n/issues/11614)) ([a6c8ee4](https://github.com/n8n-io/n8n/commit/a6c8ee4a82e6055766dc1307f79c774c17bb5f4d))
* **editor:** Do not show hover tooltip when autocomplete is active ([#11653](https://github.com/n8n-io/n8n/issues/11653)) ([23caf43](https://github.com/n8n-io/n8n/commit/23caf43e30342a21d45c825f438aa1e6193601d1))
* **editor:** Enable pinning main output with error and always allow unpinning ([#11452](https://github.com/n8n-io/n8n/issues/11452)) ([40c8882](https://github.com/n8n-io/n8n/commit/40c88822acdcda6401bd92b9cf89d013c44b8453))
* **editor:** Fix collapsing nested items in expression modal schema view ([#11645](https://github.com/n8n-io/n8n/issues/11645)) ([41dea52](https://github.com/n8n-io/n8n/commit/41dea522fbfb1c9acee51f47f384973914454b5f))
* **editor:** Fix default workflow settings ([#11632](https://github.com/n8n-io/n8n/issues/11632)) ([658568e](https://github.com/n8n-io/n8n/commit/658568e2700bfd5b61da53f3052403d0098c2d90))
* **editor:** Fix duplicate chat trigger ([#11693](https://github.com/n8n-io/n8n/issues/11693)) ([a025848](https://github.com/n8n-io/n8n/commit/a025848ec4be96f74d4de2ab104256b6d89bb837))
* **editor:** Fix hiding SQL query output when trying to select ([#11649](https://github.com/n8n-io/n8n/issues/11649)) ([4dbf2f4](https://github.com/n8n-io/n8n/commit/4dbf2f4256111985b367030020f1494b8a8b95af))
* **editor:** Fix scrolling in code edit modal ([#11647](https://github.com/n8n-io/n8n/issues/11647)) ([8f695f3](https://github.com/n8n-io/n8n/commit/8f695f3417820e7b9bb04b78972f6abbd61abbe8))
* **editor:** Prevent error being thrown in RLC while loading ([#11676](https://github.com/n8n-io/n8n/issues/11676)) ([ca8cb45](https://github.com/n8n-io/n8n/commit/ca8cb455ba59831295c238afb11aeab6ad18428e))
* **editor:** Prevent NodeCreator from swallowing AskAssistant enter event ([#11532](https://github.com/n8n-io/n8n/issues/11532)) ([db94f16](https://github.com/n8n-io/n8n/commit/db94f169fcd03983fc78a3b4c5e11543610825bf))
* **editor:** Show node executing status shortly before switching to success on new canvas ([#11675](https://github.com/n8n-io/n8n/issues/11675)) ([b0ba24c](https://github.com/n8n-io/n8n/commit/b0ba24cbbc55cebc26e9f62ead7396c4c8fbd062))
* **editor:** Show only error title and 'Open errored node' button; hide 'Ask Assistant' in root for sub-node errors ([#11573](https://github.com/n8n-io/n8n/issues/11573)) ([8cba100](https://github.com/n8n-io/n8n/commit/8cba1004888f60ca653ee069501c13b3cadcc561))
* **Facebook Lead Ads Trigger Node:** Fix issue with optional fields ([#11692](https://github.com/n8n-io/n8n/issues/11692)) ([70d315b](https://github.com/n8n-io/n8n/commit/70d315b3d5b8f5f3e0f39527bba11e254a52028e))
* **Google BigQuery Node:** Add item index to insert error ([#11702](https://github.com/n8n-io/n8n/issues/11702)) ([145d092](https://github.com/n8n-io/n8n/commit/145d0921b217bbd4b625beaacfa14429560bf51b))
* **Google Drive Node:** Fix file upload for streams ([#11698](https://github.com/n8n-io/n8n/issues/11698)) ([770230f](https://github.com/n8n-io/n8n/commit/770230fbfe0b9e86527254e201c4602fbced94ff))
* **In-Memory Vector Store Node:** Fix displaying execution data of connected embedding nodes ([#11701](https://github.com/n8n-io/n8n/issues/11701)) ([40ade15](https://github.com/n8n-io/n8n/commit/40ade151724f4af28a6ed959fd9363450ea711fd))
* **Item List Output Parser Node:** Fix number of items parameter issue ([#11696](https://github.com/n8n-io/n8n/issues/11696)) ([01ebe9d](https://github.com/n8n-io/n8n/commit/01ebe9dd38629afbab954fb489f3ef2bb7ab5b34))
* **n8n Form Node:** Find completion page ([#11674](https://github.com/n8n-io/n8n/issues/11674)) ([ed3ad6d](https://github.com/n8n-io/n8n/commit/ed3ad6d684597f7c4b7419dfa81d476e66f10eba))
* **n8n Form Node:** Open form page if form trigger has pin data ([#11673](https://github.com/n8n-io/n8n/issues/11673)) ([f0492bd](https://github.com/n8n-io/n8n/commit/f0492bd3bb0d94802a2707fb1cf861313b6ea808))
* **n8n Form Node:** Trigger page stack in waiting if error in workflow ([#11671](https://github.com/n8n-io/n8n/issues/11671)) ([94b5873](https://github.com/n8n-io/n8n/commit/94b5873248212a5500f02cf3c0d74df6f9d8fb26))
* **n8n Form Trigger Node:** Checkboxes different sizes ([#11677](https://github.com/n8n-io/n8n/issues/11677)) ([c08d23c](https://github.com/n8n-io/n8n/commit/c08d23c00f01bb6fcb3b75f02e0338af375f9b32))
* NDV search bugs ([#11613](https://github.com/n8n-io/n8n/issues/11613)) ([c32cf64](https://github.com/n8n-io/n8n/commit/c32cf644a6b8c21558e802449329877845de70b1))
* **Notion Node:** Extract page url ([#11643](https://github.com/n8n-io/n8n/issues/11643)) ([cbdd535](https://github.com/n8n-io/n8n/commit/cbdd535fe0cb4e032ea82f008dcf35cc5f2264c2))
* **Redis Chat Memory Node:** Respect the SSL flag from the credential ([#11689](https://github.com/n8n-io/n8n/issues/11689)) ([b5cbf75](https://github.com/n8n-io/n8n/commit/b5cbf7566d351d8a8e9972f13ff5867ff1c8d7d0))
* **Supabase Node:** Reset query parameters in get many operation ([#11630](https://github.com/n8n-io/n8n/issues/11630)) ([7458229](https://github.com/n8n-io/n8n/commit/74582290c04d2dd32300b1a6c7715862ae837d34))
* **Switch Node:** Maintain output connections ([#11162](https://github.com/n8n-io/n8n/issues/11162)) ([9bd79fc](https://github.com/n8n-io/n8n/commit/9bd79fceebc4453d0fe40ae5f628d5e31ff2b326))
### Features
* **AI Transform Node:** Show warning for binary data ([#11560](https://github.com/n8n-io/n8n/issues/11560)) ([ddbb263](https://github.com/n8n-io/n8n/commit/ddbb263dce0fc458abc95d850217251bb49d2b83))
* **core:** Make all http requests made with `httpRequestWithAuthentication` abortable ([#11704](https://github.com/n8n-io/n8n/issues/11704)) ([0d8aada](https://github.com/n8n-io/n8n/commit/0d8aada49005d6a6078e8460003a0de61a8f423c))
* **editor:** Improve how we show default Agent prompt and Memory session parameters ([#11491](https://github.com/n8n-io/n8n/issues/11491)) ([565f8cd](https://github.com/n8n-io/n8n/commit/565f8cd8c78b534a50e272997d659d162fa86625))
* **editor:** Improve workflow loading performance on new canvas ([#11629](https://github.com/n8n-io/n8n/issues/11629)) ([f1e2df7](https://github.com/n8n-io/n8n/commit/f1e2df7d0753aa0f33cf299100a063bf89cc8b35))
* **editor:** Redesign Canvas Chat ([#11634](https://github.com/n8n-io/n8n/issues/11634)) ([a412ab7](https://github.com/n8n-io/n8n/commit/a412ab7ebfcd6aa9051a8ca36e34f1067102c998))
* **editor:** Restrict when a ChatTrigger Node is added automatically ([#11523](https://github.com/n8n-io/n8n/issues/11523)) ([93a6f85](https://github.com/n8n-io/n8n/commit/93a6f858fa3eb53f8b48b2a3d6b7377279dd6ed1))
* Github star button in-app ([#11695](https://github.com/n8n-io/n8n/issues/11695)) ([0fd684d](https://github.com/n8n-io/n8n/commit/0fd684d90c830f8b0aab12b7f78a1fa5619c62c9))
* **Oura Node:** Update node for v2 api ([#11604](https://github.com/n8n-io/n8n/issues/11604)) ([3348fbb](https://github.com/n8n-io/n8n/commit/3348fbb1547c430ff8707b298640e3461d3f6536))
### Performance Improvements
* **editor:** Add lint rules for optimization-friendly syntax ([#11681](https://github.com/n8n-io/n8n/issues/11681)) ([88295c7](https://github.com/n8n-io/n8n/commit/88295c70495ae3d017674d5745972a346fcbaf12))
# [1.67.0](https://github.com/n8n-io/n8n/compare/n8n@1.66.0...n8n@1.67.0) (2024-11-06)
### Bug Fixes
* Bring back nodes panel telemetry events ([#11456](https://github.com/n8n-io/n8n/issues/11456)) ([130c942](https://github.com/n8n-io/n8n/commit/130c942f633788d1b2f937d6fea342d4450c6e3d))
* **core:** Account for double quotes in instance base URL ([#11495](https://github.com/n8n-io/n8n/issues/11495)) ([c5191e6](https://github.com/n8n-io/n8n/commit/c5191e697a9a9ebfa2b67587cd01b5835ebf6ea8))
* **core:** Do not delete waiting executions when saving of successful executions is disabled ([#11458](https://github.com/n8n-io/n8n/issues/11458)) ([e8757e5](https://github.com/n8n-io/n8n/commit/e8757e58f69e091ac3d2a2f8e8c8e33ac57c1e47))
* **core:** Don't send a `executionFinished` event to the browser with no run data if the execution has already been cleaned up ([#11502](https://github.com/n8n-io/n8n/issues/11502)) ([d1153f5](https://github.com/n8n-io/n8n/commit/d1153f51e80911cbc8f34ba5f038f349b75295c3))
* **core:** Include `projectId` in range query middleware ([#11590](https://github.com/n8n-io/n8n/issues/11590)) ([a6070af](https://github.com/n8n-io/n8n/commit/a6070afdda29631fd36e5213f52bf815268bcda4))
* **core:** Save exeution progress for waiting executions, even when progress saving is disabled ([#11535](https://github.com/n8n-io/n8n/issues/11535)) ([6b9353c](https://github.com/n8n-io/n8n/commit/6b9353c80f61ab36945fff434d98242dc1cab7b3))
* **core:** Use the correct docs URL for regular nodes when used as tools ([#11529](https://github.com/n8n-io/n8n/issues/11529)) ([a092b8e](https://github.com/n8n-io/n8n/commit/a092b8e972e1253d92df416f19096a045858e7c1))
* **Edit Image Node:** Fix Text operation by setting Arial as default font ([#11125](https://github.com/n8n-io/n8n/issues/11125)) ([60c1ace](https://github.com/n8n-io/n8n/commit/60c1ace64be29d651ce7b777fbb576598e38b9d7))
* **editor:** Auto focus first fields on SignIn, SignUp and ForgotMyPassword views ([#11445](https://github.com/n8n-io/n8n/issues/11445)) ([5b5bd72](https://github.com/n8n-io/n8n/commit/5b5bd7291dde17880b7699f7e6832938599ffd8f))
* **editor:** Do not overwrite the webhookId in the new canvas ([#11562](https://github.com/n8n-io/n8n/issues/11562)) ([dfd785b](https://github.com/n8n-io/n8n/commit/dfd785bc0894257eb6e62b0dd8f71248c27aae53))
* **editor:** Ensure Enter key on Cancel button correctly cancels node rename ([#11563](https://github.com/n8n-io/n8n/issues/11563)) ([be05ae3](https://github.com/n8n-io/n8n/commit/be05ae36e7790156cb48b317fc254ae46a3b2d8c))
* **editor:** Fix emitting `n8nReady` notification via `postmessage` on new canvas ([#11558](https://github.com/n8n-io/n8n/issues/11558)) ([463d101](https://github.com/n8n-io/n8n/commit/463d101f3592e6df4afd66c4d0fde0cb4aec34cc))
* **editor:** Fix run index input for RunData view in sub-nodes ([#11538](https://github.com/n8n-io/n8n/issues/11538)) ([434d31c](https://github.com/n8n-io/n8n/commit/434d31ce928342d52b6ab8b78639afd7829216d4))
* **editor:** Fix selected credential being overwritten in NDV ([#11496](https://github.com/n8n-io/n8n/issues/11496)) ([a26c0e2](https://github.com/n8n-io/n8n/commit/a26c0e2c3c7da87bfaba9737a967aa0070810d85))
* **editor:** Keep workflow pristine after load on new canvas ([#11579](https://github.com/n8n-io/n8n/issues/11579)) ([7254359](https://github.com/n8n-io/n8n/commit/7254359855b89769613cd5cc24dbb4f45a7cc76f))
* Show Pinned data in demo mode ([#11490](https://github.com/n8n-io/n8n/issues/11490)) ([ca2a583](https://github.com/n8n-io/n8n/commit/ca2a583b5cbb0cba3ecb694261806de16547aa91))
* Toast not aligned to the bottom when AI assistant disable ([#11549](https://github.com/n8n-io/n8n/issues/11549)) ([e80f7e0](https://github.com/n8n-io/n8n/commit/e80f7e0a02a972379f73af6a44de11768081086e))
### Features
* Add Rapid7 InsightVm credentials ([#11462](https://github.com/n8n-io/n8n/issues/11462)) ([46eceab](https://github.com/n8n-io/n8n/commit/46eceabc27ac219b11b85c16c533a2cff848c5dd))
* **AI Transform Node:** UX improvements ([#11280](https://github.com/n8n-io/n8n/issues/11280)) ([8a48407](https://github.com/n8n-io/n8n/commit/8a484077af3d3e1fe2d1b90b1ea9edf4ba41fcb6))
* **Anthropic Chat Model Node:** Add support for Haiku 3.5 ([#11551](https://github.com/n8n-io/n8n/issues/11551)) ([8b39825](https://github.com/n8n-io/n8n/commit/8b398256a81594a52f20f8eb8adf8ff205209bc1))
* **Convert to File Node:** Add delimiter convert to csv ([#11556](https://github.com/n8n-io/n8n/issues/11556)) ([63d454b](https://github.com/n8n-io/n8n/commit/63d454b776c092ff8c6c521a7e083774adb8f649))
* **editor:** Update panning and selection keybindings on new canvas ([#11534](https://github.com/n8n-io/n8n/issues/11534)) ([5e2e205](https://github.com/n8n-io/n8n/commit/5e2e205394adf76faf02aee2d4f21df71848e1d4))
* **Gmail Trigger Node:** Add filter option to include drafts ([#11441](https://github.com/n8n-io/n8n/issues/11441)) ([7a2be77](https://github.com/n8n-io/n8n/commit/7a2be77f384a32ede3acad8fe24fb89227c058bf))
* **Intercom Node:** Update credential to new style ([#11485](https://github.com/n8n-io/n8n/issues/11485)) ([b137e13](https://github.com/n8n-io/n8n/commit/b137e13845f0714ebf7421c837f5ab104b66709b))
# [1.66.0](https://github.com/n8n-io/n8n/compare/n8n@1.65.0...n8n@1.66.0) (2024-10-31)
### Bug Fixes
* **Asana Node:** Fix issue with pagination ([#11415](https://github.com/n8n-io/n8n/issues/11415)) ([04c075a](https://github.com/n8n-io/n8n/commit/04c075a46bcc7b1964397f0244b0fde99476212d))
* **core:** Add 'user_id' to `license-community-plus-registered` telemetry event ([#11430](https://github.com/n8n-io/n8n/issues/11430)) ([7a8dafe](https://github.com/n8n-io/n8n/commit/7a8dafe9902fbc0d5001c50579c34959b95211ab))
* **core:** Add safeguard for command publishing ([#11337](https://github.com/n8n-io/n8n/issues/11337)) ([656439e](https://github.com/n8n-io/n8n/commit/656439e87138f9f96dea5a683cfdac3f661ffefb))
* **core:** Ensure `LoggerProxy` is not scoped ([#11379](https://github.com/n8n-io/n8n/issues/11379)) ([f4ea943](https://github.com/n8n-io/n8n/commit/f4ea943c9cb2321e41705de6c5c27535a0f5eae0))
* **core:** Ensure `remove-triggers-and-pollers` command is not debounced ([#11486](https://github.com/n8n-io/n8n/issues/11486)) ([529d4fc](https://github.com/n8n-io/n8n/commit/529d4fc3ef5206bd1b02d27634342cc50b45997e))
* **core:** Ensure job processor does not reprocess amended executions ([#11438](https://github.com/n8n-io/n8n/issues/11438)) ([c152a3a](https://github.com/n8n-io/n8n/commit/c152a3ac56f140a39eea4771a94f5a3082118df7))
* **core:** Fix Message Event Bus Metrics not counting up for labeled metrics ([#11396](https://github.com/n8n-io/n8n/issues/11396)) ([7fc3b25](https://github.com/n8n-io/n8n/commit/7fc3b25d21c6c4f1802f34b1ae065a65cac3001b))
* **core:** Fix resolving of $fromAI expression via `evaluateExpression` ([#11397](https://github.com/n8n-io/n8n/issues/11397)) ([2e64464](https://github.com/n8n-io/n8n/commit/2e6446454defbd3e5a47b66e6fd46d4f1b9fbd0f))
* **core:** Make execution and its data creation atomic ([#11392](https://github.com/n8n-io/n8n/issues/11392)) ([ed30d43](https://github.com/n8n-io/n8n/commit/ed30d43236bf3c6b657022636a02a41be01aa152))
* **core:** On unhandled rejections, extract the original exception correctly ([#11389](https://github.com/n8n-io/n8n/issues/11389)) ([8608bae](https://github.com/n8n-io/n8n/commit/8608baeb7ec302daddc8adca6e39778dcf7b2eda))
* **editor:** Fix TypeError: Cannot read properties of undefined (reading '0') ([#11399](https://github.com/n8n-io/n8n/issues/11399)) ([ae37c52](https://github.com/n8n-io/n8n/commit/ae37c520a91c75e353e818944b36a3619c0d8b4a))
* **editor:** Add Retry button for AI Assistant errors ([#11345](https://github.com/n8n-io/n8n/issues/11345)) ([7699240](https://github.com/n8n-io/n8n/commit/7699240073122cdef31cf109fd37fa66961f588a))
* **editor:** Change tooltip for workflow with execute workflow trigger ([#11374](https://github.com/n8n-io/n8n/issues/11374)) ([dcd6038](https://github.com/n8n-io/n8n/commit/dcd6038c3085135803cdaa546a239359a6d449eb))
* **editor:** Ensure toasts show above modal overlays ([#11410](https://github.com/n8n-io/n8n/issues/11410)) ([351134f](https://github.com/n8n-io/n8n/commit/351134f786af933f5f310bf8d9897269387635a0))
* **editor:** Fit view consistently after nodes are initialized on new canvas ([#11457](https://github.com/n8n-io/n8n/issues/11457)) ([497d637](https://github.com/n8n-io/n8n/commit/497d637fc5308b9c4a06bc764152fde1f1a9c130))
* **editor:** Fix adding connections when initializing workspace in templates view on new canvas ([#11451](https://github.com/n8n-io/n8n/issues/11451)) ([ea47b02](https://github.com/n8n-io/n8n/commit/ea47b025fb16c967d4fc73dcacc6e260d2aecd61))
* **editor:** Fix rendering of AI logs ([#11450](https://github.com/n8n-io/n8n/issues/11450)) ([73b0a80](https://github.com/n8n-io/n8n/commit/73b0a80ac92b4f4b5a300d0ec1c833b4395a222a))
* **editor:** Hide data mapping tooltip in credential edit modal ([#11356](https://github.com/n8n-io/n8n/issues/11356)) ([ff14dcb](https://github.com/n8n-io/n8n/commit/ff14dcb3a1ddaea4eca7c1ecd2e92c0abb0c413c))
* **editor:** Prevent running workflow that has issues if listening to webhook ([#11402](https://github.com/n8n-io/n8n/issues/11402)) ([8b0a48f](https://github.com/n8n-io/n8n/commit/8b0a48f53010378e497e4cc362fda75a958cf363))
* **editor:** Run external hooks after settings have been initialized ([#11423](https://github.com/n8n-io/n8n/issues/11423)) ([0ab24c8](https://github.com/n8n-io/n8n/commit/0ab24c814abd1787268750ba808993ab2735ac52))
* **editor:** Support middle click to scroll when using a mouse on new canvas ([#11384](https://github.com/n8n-io/n8n/issues/11384)) ([46f3b4a](https://github.com/n8n-io/n8n/commit/46f3b4a258f89f02e0d2bd1eef25a22e3a721167))
* **HTTP Request Tool Node:** Fix HTML response optimization issue ([#11439](https://github.com/n8n-io/n8n/issues/11439)) ([cf37e94](https://github.com/n8n-io/n8n/commit/cf37e94dd875e9f6ab1f189146fb34e7296af93c))
* **n8n Form Node:** Form Trigger does not wait in multi-form mode ([#11404](https://github.com/n8n-io/n8n/issues/11404)) ([151f4dd](https://github.com/n8n-io/n8n/commit/151f4dd7b8f800af424f8ae64cb8238975fb3cb8))
* Update required node js version in CONTRIBUTING.md ([#11437](https://github.com/n8n-io/n8n/issues/11437)) ([4f511aa](https://github.com/n8n-io/n8n/commit/4f511aab68651caa8fe47f70cd7cdb88bb06a3e2))
### Features
* **Anthropic Chat Model Node:** Add model claude-3-5-sonnet-20241022 ([#11465](https://github.com/n8n-io/n8n/issues/11465)) ([f6c8890](https://github.com/n8n-io/n8n/commit/f6c8890a8069de221b9b96e735418ecc9624cf7b))
* **core:** Handle nodes with multiple inputs and connections during partial executions ([#11376](https://github.com/n8n-io/n8n/issues/11376)) ([cb7c4d2](https://github.com/n8n-io/n8n/commit/cb7c4d29a6f042b590822e5b9c67fff0a8f0863d))
* **editor:** Add descriptive header to projects /workflow ([#11203](https://github.com/n8n-io/n8n/issues/11203)) ([5d19e8f](https://github.com/n8n-io/n8n/commit/5d19e8f2b45dc1abc5a8253f9e3a0fdacb1ebd91))
* **editor:** Improve placeholder for vector store tool ([#11483](https://github.com/n8n-io/n8n/issues/11483)) ([629e092](https://github.com/n8n-io/n8n/commit/629e09240785bc648ff6575f97910fbb4e77cdab))
* **editor:** Remove edge execution animation on new canvas ([#11446](https://github.com/n8n-io/n8n/issues/11446)) ([a701d87](https://github.com/n8n-io/n8n/commit/a701d87f5ba94ffc811e424b60e188b26ac6c1c5))
* **editor:** Update ownership pills ([#11155](https://github.com/n8n-io/n8n/issues/11155)) ([8147038](https://github.com/n8n-io/n8n/commit/8147038cf87dca657602e617e49698065bf1a63f))
# [1.65.0](https://github.com/n8n-io/n8n/compare/n8n@1.64.0...n8n@1.65.0) (2024-10-24)
### Bug Fixes
* **AI Agent Node:** Preserve `intermediateSteps` when using output parser with non-tool agent ([#11363](https://github.com/n8n-io/n8n/issues/11363)) ([e61a853](https://github.com/n8n-io/n8n/commit/e61a8535aa39653b9a87575ea911a65318282167))
* **API:** `PUT /credentials/:id` should move the specified credential, not the first one in the database ([#11365](https://github.com/n8n-io/n8n/issues/11365)) ([e6b2f8e](https://github.com/n8n-io/n8n/commit/e6b2f8e7e6ebbb6e3776a976297d519e99ac6c64))
* **API:** Correct credential schema for response in `POST /credentials` ([#11340](https://github.com/n8n-io/n8n/issues/11340)) ([f495875](https://github.com/n8n-io/n8n/commit/f4958756b4976e0b608b9155dab84564f7e8804e))
* **core:** Account for waiting jobs during shutdown ([#11338](https://github.com/n8n-io/n8n/issues/11338)) ([c863abd](https://github.com/n8n-io/n8n/commit/c863abd08300b53ea898fc4d06aae97dec7afa9b))
* **core:** Add missing primary key to execution annotation tags table ([#11168](https://github.com/n8n-io/n8n/issues/11168)) ([b4b543d](https://github.com/n8n-io/n8n/commit/b4b543d41daa07753eca24ab93bf7445f672361d))
* **core:** Change dedupe value column type from varchar(255) to text ([#11357](https://github.com/n8n-io/n8n/issues/11357)) ([7a71cff](https://github.com/n8n-io/n8n/commit/7a71cff4d75fe4e7282a398b4843428e0161ba8c))
* **core:** Do not debounce webhooks, triggers and pollers activation ([#11306](https://github.com/n8n-io/n8n/issues/11306)) ([64bddf8](https://github.com/n8n-io/n8n/commit/64bddf86536ddd688638a643d24f80c947a12f31))
* **core:** Enforce nodejs version consistently ([#11323](https://github.com/n8n-io/n8n/issues/11323)) ([0fa2e8c](https://github.com/n8n-io/n8n/commit/0fa2e8ca85005362d9043d82469f3c3525f4c4ef))
* **core:** Fix memory issue with empty model response ([#11300](https://github.com/n8n-io/n8n/issues/11300)) ([216b119](https://github.com/n8n-io/n8n/commit/216b119350949de70f15cf2d61f474770803ad7a))
* **core:** Fix race condition when resolving post-execute promise ([#11360](https://github.com/n8n-io/n8n/issues/11360)) ([4f1816e](https://github.com/n8n-io/n8n/commit/4f1816e03db00219bc2e723e3048848aef7f8fe1))
* **core:** Sanitise IdP provided information in SAML test pages ([#11171](https://github.com/n8n-io/n8n/issues/11171)) ([74fc388](https://github.com/n8n-io/n8n/commit/74fc3889b946e8f224e65ef8d3d44125404aa4fc))
* Don't show pin button in input panel when there's binary data ([#11267](https://github.com/n8n-io/n8n/issues/11267)) ([c0b5b92](https://github.com/n8n-io/n8n/commit/c0b5b92f62a2d7ba60492eb27daced268b654fe9))
* **editor:** Add Personal project to main navigation ([#11161](https://github.com/n8n-io/n8n/issues/11161)) ([1f441f9](https://github.com/n8n-io/n8n/commit/1f441f97528f58e905eaf8930577bbcd08debf06))
* **editor:** Fix Cannot read properties of undefined (reading 'finished') ([#11367](https://github.com/n8n-io/n8n/issues/11367)) ([475d72e](https://github.com/n8n-io/n8n/commit/475d72e0bc9e13c6dc56129902f6f89c67547f78))
* **editor:** Fix delete all existing executions ([#11352](https://github.com/n8n-io/n8n/issues/11352)) ([3ec103f](https://github.com/n8n-io/n8n/commit/3ec103f8baaa89e579844947d945f00bec9e498e))
* **editor:** Fix pin data button disappearing after reload ([#11198](https://github.com/n8n-io/n8n/issues/11198)) ([3b2f63e](https://github.com/n8n-io/n8n/commit/3b2f63e248cd0cba04087e2f40e13d670073707d))
* **editor:** Fix RunData non-binary pagination when binary data is present ([#11309](https://github.com/n8n-io/n8n/issues/11309)) ([901888d](https://github.com/n8n-io/n8n/commit/901888d5b1027098653540c72f787f176941f35a))
* **editor:** Fix sorting problem in older browsers that don't support `toSorted` ([#11204](https://github.com/n8n-io/n8n/issues/11204)) ([c728a2f](https://github.com/n8n-io/n8n/commit/c728a2ffe01f510a237979a54897c4680a407800))
* **editor:** Follow-up fixes to projects side menu ([#11327](https://github.com/n8n-io/n8n/issues/11327)) ([4dde772](https://github.com/n8n-io/n8n/commit/4dde772814c55e66efcc9b369ae443328af21b14))
* **editor:** Keep always focus on the first item on the node's search panel ([#11193](https://github.com/n8n-io/n8n/issues/11193)) ([c57cac9](https://github.com/n8n-io/n8n/commit/c57cac9e4d447c3a4240a565f9f2de8aa3b7c513))
* **editor:** Open Community+ enrollment modal only for the instance owner ([#11292](https://github.com/n8n-io/n8n/issues/11292)) ([76724c3](https://github.com/n8n-io/n8n/commit/76724c3be6e001792433045c2b2aac0ef16d4b8a))
* **editor:** Record sessionStarted telemetry event in Setting Store ([#11334](https://github.com/n8n-io/n8n/issues/11334)) ([1b734dd](https://github.com/n8n-io/n8n/commit/1b734dd9f42885594ce02400cfb395a4f5e7e088))
* Ensure NDV params don't get cut off early and scrolled to the top ([#11252](https://github.com/n8n-io/n8n/issues/11252)) ([054fe97](https://github.com/n8n-io/n8n/commit/054fe9745ff6864f9088aa4cd66ed9e7869520d5))
* **HTTP Request Tool Node:** Fix the undefined response issue when authentication is enabled ([#11343](https://github.com/n8n-io/n8n/issues/11343)) ([094ec68](https://github.com/n8n-io/n8n/commit/094ec68d4c00848013aa4eec4ac5efbd2c92afc5))
* Include error in the message in JS task runner sandbox ([#11359](https://github.com/n8n-io/n8n/issues/11359)) ([0708b3a](https://github.com/n8n-io/n8n/commit/0708b3a1f8097af829c92fe106ea6ba375d6c500))
* **Microsoft SQL Node:** Fix execute query to allow for non select query to run ([#11335](https://github.com/n8n-io/n8n/issues/11335)) ([ba158b4](https://github.com/n8n-io/n8n/commit/ba158b4f8533bd3430db8766d4921f75db5c1a11))
* **OpenAI Chat Model Node, Ollama Chat Model Node:** Change default model to a more up-to-date option ([#11293](https://github.com/n8n-io/n8n/issues/11293)) ([0be04c6](https://github.com/n8n-io/n8n/commit/0be04c6348d8c059a96c3d37a6d6cd587bfb97f3))
* **Pinecone Vector Store Node:** Prevent populating of vectors after manually stopping the execution ([#11288](https://github.com/n8n-io/n8n/issues/11288)) ([fbae17d](https://github.com/n8n-io/n8n/commit/fbae17d8fb35a5197fa183e3639bb36762dc73d2))
* **Postgres Node:** Special datetime values cause errors ([#11225](https://github.com/n8n-io/n8n/issues/11225)) ([3c57f46](https://github.com/n8n-io/n8n/commit/3c57f46aaeb968d2974f2dc9790317a6a6fab624))
* Resend invite operation on users list ([#11351](https://github.com/n8n-io/n8n/issues/11351)) ([e4218de](https://github.com/n8n-io/n8n/commit/e4218debd18812fa3aa508339afd3de03c4d69dc))
* **SSH Node:** Cleanup temporary binary files as soon as possible ([#11305](https://github.com/n8n-io/n8n/issues/11305)) ([08a7b5b](https://github.com/n8n-io/n8n/commit/08a7b5b7425663ec6593114921c2e22ab37d039e))
### Features
* Add report bug buttons ([#11304](https://github.com/n8n-io/n8n/issues/11304)) ([296f68f](https://github.com/n8n-io/n8n/commit/296f68f041b93fd32ac7be2b53c2b41d58c2998a))
* **AI Agent Node:** Make tools optional when using OpenAI model with Tools agent ([#11212](https://github.com/n8n-io/n8n/issues/11212)) ([fed7c3e](https://github.com/n8n-io/n8n/commit/fed7c3ec1fb0553adaa9a933f91aabfd54fe83a3))
* **core:** introduce JWT API keys for the public API ([#11005](https://github.com/n8n-io/n8n/issues/11005)) ([679fa4a](https://github.com/n8n-io/n8n/commit/679fa4a10a85fc96e12ca66fe12cdb32368bc12b))
* **core:** Enforce config file permissions on startup ([#11328](https://github.com/n8n-io/n8n/issues/11328)) ([c078a51](https://github.com/n8n-io/n8n/commit/c078a516bec857831cc904ef807d0791b889f3a2))
* **core:** Handle cycles in workflows when partially executing them ([#11187](https://github.com/n8n-io/n8n/issues/11187)) ([321d6de](https://github.com/n8n-io/n8n/commit/321d6deef18806d88d97afef2f2c6f29e739ccb4))
* **editor:** Separate node output execution tooltip from status icon ([#11196](https://github.com/n8n-io/n8n/issues/11196)) ([cd15e95](https://github.com/n8n-io/n8n/commit/cd15e959c7af82a7d8c682e94add2b2640624a70))
* **GitHub Node:** Add workflow resource operations ([#10744](https://github.com/n8n-io/n8n/issues/10744)) ([d309112](https://github.com/n8n-io/n8n/commit/d3091126472faa2c8f270650e54027d19dc56bb6))
* **n8n Form Page Node:** New node ([#10390](https://github.com/n8n-io/n8n/issues/10390)) ([643d66c](https://github.com/n8n-io/n8n/commit/643d66c0ae084a0d93dac652703adc0a32cab8de))
* **n8n Google My Business Node:** New node ([#10504](https://github.com/n8n-io/n8n/issues/10504)) ([bf28fbe](https://github.com/n8n-io/n8n/commit/bf28fbefe5e8ba648cba1555a2d396b75ee32bbb))
* Run `mfa.beforeSetup` hook before enabling MFA ([#11116](https://github.com/n8n-io/n8n/issues/11116)) ([25c1c32](https://github.com/n8n-io/n8n/commit/25c1c3218cf1075ca3abd961236f3b2fbd9d6ba9))
* **Structured Output Parser Node:** Refactor Output Parsers and Improve Error Handling ([#11148](https://github.com/n8n-io/n8n/issues/11148)) ([45274f2](https://github.com/n8n-io/n8n/commit/45274f2e7f081e194e330e1c9e6a5c26fca0b141))
# [1.64.0](https://github.com/n8n-io/n8n/compare/n8n@1.63.0...n8n@1.64.0) (2024-10-16)

View file

@ -19,6 +19,7 @@ Great that you are here and you want to contribute to n8n
- [Actual n8n setup](#actual-n8n-setup)
- [Start](#start)
- [Development cycle](#development-cycle)
- [Community PR Guidelines](#community-pr-guidelines)
- [Test suite](#test-suite)
- [Unit tests](#unit-tests)
- [E2E tests](#e2e-tests)
@ -68,7 +69,7 @@ If you already have VS Code and Docker installed, you can click [here](https://v
#### Node.js
[Node.js](https://nodejs.org/en/) version 18.10 or newer is required for development purposes.
[Node.js](https://nodejs.org/en/) version 20.15 or newer is required for development purposes.
#### pnpm
@ -191,6 +192,51 @@ automatically build your code, restart the backend and refresh the frontend
```
1. Commit code and [create a pull request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
---
### Community PR Guidelines
#### **1. Change Request/Comment**
Please address the requested changes or provide feedback within 14 days. If there is no response or updates to the pull request during this time, it will be automatically closed. The PR can be reopened once the requested changes are applied.
#### **2. General Requirements**
- **Follow the Style Guide:**
- Ensure your code adheres to n8n's coding standards and conventions (e.g., formatting, naming, indentation). Use linting tools where applicable.
- **TypeScript Compliance:**
- Do not use `ts-ignore` .
- Ensure code adheres to TypeScript rules.
- **Avoid Repetitive Code:**
- Reuse existing components, parameters, and logic wherever possible instead of redefining or duplicating them.
- For nodes: Use the same parameter across multiple operations rather than defining a new parameter for each operation (if applicable).
- **Testing Requirements:**
- PRs **must include tests**:
- Unit tests
- Workflow tests for nodes (example [here](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/nodes/Switch/V3/test))
- UI tests (if applicable)
- **Typos:**
- Use a spell-checking tool, such as [**Code Spell Checker**](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker), to avoid typos.
#### **3. PR Specific Requirements**
- **Small PRs Only:**
- Focus on a single feature or fix per PR.
- **Naming Convention:**
- Follow [n8n's PR Title Conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md#L36).
- **New Nodes:**
- PRs that introduce new nodes will be **auto-closed** unless they are explicitly requested by the n8n team and aligned with an agreed project scope. However, you can still explore [building your own nodes](https://docs.n8n.io/integrations/creating-nodes/) , as n8n offers the flexibility to create your own custom nodes.
- **Typo-Only PRs:**
- Typos are not sufficient justification for a PR and will be rejected.
#### **4. Workflow Summary for Non-Compliant PRs**
- **No Tests:** If tests are not provided, the PR will be auto-closed after **14 days**.
- **Non-Small PRs:** Large or multifaceted PRs will be returned for segmentation.
- **New Nodes/Typo PRs:** Automatically rejected if not aligned with project scope or guidelines.
---
### Test suite
#### Unit tests

View file

@ -3,9 +3,11 @@
Portions of this software are licensed as follows:
- Content of branches other than the main branch (i.e. "master") are not licensed.
- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
- Source code files that contain ".ee." in their filename or ".ee" in their dirname are NOT licensed under
the Sustainable Use License.
To use source code files that contain ".ee." in their filename or ".ee" in their dirname you must hold a
valid n8n Enterprise License specifically allowing you access to such source code files and as defined
in "LICENSE_EE.md".
- All third party components incorporated into the n8n Software are licensed under the original license
provided by the owner of the applicable component.
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use

127
README.md
View file

@ -1,104 +1,67 @@
![n8n.io - Workflow Automation](https://user-images.githubusercontent.com/65276001/173571060-9f2f6d7b-bac0-43b6-bdb2-001da9694058.png)
![Banner image](https://user-images.githubusercontent.com/10284570/173569848-c624317f-42b1-45a6-ab09-f0ea3c247648.png)
# n8n - Workflow automation tool
# n8n - Secure Workflow Automation for Technical Teams
n8n is an extendable workflow automation tool. With a [fair-code](https://faircode.io) distribution model, n8n
will always have visible source code, be available to self-host, and allow you to add your own custom
functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect
anything to everything.
n8n is a workflow automation platform that gives technical teams the flexibility of code with the speed of no-code. With 400+ integrations, native AI capabilities, and a fair-code license, n8n lets you build powerful automations while maintaining full control over your data and deployments.
![n8n.io - Screenshot](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png)
![n8n.io - Screenshot](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot-readme.png)
## Demo
## Key Capabilities
[:tv: A short video (< 5 min)](https://www.youtube.com/watch?v=1MwSoB0gnM4) that goes over key concepts of
creating workflows in n8n.
- **Code When You Need It**: Write JavaScript/Python, add npm packages, or use the visual interface
- **AI-Native Platform**: Build AI agent workflows based on LangChain with your own data and models
- **Full Control**: Self-host with our fair-code license or use our [cloud offering](https://app.n8n.cloud/login)
- **Enterprise-Ready**: Advanced permissions, SSO, and air-gapped deployments
- **Active Community**: 400+ integrations and 900+ ready-to-use [templates](https://n8n.io/workflows)
## Available integrations
## Quick Start
n8n has 200+ different nodes to automate workflows. The list can be found on:
[https://n8n.io/integrations](https://n8n.io/integrations)
## Documentation
The official n8n documentation can be found on our [documentation website](https://docs.n8n.io)
Additional information and example workflows on the [n8n.io website](https://n8n.io)
The release notes can be found [here](https://docs.n8n.io/release-notes/) and the list of breaking
changes [here](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md).
## Usage
- :books: Learn
[how to **use** it from the command line](https://docs.n8n.io/reference/cli-commands/)
- :whale: Learn
[how to run n8n in **Docker**](https://docs.n8n.io/hosting/installation/docker/)
## Start
You can try n8n without installing it using npx. You must have [Node.js](https://nodejs.org/en/) installed.
From the terminal, run:
Try n8n instantly with [npx](https://docs.n8n.io/hosting/installation/npm/) (requires [Node.js](https://nodejs.org/en/)):
`npx n8n`
This command will download everything that is needed to start n8n. You can then access n8n and start building workflows by opening [http://localhost:5678](http://localhost:5678).
Or deploy with [Docker](https://docs.n8n.io/hosting/installation/docker/):
## n8n cloud
`docker run -it --rm --name n8n -p 5678:5678 docker.n8n.io/n8nio/n8n`
Sign-up for an [n8n cloud](https://www.n8n.io/cloud/) account.
Access the editor at http://localhost:5678
While n8n cloud and n8n are the same in terms of features, n8n cloud provides certain conveniences such as:
## Resources
- Not having to set up and maintain your n8n instance
- Managed OAuth for authentication
- Easily upgrading to the newer n8n versions
## Build with LangChain and AI in n8n (beta)
With n8n's LangChain nodes you can build AI-powered functionality within your workflows. The LangChain nodes are configurable, meaning you can choose your preferred agent, LLM, memory, and so on. Alongside the LangChain nodes, you can connect any n8n node as normal: this means you can integrate your LangChain logic with other data sources and services.
Learn more in the [documentation](https://docs.n8n.io/langchain/).
- [LangChain nodes package](https://www.npmjs.com/package/@n8n/n8n-nodes-langchain)
- [Chatbot package](https://www.npmjs.com/package/@n8n/chat)
- 📚 [Documentation](https://docs.n8n.io)
- 🔧 [400+ Integrations](https://n8n.io/integrations)
- 💡 [Example Workflows](https://n8n.io/workflows)
- 🤖 [AI & LangChain Guide](https://docs.n8n.io/langchain/)
- 👥 [Community Forum](https://community.n8n.io)
- 📖 [Community Tutorials](https://community.n8n.io/c/tutorials/28)
## Support
If you have problems or questions go to our forum, we will then try to help you asap:
[https://community.n8n.io](https://community.n8n.io)
## Jobs
If you are interested in working for n8n and so shape the future of the project check out our
[job posts](https://apply.workable.com/n8n/)
## What does n8n mean and how do you pronounce it?
**Short answer:** It means "nodemation" and it is pronounced as n-eight-n.
**Long answer:** "I get that question quite often (more often than I expected) so I decided it is probably
best to answer it here. While looking for a good name for the project with a free domain I realized very
quickly that all the good ones I could think of were already taken. So, in the end, I chose nodemation.
'node-' in the sense that it uses a Node-View and that it uses Node.js and '-mation' for 'automation' which is
what the project is supposed to help with. However, I did not like how long the name was and I could not
imagine writing something that long every time in the CLI. That is when I then ended up on 'n8n'." - **Jan
Oberhauser, Founder and CEO, n8n.io**
## Development setup
Have you found a bug :bug: ? Or maybe you have a nice feature :sparkles: to contribute ? The
[CONTRIBUTING guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md) will help you get your
development environment ready in minutes.
Need help? Our community forum is the place to get support and connect with other users:
[community.n8n.io](https://community.n8n.io)
## License
n8n is [fair-code](https://faircode.io) distributed under the
[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/LICENSE.md) and the
[**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/LICENSE_EE.md).
n8n is [fair-code](https://faircode.io) distributed under the [Sustainable Use License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md) and [n8n Enterprise License](https://github.com/n8n-io/n8n/blob/master/LICENSE_EE.md).
Proprietary licenses are available for enterprise customers. [Get in touch](mailto:license@n8n.io)
- **Source Available**: Always visible source code
- **Self-Hostable**: Deploy anywhere
- **Extensible**: Add your own nodes and functionality
Additional information about the license model can be found in the
[docs](https://docs.n8n.io/reference/license/).
[Enterprise licenses](mailto:license@n8n.io) available for additional features and support.
Additional information about the license model can be found in the [docs](https://docs.n8n.io/reference/license/).
## Contributing
Found a bug 🐛 or have a feature idea ✨? Check our [Contributing Guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md) to get started.
## Join the Team
Want to shape the future of automation? Check out our [job posts](https://n8n.io/careers) and join our team!
## What does n8n mean?
**Short answer:** It means "nodemation" and is pronounced as n-eight-n.
**Long answer:** "I get that question quite often (more often than I expected) so I decided it is probably best to answer it here. While looking for a good name for the project with a free domain I realized very quickly that all the good ones I could think of were already taken. So, in the end, I chose nodemation. 'node-' in the sense that it uses a Node-View and that it uses Node.js and '-mation' for 'automation' which is what the project is supposed to help with. However, I did not like how long the name was and I could not imagine writing something that long every time in the CLI. That is when I then ended up on 'n8n'." - **Jan Oberhauser, Founder and CEO, n8n.io**

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View file

@ -0,0 +1,29 @@
/**
* Getters
*/
export const getExecutionsSidebar = () => cy.getByTestId('executions-sidebar');
export const getWorkflowExecutionPreviewIframe = () => cy.getByTestId('workflow-preview-iframe');
export const getExecutionPreviewBody = () =>
getWorkflowExecutionPreviewIframe()
.its('0.contentDocument.body')
.then((el) => cy.wrap(el));
export const getExecutionPreviewBodyNodes = () =>
getExecutionPreviewBody().findChildByTestId('canvas-node');
export const getExecutionPreviewBodyNodesByName = (name: string) =>
getExecutionPreviewBody().findChildByTestId('canvas-node').filter(`[data-name="${name}"]`).eq(0);
export function getExecutionPreviewOutputPanelRelatedExecutionLink() {
return getExecutionPreviewBody().findChildByTestId('related-execution-link');
}
/**
* Actions
*/
export const openExecutionPreviewNode = (name: string) =>
getExecutionPreviewBodyNodesByName(name).dblclick();

View file

@ -3,7 +3,7 @@
*/
export function getManualChatModal() {
return cy.getByTestId('lmChat-modal');
return cy.getByTestId('canvas-chat');
}
export function getManualChatInput() {
@ -19,11 +19,11 @@ export function getManualChatMessages() {
}
export function getManualChatModalCloseButton() {
return getManualChatModal().get('.el-dialog__close');
return cy.getByTestId('workflow-chat-button');
}
export function getManualChatModalLogs() {
return getManualChatModal().getByTestId('lm-chat-logs');
return cy.getByTestId('canvas-chat-logs');
}
export function getManualChatDialog() {
return getManualChatModal().getByTestId('workflow-lm-chat-dialog');

View file

@ -35,7 +35,12 @@ export function setCredentialConnectionParameterInputByName(name: string, value:
}
export function saveCredential() {
getCredentialSaveButton().click({ force: true });
getCredentialSaveButton()
.click({ force: true })
.within(() => {
cy.get('button').should('not.exist');
});
getCredentialSaveButton().should('have.text', 'Saved');
}
export function closeCredentialModal() {

View file

@ -40,10 +40,50 @@ export function getOutputPanelDataContainer() {
return getOutputPanel().getByTestId('ndv-data-container');
}
export function getOutputTableRows() {
return getOutputPanelDataContainer().find('table tr');
}
export function getOutputTableRow(row: number) {
return getOutputTableRows().eq(row);
}
export function getOutputTableHeaders() {
return getOutputPanelDataContainer().find('table thead th');
}
export function getOutputTableHeaderByText(text: string) {
return getOutputTableHeaders().contains(text);
}
export function getOutputTbodyCell(row: number, col: number) {
return getOutputTableRows().eq(row).find('td').eq(col);
}
export function getOutputRunSelector() {
return getOutputPanel().findChildByTestId('run-selector');
}
export function getOutputRunSelectorInput() {
return getOutputRunSelector().find('input');
}
export function getOutputPanelTable() {
return getOutputPanelDataContainer().get('table');
}
export function getRunDataInfoCallout() {
return cy.getByTestId('run-data-callout');
}
export function getOutputPanelItemsCount() {
return getOutputPanel().getByTestId('ndv-items-count');
}
export function getOutputPanelRelatedExecutionLink() {
return getOutputPanel().getByTestId('related-execution-link');
}
/**
* Actions
*/
@ -82,3 +122,8 @@ export function setParameterSelectByContent(name: string, content: string) {
getParameterInputByName(name).realClick();
getVisibleSelect().find('.option-headline').contains(content).click();
}
export function changeOutputRunSelector(runName: string) {
getOutputRunSelector().click();
getVisibleSelect().find('.el-select-dropdown__item').contains(runName).click();
}

View file

@ -6,11 +6,40 @@ const credentialsModal = new CredentialsModal();
export const getHomeButton = () => cy.getByTestId('project-home-menu-item');
export const getMenuItems = () => cy.getByTestId('project-menu-item');
export const getAddProjectButton = () =>
cy.getByTestId('add-project-menu-item').should('contain', 'Add project').should('be.visible');
export const getAddProjectButton = () => {
cy.getByTestId('universal-add').should('be.visible').click();
cy.getByTestId('universal-add')
.find('.el-sub-menu__title')
.as('menuitem')
.should('have.attr', 'aria-describedby');
cy.get('@menuitem')
.invoke('attr', 'aria-describedby')
.then((el) => cy.get(`[id="${el}"]`))
.as('submenu');
cy.get('@submenu').within((submenu) =>
cy
.wrap(submenu)
.getByTestId('navigation-menu-item')
.should('be.visible')
.filter(':contains("Project")')
.as('button'),
);
return cy.get('@button');
};
export const getAddFirstProjectButton = () => cy.getByTestId('add-first-project-button');
export const getIconPickerButton = () => cy.getByTestId('icon-picker-button');
export const getIconPickerTab = (tab: string) => cy.getByTestId('icon-picker-tabs').contains(tab);
export const getIconPickerIcons = () => cy.getByTestId('icon-picker-icon');
export const getIconPickerEmojis = () => cy.getByTestId('icon-picker-emoji');
// export const getAddProjectButton = () =>
// cy.getByTestId('universal-add').should('contain', 'Add project').should('be.visible');
export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a');
export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]');
export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]');
export const getProjectTabExecutions = () => getProjectTabs().filter('a[href$="/executions"]');
export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]');
export const getProjectSettingsNameInput = () =>
cy.getByTestId('project-settings-name-input').find('input');

View file

@ -69,6 +69,25 @@ export function getNodeCreatorPlusButton() {
return cy.getByTestId('node-creator-plus-button');
}
export function getCanvasNodes() {
return cy.ifCanvasVersion(
() => cy.getByTestId('canvas-node'),
() => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'),
);
}
export function getCanvasNodeByName(nodeName: string) {
return getCanvasNodes().filter(`:contains(${nodeName})`);
}
export function getSaveButton() {
return cy.getByTestId('workflow-save-button');
}
export function getZoomToFitButton() {
return cy.getByTestId('zoom-to-fit');
}
/**
* Actions
*/
@ -163,3 +182,24 @@ export function clickManualChatButton() {
export function openNode(nodeName: string) {
getNodeByName(nodeName).dblclick();
}
export function saveWorkflowOnButtonClick() {
cy.intercept('POST', '/rest/workflows').as('createWorkflow');
getSaveButton().should('contain', 'Save');
getSaveButton().click();
getSaveButton().should('contain', 'Saved');
cy.url().should('not.have.string', '/new');
}
export function pasteWorkflow(workflow: object) {
cy.get('body').paste(JSON.stringify(workflow));
}
export function clickZoomToFit() {
getZoomToFitButton().click();
}
export function deleteNode(name: string) {
getCanvasNodeByName(name).first().click();
cy.get('body').type('{del}');
}

View file

@ -20,6 +20,7 @@ describe('Undo/Redo', () => {
WorkflowPage.actions.visit();
});
// FIXME: Canvas V2: Fix redo connections
it('should undo/redo adding node in the middle', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -114,6 +115,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});
// FIXME: Canvas V2: Fix moving of nodes via e2e tests
it('should undo/redo moving nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -146,18 +148,14 @@ describe('Undo/Redo', () => {
it('should undo/redo deleting a connection using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().realHover();
cy.get('.connection-actions .delete')
.filter(':visible')
.should('be.visible')
.click({ force: true });
WorkflowPage.actions.deleteNodeBetweenNodes(SCHEDULE_TRIGGER_NODE_NAME, CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});
// FIXME: Canvas V2: Fix disconnecting by moving
it('should undo/redo deleting a connection by moving it away', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -206,6 +204,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.disabledNodes().should('have.length', 2);
});
// FIXME: Canvas V2: Fix undo renaming node
it('should undo/redo renaming node using keyboard shortcut', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -244,6 +243,7 @@ describe('Undo/Redo', () => {
});
});
// FIXME: Canvas V2: Figure out why moving doesn't work from e2e
it('should undo/redo multiple steps', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);

View file

@ -129,7 +129,7 @@ describe('Inline expression editor', () => {
// Run workflow
ndv.actions.close();
WorkflowPage.actions.executeNode('No Operation');
WorkflowPage.actions.executeNode('No Operation', { anchor: 'topLeft' });
WorkflowPage.actions.openNode('Hacker News');
WorkflowPage.actions.openInlineExpressionEditor();

View file

@ -16,6 +16,7 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.visit();
});
// FIXME: Canvas V2: Missing execute button if no nodes
it('should render canvas', () => {
WorkflowPage.getters.nodeViewRoot().should('be.visible');
WorkflowPage.getters.canvasPlusButton().should('be.visible');
@ -25,10 +26,11 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.executeWorkflowButton().should('be.visible');
});
// FIXME: Canvas V2: Fix changing of connection
it('should connect and disconnect a simple node', () => {
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
cy.get('.jtk-connector').should('have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.getters.nodeViewBackground().click(600, 400, { force: true });
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
@ -40,16 +42,16 @@ describe('Canvas Actions', () => {
);
WorkflowPage.getters
.canvasNodeInputEndpointByName(`${EDIT_FIELDS_SET_NODE_NAME}1`)
.should('have.class', 'jtk-endpoint-connected');
.getConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, `${EDIT_FIELDS_SET_NODE_NAME}1`)
.should('be.visible');
cy.get('.jtk-connector').should('have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
// Disconnect Set1
cy.drag(
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
[-200, 100],
);
cy.get('.jtk-connector').should('have.length', 0);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});
it('should add first step', () => {
@ -74,7 +76,7 @@ describe('Canvas Actions', () => {
it('should add a connected node using plus endpoint', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
cy.get('.plus-endpoint').should('be.visible').click();
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}');
@ -85,7 +87,7 @@ describe('Canvas Actions', () => {
it('should add a connected node dragging from node creator', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
cy.get('.plus-endpoint').should('be.visible').click();
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
cy.drag(WorkflowPage.getters.nodeCreatorNodeItems().first(), [100, 100], {
@ -99,7 +101,7 @@ describe('Canvas Actions', () => {
it('should open a category when trying to drag and drop it on the canvas', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
cy.get('.plus-endpoint').should('be.visible').click();
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
cy.drag(WorkflowPage.getters.nodeCreatorActionItems().first(), [100, 100], {
@ -114,7 +116,7 @@ describe('Canvas Actions', () => {
it('should add disconnected node if nothing is selected', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
// Deselect nodes
WorkflowPage.getters.nodeViewBackground().click({ force: true });
WorkflowPage.getters.nodeView().click({ force: true });
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
@ -136,10 +138,10 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 3);
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left'));
const editFieldsNodeLeft = WorkflowPage.getters.getNodeLeftPosition($editFieldsNode);
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
const httpNodeLeft = parseFloat($httpNode.css('left'));
const httpNodeLeft = WorkflowPage.getters.getNodeLeftPosition($httpNode);
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
});
});
@ -159,10 +161,12 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().first().realHover();
cy.get('.connection-actions .delete').first().click({ force: true });
WorkflowPage.actions.deleteNodeBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});
// FIXME: Canvas V2: Fix disconnecting of connection by dragging it
it('should delete a connection by moving it away from endpoint', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
@ -216,10 +220,10 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitCopy();
successToast().should('contain', 'Copied!');
successToast().should('contain', 'Copied to clipboard');
WorkflowPage.actions.copyNode(CODE_NODE_NAME);
successToast().should('contain', 'Copied!');
successToast().should('contain', 'Copied to clipboard');
});
it('should select/deselect all nodes', () => {
@ -231,17 +235,31 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.selectedNodes().should('have.length', 0);
});
// FIXME: Canvas V2: Selection via arrow keys is broken
it('should select nodes using arrow keys', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500);
cy.get('body').type('{leftArrow}');
WorkflowPage.getters.canvasNodes().first().should('have.class', 'jtk-drag-selected');
const selectedCanvasNodes = () =>
cy.ifCanvasVersion(
() => WorkflowPage.getters.canvasNodes(),
() => WorkflowPage.getters.canvasNodes().parent(),
);
cy.ifCanvasVersion(
() => selectedCanvasNodes().first().should('have.class', 'jtk-drag-selected'),
() => selectedCanvasNodes().first().should('have.class', 'selected'),
);
cy.get('body').type('{rightArrow}');
WorkflowPage.getters.canvasNodes().last().should('have.class', 'jtk-drag-selected');
cy.ifCanvasVersion(
() => selectedCanvasNodes().last().should('have.class', 'jtk-drag-selected'),
() => selectedCanvasNodes().last().should('have.class', 'selected'),
);
});
// FIXME: Canvas V2: Selection via shift and arrow keys is broken
it('should select nodes using shift and arrow keys', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
@ -251,6 +269,7 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.selectedNodes().should('have.length', 2);
});
// FIXME: Canvas V2: Fix select & deselect
it('should not break lasso selection when dragging node action buttons', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters
@ -262,6 +281,7 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
});
// FIXME: Canvas V2: Fix select & deselect
it('should not break lasso selection with multiple clicks on node action buttons', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);

View file

@ -9,6 +9,7 @@ import {
} from './../constants';
import { NDV, WorkflowExecutionsTab } from '../pages';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { isCanvasV2 } from '../utils/workflowUtils';
const WorkflowPage = new WorkflowPageClass();
const ExecutionsTab = new WorkflowExecutionsTab();
@ -52,15 +53,15 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.reload();
cy.waitForLoad();
// Make sure outputless switch was connected correctly
cy.get(
`[data-target-node="${SWITCH_NODE_NAME}1"][data-source-node="${EDIT_FIELDS_SET_NODE_NAME}3"]`,
).should('be.visible');
WorkflowPage.getters
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}3`, `${SWITCH_NODE_NAME}1`)
.should('exist');
// Make sure all connections are there after reload
for (let i = 0; i < desiredOutputs; i++) {
const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`;
WorkflowPage.getters
.canvasNodeInputEndpointByName(setName)
.should('have.class', 'jtk-endpoint-connected');
.getConnectionBetweenNodes(`${SWITCH_NODE_NAME}`, setName)
.should('exist');
}
});
@ -69,9 +70,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
for (let i = 0; i < 2; i++) {
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
WorkflowPage.getters
.nodeViewBackground()
.click((i + 1) * 200, (i + 1) * 200, { force: true });
WorkflowPage.getters.nodeView().click((i + 1) * 200, (i + 1) * 200, { force: true });
}
WorkflowPage.actions.zoomToFit();
@ -84,8 +83,6 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
);
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 2);
// Connect Set1 and Set2 to merge
cy.draganddrop(
WorkflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME),
@ -95,20 +92,36 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`),
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
);
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
const checkConnections = () => {
WorkflowPage.getters
.getConnectionBetweenNodes(
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
`${EDIT_FIELDS_SET_NODE_NAME}1`,
)
.should('exist');
WorkflowPage.getters
.getConnectionBetweenNodes(EDIT_FIELDS_SET_NODE_NAME, MERGE_NODE_NAME)
.should('exist');
WorkflowPage.getters
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}1`, MERGE_NODE_NAME)
.should('exist');
};
checkConnections();
// Make sure all connections are there after save & reload
WorkflowPage.actions.saveWorkflowOnButtonClick();
cy.reload();
cy.waitForLoad();
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
checkConnections();
// cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
WorkflowPage.actions.executeWorkflow();
WorkflowPage.getters.stopExecutionButton().should('not.exist');
// If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node
cy.get('[data-label="2 items"]').should('be.visible');
cy.ifCanvasVersion(
() => cy.get('[data-label="2 items"]').should('be.visible'),
() => cy.getByTestId('canvas-node-output-handle').contains('2 items').should('be.visible'),
);
});
it('should add nodes and check execution success', () => {
@ -120,16 +133,42 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.executeWorkflow();
cy.get('.jtk-connector.success').should('have.length', 3);
cy.get('.data-count').should('have.length', 4);
cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success');
cy.ifCanvasVersion(
() => cy.get('.jtk-connector.success').should('have.length', 3),
() => cy.get('[data-edge-status=success]').should('have.length', 3),
);
cy.ifCanvasVersion(
() => cy.get('.data-count').should('have.length', 4),
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
);
cy.ifCanvasVersion(
() => cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'),
() => cy.getByTestId('canvas-handle-plus').should('have.attr', 'data-plus-type', 'success'),
);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.zoomToFit();
cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success');
cy.get('.jtk-connector.success').should('have.length', 3);
cy.get('.jtk-connector').should('have.length', 4);
cy.ifCanvasVersion(
() =>
cy
.get('.plus-draggable-endpoint')
.filter(':visible')
.should('not.have.class', 'ep-success'),
() =>
cy.getByTestId('canvas-handle-plus').should('not.have.attr', 'data-plus-type', 'success'),
);
cy.ifCanvasVersion(
() => cy.get('.jtk-connector.success').should('have.length', 3),
// The new version of the canvas correctly shows executed data being passed to the input of the next node
() => cy.get('[data-edge-status=success]').should('have.length', 4),
);
cy.ifCanvasVersion(
() => cy.get('.data-count').should('have.length', 4),
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
);
});
it('should delete node using context menu', () => {
@ -194,19 +233,29 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodes().should('have.length', 0);
});
// FIXME: Canvas V2: Figure out how to test moving of the node
it('should move node', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.zoomToFit();
WorkflowPage.getters
.canvasNodes()
.last()
.then(($node) => {
const { left, top } = $node.position();
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
clickToFinish: true,
});
if (isCanvasV2()) {
cy.drag('.vue-flow__node', [300, 300], {
realMouse: true,
});
} else {
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
clickToFinish: true,
});
}
WorkflowPage.getters
.canvasNodes()
.last()
@ -218,91 +267,80 @@ describe('Canvas Node Manipulation and Navigation', () => {
});
});
it('should zoom in', () => {
WorkflowPage.getters.zoomInButton().should('be.visible').click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${ZOOM_IN_X1_FACTOR}, 0, 0, ${ZOOM_IN_X1_FACTOR}, 0, 0)`,
describe('Canvas Zoom Functionality', () => {
const getContainer = () =>
cy.ifCanvasVersion(
() => WorkflowPage.getters.nodeView(),
() => WorkflowPage.getters.canvasViewport(),
);
WorkflowPage.getters.zoomInButton().click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${ZOOM_IN_X2_FACTOR}, 0, 0, ${ZOOM_IN_X2_FACTOR}, 0, 0)`,
);
});
const checkZoomLevel = (expectedFactor: number) => {
return getContainer().should(($nodeView) => {
const newTransform = $nodeView.css('transform');
const newScale = parseFloat(newTransform.split(',')[0].slice(7));
it('should zoom out', () => {
WorkflowPage.getters.zoomOutButton().should('be.visible').click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${ZOOM_OUT_X1_FACTOR}, 0, 0, ${ZOOM_OUT_X1_FACTOR}, 0, 0)`,
);
WorkflowPage.getters.zoomOutButton().click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${ZOOM_OUT_X2_FACTOR}, 0, 0, ${ZOOM_OUT_X2_FACTOR}, 0, 0)`,
);
});
expect(newScale).to.be.closeTo(expectedFactor, 0.2);
});
};
it('should zoom using scroll or pinch gesture', () => {
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${PINCH_ZOOM_IN_FACTOR}, 0, 0, ${PINCH_ZOOM_IN_FACTOR}, 0, 0)`,
const zoomAndCheck = (action: 'zoomIn' | 'zoomOut', expectedFactor: number) => {
WorkflowPage.getters[`${action}Button`]().click();
checkZoomLevel(expectedFactor);
};
it('should zoom in', () => {
WorkflowPage.getters.zoomInButton().should('be.visible');
getContainer().then(($nodeView) => {
const initialTransform = $nodeView.css('transform');
const initialScale =
initialTransform === 'none' ? 1 : parseFloat(initialTransform.split(',')[0].slice(7));
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X1_FACTOR);
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X2_FACTOR);
});
});
it('should zoom out', () => {
zoomAndCheck('zoomOut', ZOOM_OUT_X1_FACTOR);
zoomAndCheck('zoomOut', ZOOM_OUT_X2_FACTOR);
});
it('should zoom using scroll or pinch gesture', () => {
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
// V2 Canvas is using the same zoom factor for both pinch and scroll
cy.ifCanvasVersion(
() => checkZoomLevel(PINCH_ZOOM_IN_FACTOR),
() => checkZoomLevel(ZOOM_IN_X1_FACTOR),
);
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.actions.pinchToZoom(1, 'zoomOut');
checkZoomLevel(1); // Zoom in 1x + Zoom out 1x should reset to default (=1)
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${PINCH_ZOOM_OUT_FACTOR}, 0, 0, ${PINCH_ZOOM_OUT_FACTOR}, 0, 0)`,
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
cy.ifCanvasVersion(
() => checkZoomLevel(PINCH_ZOOM_OUT_FACTOR),
() => checkZoomLevel(ZOOM_OUT_X1_FACTOR),
);
});
});
it('should reset zoom', () => {
// Reset zoom should not appear until zoom level changed
WorkflowPage.getters.resetZoomButton().should('not.exist');
WorkflowPage.getters.zoomInButton().click();
WorkflowPage.getters.resetZoomButton().should('be.visible').click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${DEFAULT_ZOOM_FACTOR}, 0, 0, ${DEFAULT_ZOOM_FACTOR}, 0, 0)`,
);
});
it('should reset zoom', () => {
WorkflowPage.getters.resetZoomButton().should('not.exist');
WorkflowPage.getters.zoomInButton().click();
WorkflowPage.getters.resetZoomButton().should('be.visible').click();
checkZoomLevel(DEFAULT_ZOOM_FACTOR);
});
it('should zoom to fit', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
// At this point last added node should be off-screen
WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
WorkflowPage.getters.zoomToFitButton().click();
WorkflowPage.getters.canvasNodes().last().should('be.visible');
it('should zoom to fit', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
// At this point last added node should be off-screen
WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
WorkflowPage.getters.zoomToFitButton().click();
WorkflowPage.getters.canvasNodes().last().should('be.visible');
});
});
it('should disable node (context menu or shortcut)', () => {
@ -426,9 +464,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.reload();
cy.waitForLoad();
WorkflowPage.getters.canvasNodes().should('have.length', 2);
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
});
// FIXME: Canvas V2: Credentials should show issue on the first open
it('should remove unknown credentials on pasting workflow', () => {
cy.fixture('workflow-with-unknown-credentials.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
@ -441,6 +479,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
});
});
// FIXME: Canvas V2: Unknown nodes should still render connection endpoints
it('should render connections correctly if unkown nodes are present', () => {
const unknownNodeName = 'Unknown node';
cy.createFixtureWorkflow('workflow-with-unknown-nodes.json', 'Unknown nodes');

View file

@ -112,7 +112,7 @@ describe('Data pinning', () => {
it('Should be able to pin data from canvas (context menu or shortcut)', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
workflowPage.actions.openContextMenu(EDIT_FIELDS_SET_NODE_NAME, 'overflow-button');
workflowPage.actions.openContextMenu(EDIT_FIELDS_SET_NODE_NAME, { method: 'overflow-button' });
workflowPage.getters
.contextMenuAction('toggle_pin')
.parent()

View file

@ -41,7 +41,9 @@ describe('Data mapping', () => {
ndv.actions.mapDataFromHeader(1, 'value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.getters.parameterExpressionPreview('value').should('include.text', '2024');
ndv.getters
.parameterExpressionPreview('value')
.should('include.text', new Date().getFullYear());
ndv.actions.mapDataFromHeader(2, 'value');
ndv.getters
@ -185,7 +187,6 @@ describe('Data mapping', () => {
workflowPage.actions.openNode('Set1');
ndv.actions.executePrevious();
ndv.actions.expandSchemaViewNode(SCHEDULE_TRIGGER_NODE_NAME);
const dataPill = ndv.getters
.inputDataContainer()

View file

@ -16,12 +16,14 @@ describe('n8n Form Trigger', () => {
ndv.getters.parameterInput('formDescription').type('Test Form Description');
ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
ndv.getters.backToCanvas().click();
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist');
});
it('should fill up form fields', () => {
workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger');
workflowPage.getters.canvasNodes().first().dblclick();
workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger', {
isTrigger: true,
action: 'On new n8n Form event',
});
ndv.getters.parameterInput('formTitle').type('Test Form');
ndv.getters.parameterInput('formDescription').type('Test Form Description');
//fill up first field of type number
@ -42,8 +44,7 @@ describe('n8n Form Trigger', () => {
':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item',
)
.find('input[placeholder*="e.g. What is your name?"]')
.type('Test Field 3')
.blur();
.type('Test Field 3');
cy.get(
':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item',
).click();
@ -54,27 +55,24 @@ describe('n8n Form Trigger', () => {
':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item',
)
.find('input[placeholder*="e.g. What is your name?"]')
.type('Test Field 4')
.blur();
.type('Test Field 4');
cy.get(
':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item',
).click();
getVisibleSelect().contains('Dropdown').click();
cy.get(
'.border-top-dashed > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > :nth-child(2) > .button',
).click();
cy.get(
':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(1)',
)
.find('input')
.type('Option 1')
.blur();
cy.get(
':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(2)',
)
.find('input')
.type('Option 2')
.blur();
cy.contains('button', 'Add Field Option').click();
cy.contains('label', 'Field Options')
.parent()
.nextAll()
.find('[data-test-id="parameter-input-field"]')
.eq(0)
.type('Option 1');
cy.contains('label', 'Field Options')
.parent()
.nextAll()
.find('[data-test-id="parameter-input-field"]')
.eq(1)
.type('Option 2');
//add optional submitted message
cy.get('.param-options').click();
@ -92,10 +90,9 @@ describe('n8n Form Trigger', () => {
.children()
.children()
.first()
.clear()
.type('Your test form was successfully submitted');
ndv.getters.backToCanvas().click();
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist');
});
});

View file

@ -1,3 +1,4 @@
import { saveCredential } from '../composables/modals/credential-modal';
import * as projects from '../composables/projects';
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN, NOTION_NODE_NAME } from '../constants';
import {
@ -88,7 +89,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 1);
workflowsPage.getters.workflowCard('Workflow W1').click();
workflowsPage.getters.workflowCardContent('Workflow W1').click();
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
ndv.getters.credentialInput().find('input').should('have.value', 'Credential C2');
ndv.actions.close();
@ -104,7 +105,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCard('Workflow W1').click();
workflowsPage.getters.workflowCardContent('Workflow W1').click();
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
ndv.getters.credentialInput().find('input').should('have.value', 'Credential C2');
ndv.actions.close();
@ -133,7 +134,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCard('Workflow W1').click();
workflowsPage.getters.workflowCardContent('Workflow W1').click();
workflowPage.actions.openNode('Notion');
ndv.getters
.credentialInput()
@ -144,7 +145,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
cy.waitForLoad();
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard('Workflow W2').click('top');
workflowsPage.getters.workflowCardContent('Workflow W2').click('top');
workflowPage.actions.executeWorkflow();
});
@ -225,7 +226,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
.filter(':contains("Development")')
.should('have.length', 1)
.click();
credentialsModal.getters.saveButton().click();
saveCredential();
credentialsModal.actions.close();
projects.getProjectTabWorkflows().click();
@ -251,13 +252,13 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsModal.actions.changeTab('Sharing');
credentialsModal.getters.usersSelect().click();
getVisibleSelect().find('li').should('have.length', 4).first().click();
credentialsModal.getters.saveButton().click();
saveCredential();
credentialsModal.actions.close();
credentialsPage.getters
.credentialCards()
.should('have.length', 2)
.filter(':contains("Owned by me")')
.filter(':contains("Personal")')
.should('have.length', 1);
});
});
@ -351,7 +352,7 @@ describe('Credential Usage in Cross Shared Workflows', () => {
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard(workflowName).click();
workflowsPage.getters.workflowCardContent(workflowName).click();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Only the own credential the shared one (+ the 'Create new' option)
@ -396,7 +397,7 @@ describe('Credential Usage in Cross Shared Workflows', () => {
credentialsPage.getters.createCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard(workflowName).click();
workflowsPage.getters.workflowCardContent(workflowName).click();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Only the personal credentials of the workflow owner and the global owner

View file

@ -148,24 +148,9 @@ describe('User Management', { disableAutoLogin: true }, () => {
personalSettingsPage.actions.changeTheme('Dark');
cy.get('body').should('have.attr', 'data-theme', 'dark');
settingsSidebar.actions.back();
mainSidebar.getters
.logo()
.should('have.attr', 'src')
.then((src) => {
expect(src).to.include('/static/logo/channel/dev-dark.svg');
});
cy.visit(personalSettingsPage.url);
personalSettingsPage.actions.changeTheme('Light');
cy.get('body').should('have.attr', 'data-theme', 'light');
settingsSidebar.actions.back();
mainSidebar.getters
.logo()
.should('have.attr', 'src')
.then((src) => {
expect(src).to.include('/static/logo/channel/dev.svg');
});
});
it('should delete user and their data', () => {

View file

@ -1,6 +1,7 @@
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { clearNotifications, errorToast, successToast } from '../pages/notifications';
import { isCanvasV2 } from '../utils/workflowUtils';
const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();
@ -117,15 +118,22 @@ describe('Execution', () => {
.canvasNodeByName('Manual')
.within(() => cy.get('.fa-check'))
.should('exist');
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
if (isCanvasV2()) {
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
} else {
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
}
workflowPage.getters
.canvasNodeByName('Set')
.within(() => cy.get('.fa-check').should('not.exist'));
successToast().should('be.visible');
clearNotifications();
// Clear execution data
workflowPage.getters.clearExecutionDataButton().should('be.visible');
@ -206,6 +214,7 @@ describe('Execution', () => {
workflowPage.getters.clearExecutionDataButton().should('not.exist');
});
// FIXME: Canvas V2: Webhook should show waiting state but it doesn't
it('should test webhook workflow stop', () => {
cy.createFixtureWorkflow('Webhook_wait_set.json');
@ -267,9 +276,17 @@ describe('Execution', () => {
.canvasNodeByName('Webhook')
.within(() => cy.get('.fa-check'))
.should('exist');
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
if (isCanvasV2()) {
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
} else {
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
}
workflowPage.getters
.canvasNodeByName('Set')
.within(() => cy.get('.fa-check').should('not.exist'));
@ -295,6 +312,7 @@ describe('Execution', () => {
});
});
// FIXME: Canvas V2: Missing pinned states for `edge-label-wrapper`
describe('connections should be colored differently for pinned data', () => {
beforeEach(() => {
cy.createFixtureWorkflow('Schedule_pinned.json');

View file

@ -1,5 +1,6 @@
import { type ICredentialType } from 'n8n-workflow';
import { getCredentialSaveButton, saveCredential } from '../composables/modals/credential-modal';
import {
AGENT_NODE_NAME,
AI_TOOL_HTTP_NODE_NAME,
@ -15,7 +16,7 @@ import {
TRELLO_NODE_NAME,
} from '../constants';
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
import { successToast } from '../pages/notifications';
import { errorToast, successToast } from '../pages/notifications';
import { getVisibleSelect } from '../utils';
const credentialsPage = new CredentialsPage();
@ -26,6 +27,22 @@ const nodeDetailsView = new NDV();
const NEW_CREDENTIAL_NAME = 'Something else';
const NEW_CREDENTIAL_NAME2 = 'Something else entirely';
function createNotionCredential() {
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
workflowPage.actions.openNode(NOTION_NODE_NAME);
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
credentialsModal.actions.fillCredentialsForm();
cy.get('body').type('{esc}');
workflowPage.actions.deleteNode(NOTION_NODE_NAME);
}
function deleteSelectedCredential() {
workflowPage.getters.nodeCredentialsEditButton().click();
credentialsModal.getters.deleteButton().click();
cy.get('.el-message-box').find('button').contains('Yes').click();
}
describe('Credentials', () => {
beforeEach(() => {
cy.visit(credentialsPage.url);
@ -178,7 +195,7 @@ describe('Credentials', () => {
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.name().click();
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME);
credentialsModal.getters.saveButton().click();
saveCredential();
credentialsModal.getters.closeButton().click();
workflowPage.getters
.nodeCredentialsSelect()
@ -196,7 +213,7 @@ describe('Credentials', () => {
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.name().click();
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME2);
credentialsModal.getters.saveButton().click();
saveCredential();
credentialsModal.getters.closeButton().click();
workflowPage.getters
.nodeCredentialsSelect()
@ -221,7 +238,7 @@ describe('Credentials', () => {
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.name().click();
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME);
credentialsModal.getters.saveButton().click();
saveCredential();
credentialsModal.getters.closeButton().click();
workflowPage.getters
.nodeCredentialsSelect()
@ -229,6 +246,40 @@ describe('Credentials', () => {
.should('have.value', NEW_CREDENTIAL_NAME);
});
it('should set a default credential when adding nodes', () => {
workflowPage.actions.visit();
createNotionCredential();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_NOTION_ACCOUNT_NAME);
deleteSelectedCredential();
});
it('should set a default credential when editing a node', () => {
workflowPage.actions.visit();
createNotionCredential();
workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true);
nodeDetailsView.getters.parameterInput('authentication').click();
getVisibleSelect().find('li').contains('Predefined').click();
nodeDetailsView.getters.parameterInput('nodeCredentialType').click();
getVisibleSelect().find('li').contains('Notion API').click();
workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_NOTION_ACCOUNT_NAME);
deleteSelectedCredential();
});
it('should setup generic authentication for HTTP node', () => {
workflowPage.actions.visit();
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
@ -278,4 +329,26 @@ describe('Credentials', () => {
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
nodeDetailsView.getters.copyInput().should('not.exist');
});
it('ADO-2583 should show notifications above credential modal overlay', () => {
// check error notifications because they are sticky
cy.intercept('POST', '/rest/credentials', { forceNetworkError: true });
credentialsPage.getters.createCredentialButton().click();
credentialsModal.getters.newCredentialModal().should('be.visible');
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.actions.setName('My awesome Notion account');
getCredentialSaveButton().click();
errorToast().should('have.length', 1);
errorToast().should('be.visible');
errorToast().should('have.css', 'z-index', '2100');
cy.get('.el-overlay').should('have.css', 'z-index', '2001');
});
});

View file

@ -0,0 +1,21 @@
import { WEBHOOK_NODE_NAME } from '../constants';
import { NDV, WorkflowPage } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('ADO-2270 Save button resets on webhook node open', () => {
it('should not reset the save button if webhook node is opened and closed', () => {
workflowPage.actions.visit();
workflowPage.actions.addInitialNodeToCanvas(WEBHOOK_NODE_NAME);
workflowPage.getters.saveButton().click();
workflowPage.actions.openNode(WEBHOOK_NODE_NAME);
ndv.actions.close();
cy.ifCanvasVersion(
() => cy.getByTestId('workflow-save-button').should('not.contain', 'Saved'),
() => cy.getByTestId('workflow-save-button').should('contain', 'Saved'),
);
});
});

View file

@ -90,6 +90,14 @@ function createRunDataWithError(inputMessage: string) {
routine: 'InitPostgres',
} as unknown as Error,
} as ExecutionError,
metadata: {
subRun: [
{
node: 'Postgres Chat Memory',
runIndex: 0,
},
],
},
}),
createMockNodeExecutionData(AGENT_NODE_NAME, {
executionStatus: 'error',
@ -124,14 +132,6 @@ function createRunDataWithError(inputMessage: string) {
description: 'Internal error',
message: 'Internal error',
} as unknown as ExecutionError,
metadata: {
subRun: [
{
node: 'Postgres Chat Memory',
runIndex: 0,
},
],
},
}),
];
}

View file

@ -0,0 +1,86 @@
import { NDV, WorkflowPage } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('ADO-2362 ADO-2350 NDV Prevent clipping long parameters and scrolling to expression', () => {
it('should show last parameters and open at scroll top of parameters', () => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json');
workflowPage.actions.openNode('Schedule Trigger');
ndv.getters.inlineExpressionEditorInput().should('be.visible');
ndv.actions.close();
workflowPage.actions.openNode('Edit Fields1');
// first parameter should be visible
ndv.getters.inputLabel().eq(0).should('include.text', 'Mode');
ndv.getters.inputLabel().eq(0).should('be.visible');
ndv.getters.inlineExpressionEditorInput().should('have.length', 2);
// last parameter in view should be visible
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible!');
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
// next parameter in view should not be visible
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
ndv.actions.close();
workflowPage.actions.openNode('Schedule Trigger');
// first parameter (notice) should be visible
ndv.getters.nthParam(0).should('include.text', 'This workflow will run on the schedule ');
ndv.getters.inputLabel().eq(0).should('be.visible');
ndv.getters.inlineExpressionEditorInput().should('have.length', 2);
// last parameter in view should be visible
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible');
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
// next parameter in view should not be visible
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
ndv.actions.close();
workflowPage.actions.openNode('Slack');
// first field (credentials) should be visible
ndv.getters.nodeCredentialsLabel().should('be.visible');
// last parameter in view should be visible
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible');
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
// next parameter in view should not be visible
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
});
it('NODE-1272 ensure expressions scrolled to top, not middle', () => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json');
workflowPage.actions.openNode('With long expression');
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
// should be scrolled at top
ndv.getters
.inlineExpressionEditorInput()
.eq(0)
.find('.cm-line')
.eq(0)
.should('have.text', '1 visible!');
ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(0).should('be.visible');
ndv.getters
.inlineExpressionEditorInput()
.eq(0)
.find('.cm-line')
.eq(6)
.should('have.text', '7 not visible!');
ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(6).should('not.be.visible');
});
});

View file

@ -31,29 +31,31 @@ describe('NDV', () => {
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(1).realHover();
ndv.actions.dragMainPanelToRight();
ndv.getters.inputTableRow(1).realMouseMove(10, 1);
ndv.getters.outputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(2).realHover();
ndv.getters.inputTableRow(2).realMouseMove(10, 1);
ndv.getters.outputTableRow(2).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(3).realHover();
ndv.getters.inputTableRow(3).realMouseMove(10, 1);
ndv.getters.outputTableRow(6).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
// output to input
ndv.getters.outputTableRow(1).realHover();
ndv.actions.dragMainPanelToLeft();
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
ndv.getters.inputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputTableRow(4).realHover();
ndv.getters.outputTableRow(4).realMouseMove(10, 1);
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputTableRow(2).realHover();
ndv.getters.outputTableRow(2).realMouseMove(10, 1);
ndv.getters.inputTableRow(2).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputTableRow(6).realHover();
ndv.getters.outputTableRow(6).realMouseMove(10, 1);
ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputTableRow(1).realHover();
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
ndv.getters.inputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
});
@ -75,31 +77,32 @@ describe('NDV', () => {
ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table');
ndv.getters.backToCanvas().realHover(); // reset to default hover
ndv.getters.backToCanvas().realMouseMove(10, 1); // reset to default hover
ndv.getters.outputHoveringItem().should('not.exist');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
ndv.actions.selectInputNode('Set1');
ndv.getters.backToCanvas().realHover(); // reset to default hover
ndv.getters.backToCanvas().realMouseMove(10, 1); // reset to default hover
ndv.getters.inputTableRow(1).should('have.text', '1000');
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(1).realHover();
cy.wait(50);
ndv.actions.dragMainPanelToRight();
ndv.getters.inputTbodyCell(1, 0).realMouseMove(10, 1);
ndv.getters.outputHoveringItem().should('have.text', '1000');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
ndv.actions.selectInputNode('Sort');
ndv.actions.dragMainPanelToLeft();
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
ndv.getters.backToCanvas().realHover(); // reset to default hover
ndv.getters.backToCanvas().realMouseMove(10, 1); // reset to default hover
ndv.getters.inputTableRow(1).should('have.text', '1111');
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(1).realHover();
cy.wait(50);
ndv.actions.dragMainPanelToRight();
ndv.getters.inputTbodyCell(1, 0).realMouseMove(10, 1);
ndv.getters.outputHoveringItem().should('have.text', '1111');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
});
@ -132,20 +135,22 @@ describe('NDV', () => {
ndv.getters.inputTableRow(1).should('have.text', '1111');
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.actions.dragMainPanelToLeft();
ndv.getters.outputTableRow(1).should('have.text', '1111');
ndv.getters.outputTableRow(1).realHover();
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
ndv.getters.outputTableRow(3).should('have.text', '4444');
ndv.getters.outputTableRow(3).realHover();
ndv.getters.outputTableRow(3).realMouseMove(10, 1);
ndv.getters.inputTableRow(3).should('have.text', '4444');
ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.actions.changeOutputRunSelector('2 of 2 (6 items)');
cy.wait(50);
ndv.getters.inputTableRow(1).should('have.text', '1000');
ndv.getters.inputTableRow(1).realHover();
ndv.actions.dragMainPanelToRight();
ndv.getters.inputTableRow(1).realMouseMove(10, 1);
ndv.getters.outputTableRow(1).should('have.text', '1000');
ndv.getters
@ -155,7 +160,8 @@ describe('NDV', () => {
.should('equal', 'hovering-item');
ndv.getters.outputTableRow(3).should('have.text', '2000');
ndv.getters.outputTableRow(3).realHover();
ndv.actions.dragMainPanelToLeft();
ndv.getters.outputTableRow(3).realMouseMove(10, 1);
ndv.getters.inputTableRow(3).should('have.text', '2000');
@ -175,14 +181,15 @@ describe('NDV', () => {
ndv.actions.switchOutputBranch('False Branch (2 items)');
ndv.getters.outputTableRow(1).should('have.text', '8888');
ndv.getters.outputTableRow(1).realHover();
ndv.actions.dragMainPanelToLeft();
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
ndv.getters.inputTableRow(5).should('have.text', '8888');
ndv.getters.inputTableRow(5).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputTableRow(2).should('have.text', '9999');
ndv.getters.outputTableRow(2).realHover();
ndv.getters.outputTableRow(2).realMouseMove(10, 1);
ndv.getters.inputTableRow(6).should('have.text', '9999');
@ -192,29 +199,35 @@ describe('NDV', () => {
workflowPage.actions.openNode('Set5');
ndv.actions.dragMainPanelToRight();
ndv.actions.switchInputBranch('True Branch');
ndv.actions.dragMainPanelToLeft();
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)');
ndv.getters.outputTableRow(1).should('have.text', '8888');
ndv.getters.outputTableRow(1).realHover();
cy.wait(100);
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
ndv.getters.inputHoveringItem().should('not.exist');
ndv.getters.inputTableRow(1).should('have.text', '1111');
ndv.getters.inputTableRow(1).realHover();
cy.wait(100);
ndv.actions.dragMainPanelToRight();
ndv.getters.inputTableRow(1).realMouseMove(10, 1);
ndv.getters.outputHoveringItem().should('not.exist');
ndv.actions.switchInputBranch('False Branch');
ndv.getters.inputTableRow(1).should('have.text', '8888');
ndv.getters.inputTableRow(1).realHover();
ndv.actions.dragMainPanelToRight();
ndv.getters.inputTableRow(1).realMouseMove(10, 1);
ndv.actions.dragMainPanelToLeft();
ndv.actions.changeOutputRunSelector('2 of 2 (4 items)');
ndv.getters.outputTableRow(1).should('have.text', '1111');
ndv.getters.outputTableRow(1).realHover();
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)');
ndv.getters.inputTableRow(1).should('have.text', '8888');
ndv.getters.inputTableRow(1).realHover();
ndv.actions.dragMainPanelToRight();
ndv.getters.inputTableRow(1).realMouseMove(10, 1);
ndv.getters.outputHoveringItem().should('have.text', '8888');
// todo there's a bug here need to fix ADO-534
// ndv.getters.outputHoveringItem().should('not.exist');

View file

@ -49,33 +49,47 @@ describe('Two-factor authentication', { disableAutoLogin: true }, () => {
cy.intercept('GET', '/rest/mfa/qr').as('getMfaQrCode');
});
it('Should be able to login with MFA token', () => {
it('Should be able to login with MFA code', () => {
const { email, password } = user;
signinPage.actions.loginWithEmailAndPassword(email, password);
personalSettingsPage.actions.enableMfa();
mainSidebar.actions.signout();
const token = generateOTPToken(user.mfaSecret);
mfaLoginPage.actions.loginWithMfaToken(email, password, token);
const mfaCode = generateOTPToken(user.mfaSecret);
mfaLoginPage.actions.loginWithMfaCode(email, password, mfaCode);
mainSidebar.actions.signout();
});
it('Should be able to login with recovery code', () => {
it('Should be able to login with MFA recovery code', () => {
const { email, password } = user;
signinPage.actions.loginWithEmailAndPassword(email, password);
personalSettingsPage.actions.enableMfa();
mainSidebar.actions.signout();
mfaLoginPage.actions.loginWithRecoveryCode(email, password, user.mfaRecoveryCodes[0]);
mfaLoginPage.actions.loginWithMfaRecoveryCode(email, password, user.mfaRecoveryCodes[0]);
mainSidebar.actions.signout();
});
it('Should be able to disable MFA in account', () => {
it('Should be able to disable MFA in account with MFA code', () => {
const { email, password } = user;
signinPage.actions.loginWithEmailAndPassword(email, password);
personalSettingsPage.actions.enableMfa();
mainSidebar.actions.signout();
const token = generateOTPToken(user.mfaSecret);
mfaLoginPage.actions.loginWithMfaToken(email, password, token);
personalSettingsPage.actions.disableMfa();
const mfaCode = generateOTPToken(user.mfaSecret);
mfaLoginPage.actions.loginWithMfaCode(email, password, mfaCode);
const disableToken = generateOTPToken(user.mfaSecret);
personalSettingsPage.actions.disableMfa(disableToken);
personalSettingsPage.getters.enableMfaButton().should('exist');
mainSidebar.actions.signout();
});
it('Should be able to disable MFA in account with recovery code', () => {
const { email, password } = user;
signinPage.actions.loginWithEmailAndPassword(email, password);
personalSettingsPage.actions.enableMfa();
mainSidebar.actions.signout();
const mfaCode = generateOTPToken(user.mfaSecret);
mfaLoginPage.actions.loginWithMfaCode(email, password, mfaCode);
personalSettingsPage.actions.disableMfa(user.mfaRecoveryCodes[0]);
personalSettingsPage.getters.enableMfaButton().should('exist');
mainSidebar.actions.signout();
});
});

View file

@ -87,11 +87,28 @@ describe('Debug', () => {
confirmDialog.get('.btn--confirm').click();
cy.url().should('include', '/debug');
workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon');
workflowPage.getters
.canvasNodes()
.not(':first')
.should('not.have.descendants', '.node-pin-data-icon');
cy.ifCanvasVersion(
() => {
workflowPage.getters
.canvasNodes()
.first()
.should('have.descendants', '.node-pin-data-icon');
workflowPage.getters
.canvasNodes()
.not(':first')
.should('not.have.descendants', '.node-pin-data-icon');
},
() => {
workflowPage.getters
.canvasNodes()
.first()
.should('have.descendants', '[data-test-id="canvas-node-status-pinned"]');
workflowPage.getters
.canvasNodes()
.not(':first')
.should('not.have.descendants', '[data-test-id="canvas-node-status-pinned"]');
},
);
cy.reload(true);
cy.wait(['@getExecution']);
@ -114,10 +131,22 @@ describe('Debug', () => {
confirmDialog.get('.btn--confirm').click();
cy.url().should('include', '/debug');
workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty');
cy.ifCanvasVersion(
() => {
workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty');
},
() => {
workflowPage.getters
.canvasNodes()
.last()
.find('[class*="statusIcons"]')
.should('not.exist');
},
);
workflowPage.getters.canvasNodes().first().dblclick();
ndv.getters.pinDataButton().click();
ndv.actions.unPinData();
ndv.actions.close();
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();

View file

@ -129,7 +129,6 @@ describe('Workflow templates', () => {
workflowPage.actions.shouldHaveWorkflowName('Demo: ' + OnboardingWorkflow.name);
workflowPage.getters.canvasNodes().should('have.length', 4);
workflowPage.getters.stickies().should('have.length', 1);
workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon');
});
it('can import template', () => {
@ -142,6 +141,7 @@ describe('Workflow templates', () => {
});
it('should save template id with the workflow', () => {
cy.intercept('POST', '/rest/workflows').as('saveWorkflow');
templatesPage.actions.importTemplate();
cy.visit(templatesPage.url);
@ -159,10 +159,8 @@ describe('Workflow templates', () => {
workflowPage.actions.hitSelectAll();
workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
// Check workflow JSON by copying it to clipboard
cy.readClipboard().then((workflowJSON) => {
expect(workflowJSON).to.contain('"templateId": "1"');
cy.wait('@saveWorkflow').then((interception) => {
expect(interception.request.body.meta.templateId).to.equal('1');
});
});

View file

@ -0,0 +1,17 @@
import {
deleteNode,
getCanvasNodes,
navigateToNewWorkflowPage,
pasteWorkflow,
} from '../composables/workflow';
import Workflow from '../fixtures/Switch_node_with_null_connection.json';
describe('ADO-2929 can load Switch nodes', () => {
it('can load workflows with Switch nodes with null at connection index', () => {
navigateToNewWorkflowPage();
pasteWorkflow(Workflow);
getCanvasNodes().should('have.length', 3);
deleteNode('Switch');
getCanvasNodes().should('have.length', 2);
});
});

View file

@ -14,7 +14,6 @@ import {
} from './../constants';
import {
closeManualChatModal,
getManualChatDialog,
getManualChatMessages,
getManualChatModal,
getManualChatModalLogs,
@ -27,6 +26,8 @@ import {
clickCreateNewCredential,
clickExecuteNode,
clickGetBackToCanvas,
getRunDataInfoCallout,
getOutputPanelTable,
toggleParameterCheckboxInputByName,
} from '../composables/ndv';
import {
@ -44,6 +45,7 @@ import {
openNode,
getConnectionBySourceAndTarget,
} from '../composables/workflow';
import { NDV, WorkflowPage } from '../pages';
import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
describe('Langchain Integration', () => {
@ -167,7 +169,7 @@ describe('Langchain Integration', () => {
lastNodeExecuted: BASIC_LLM_CHAIN_NODE_NAME,
});
getManualChatDialog().should('contain', outputMessage);
getManualChatMessages().should('contain', outputMessage);
});
it('should be able to open and execute Agent node', () => {
@ -207,7 +209,7 @@ describe('Langchain Integration', () => {
lastNodeExecuted: AGENT_NODE_NAME,
});
getManualChatDialog().should('contain', outputMessage);
getManualChatMessages().should('contain', outputMessage);
});
it('should add and use Manual Chat Trigger node together with Agent node', () => {
@ -228,99 +230,98 @@ describe('Langchain Integration', () => {
clickManualChatButton();
getManualChatModalLogs().should('not.exist');
const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?';
const runData = [
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
jsonData: {
main: { input: inputMessage },
},
}),
createMockNodeExecutionData(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, {
jsonData: {
ai_languageModel: {
response: {
generations: [
{
text: `{
"action": "Final Answer",
"action_input": "${outputMessage}"
}`,
message: {
lc: 1,
type: 'constructor',
id: ['langchain', 'schema', 'AIMessage'],
kwargs: {
content: `{
"action": "Final Answer",
"action_input": "${outputMessage}"
}`,
additional_kwargs: {},
},
},
generationInfo: { finish_reason: 'stop' },
},
],
llmOutput: {
tokenUsage: {
completionTokens: 26,
promptTokens: 519,
totalTokens: 545,
},
},
},
},
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
inputOverride: {
ai_languageModel: [
[
{
json: {
messages: [
{
lc: 1,
type: 'constructor',
id: ['langchain', 'schema', 'SystemMessage'],
kwargs: {
content:
'Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist. However, above all else, all responses must adhere to the format of RESPONSE FORMAT INSTRUCTIONS.',
additional_kwargs: {},
},
},
{
lc: 1,
type: 'constructor',
id: ['langchain', 'schema', 'HumanMessage'],
kwargs: {
content:
'TOOLS\n------\nAssistant can ask the user to use tools to look up information that may be helpful in answering the users original question. The tools the human can use are:\n\n\n\nRESPONSE FORMAT INSTRUCTIONS\n----------------------------\n\nOutput a JSON markdown code snippet containing a valid JSON object in one of two formats:\n\n**Option 1:**\nUse this if you want the human to use a tool.\nMarkdown code snippet formatted in the following schema:\n\n```json\n{\n "action": string, // The action to take. Must be one of []\n "action_input": string // The input to the action. May be a stringified object.\n}\n```\n\n**Option #2:**\nUse this if you want to respond directly and conversationally to the human. Markdown code snippet formatted in the following schema:\n\n```json\n{\n "action": "Final Answer",\n "action_input": string // You should put what you want to return to use here and make sure to use valid json newline characters.\n}\n```\n\nFor both options, remember to always include the surrounding markdown code snippet delimiters (begin with "```json" and end with "```")!\n\n\nUSER\'S INPUT\n--------------------\nHere is the user\'s input (remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else):\n\nHello!',
additional_kwargs: {},
},
},
],
options: { stop: ['Observation:'], promptIndex: 0 },
},
},
],
],
},
}),
createMockNodeExecutionData(AGENT_NODE_NAME, {
jsonData: {
main: { output: 'Hi there! How can I assist you today?' },
},
}),
];
runMockWorkflowExecution({
trigger: () => {
sendManualChatMessage(inputMessage);
},
runData: [
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
jsonData: {
main: { input: inputMessage },
},
}),
createMockNodeExecutionData(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, {
jsonData: {
ai_languageModel: {
response: {
generations: [
{
text: `{
"action": "Final Answer",
"action_input": "${outputMessage}"
}`,
message: {
lc: 1,
type: 'constructor',
id: ['langchain', 'schema', 'AIMessage'],
kwargs: {
content: `{
"action": "Final Answer",
"action_input": "${outputMessage}"
}`,
additional_kwargs: {},
},
},
generationInfo: { finish_reason: 'stop' },
},
],
llmOutput: {
tokenUsage: {
completionTokens: 26,
promptTokens: 519,
totalTokens: 545,
},
},
},
},
},
inputOverride: {
ai_languageModel: [
[
{
json: {
messages: [
{
lc: 1,
type: 'constructor',
id: ['langchain', 'schema', 'SystemMessage'],
kwargs: {
content:
'Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist. However, above all else, all responses must adhere to the format of RESPONSE FORMAT INSTRUCTIONS.',
additional_kwargs: {},
},
},
{
lc: 1,
type: 'constructor',
id: ['langchain', 'schema', 'HumanMessage'],
kwargs: {
content:
'TOOLS\n------\nAssistant can ask the user to use tools to look up information that may be helpful in answering the users original question. The tools the human can use are:\n\n\n\nRESPONSE FORMAT INSTRUCTIONS\n----------------------------\n\nOutput a JSON markdown code snippet containing a valid JSON object in one of two formats:\n\n**Option 1:**\nUse this if you want the human to use a tool.\nMarkdown code snippet formatted in the following schema:\n\n```json\n{\n "action": string, // The action to take. Must be one of []\n "action_input": string // The input to the action. May be a stringified object.\n}\n```\n\n**Option #2:**\nUse this if you want to respond directly and conversationally to the human. Markdown code snippet formatted in the following schema:\n\n```json\n{\n "action": "Final Answer",\n "action_input": string // You should put what you want to return to use here and make sure to use valid json newline characters.\n}\n```\n\nFor both options, remember to always include the surrounding markdown code snippet delimiters (begin with "```json" and end with "```")!\n\n\nUSER\'S INPUT\n--------------------\nHere is the user\'s input (remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else):\n\nHello!',
additional_kwargs: {},
},
},
],
options: { stop: ['Observation:'], promptIndex: 0 },
},
},
],
],
},
}),
createMockNodeExecutionData(AGENT_NODE_NAME, {
jsonData: {
main: { output: 'Hi there! How can I assist you today?' },
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
}),
],
runData,
lastNodeExecuted: AGENT_NODE_NAME,
});
@ -333,6 +334,8 @@ describe('Langchain Integration', () => {
getManualChatModalLogsEntries().should('have.length', 1);
closeManualChatModal();
getManualChatModalLogs().should('not.exist');
getManualChatModal().should('not.exist');
});
it('should auto-add chat trigger and basic LLM chain when adding LLM node', () => {
@ -357,4 +360,162 @@ describe('Langchain Integration', () => {
getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist');
getNodes().should('have.length', 3);
});
it('should not auto-add nodes if ChatTrigger is already present', () => {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
addNodeToCanvas(AGENT_NODE_NAME, true);
addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true);
getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist');
getNodes().should('have.length', 3);
});
it('should render runItems for sub-nodes and allow switching between them', () => {
const workflowPage = new WorkflowPage();
const ndv = new NDV();
cy.visit(workflowPage.url);
cy.createFixtureWorkflow('In_memory_vector_store_fake_embeddings.json');
workflowPage.actions.zoomToFit();
workflowPage.actions.executeNode('Populate VS');
cy.get('[data-label="25 items"]').should('exist');
const assertInputOutputText = (text: string, assertion: 'exist' | 'not.exist') => {
ndv.getters.outputPanel().contains(text).should(assertion);
ndv.getters.inputPanel().contains(text).should(assertion);
};
workflowPage.actions.openNode('Character Text Splitter');
ndv.getters.outputRunSelector().should('exist');
ndv.getters.inputRunSelector().should('exist');
ndv.getters.inputRunSelector().find('input').should('include.value', '3 of 3');
ndv.getters.outputRunSelector().find('input').should('include.value', '3 of 3');
assertInputOutputText('Kyiv', 'exist');
assertInputOutputText('Berlin', 'not.exist');
assertInputOutputText('Prague', 'not.exist');
ndv.actions.changeOutputRunSelector('2 of 3');
assertInputOutputText('Berlin', 'exist');
assertInputOutputText('Kyiv', 'not.exist');
assertInputOutputText('Prague', 'not.exist');
ndv.actions.changeOutputRunSelector('1 of 3');
assertInputOutputText('Prague', 'exist');
assertInputOutputText('Berlin', 'not.exist');
assertInputOutputText('Kyiv', 'not.exist');
ndv.actions.toggleInputRunLinking();
ndv.actions.changeOutputRunSelector('2 of 3');
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3');
ndv.getters.outputRunSelector().find('input').should('include.value', '2 of 3');
ndv.getters.inputPanel().contains('Prague').should('exist');
ndv.getters.inputPanel().contains('Berlin').should('not.exist');
ndv.getters.outputPanel().contains('Berlin').should('exist');
ndv.getters.outputPanel().contains('Prague').should('not.exist');
ndv.actions.toggleInputRunLinking();
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3');
ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 3');
assertInputOutputText('Prague', 'exist');
assertInputOutputText('Berlin', 'not.exist');
assertInputOutputText('Kyiv', 'not.exist');
});
it('should show tool info notice if no existing tools were used during execution', () => {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
addNodeToCanvas(AGENT_NODE_NAME, true);
addLanguageModelNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AGENT_NODE_NAME,
true,
);
clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
clickGetBackToCanvas();
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();
openNode(AGENT_NODE_NAME);
const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?';
clickExecuteNode();
runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage),
runData: [
createMockNodeExecutionData(AGENT_NODE_NAME, {
jsonData: {
main: { output: outputMessage },
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
}),
],
lastNodeExecuted: AGENT_NODE_NAME,
});
closeManualChatModal();
openNode(AGENT_NODE_NAME);
getRunDataInfoCallout().should('exist');
});
it('should not show tool info notice if tools were used during execution', () => {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
addNodeToCanvas(AGENT_NODE_NAME, true, true);
getRunDataInfoCallout().should('not.exist');
clickGetBackToCanvas();
addLanguageModelNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AGENT_NODE_NAME,
true,
);
clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
clickGetBackToCanvas();
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();
openNode(AGENT_NODE_NAME);
getRunDataInfoCallout().should('not.exist');
const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?';
clickExecuteNode();
runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage),
runData: [
createMockNodeExecutionData(AGENT_NODE_NAME, {
jsonData: {
main: { output: outputMessage },
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
}),
createMockNodeExecutionData(AI_TOOL_CALCULATOR_NODE_NAME, {}),
],
lastNodeExecuted: AGENT_NODE_NAME,
});
closeManualChatModal();
openNode(AGENT_NODE_NAME);
// This waits to ensure the output panel is rendered
getOutputPanelTable();
getRunDataInfoCallout().should('not.exist');
});
});

View file

@ -1,21 +1,29 @@
import workflow from '../fixtures/Manual_wait_set.json';
import { getOutputTableRow } from '../composables/ndv';
import { getCanvasNodes, openNode } from '../composables/workflow';
import SIMPLE_WORKFLOW from '../fixtures/Manual_wait_set.json';
import WORKFLOW_WITH_PINNED from '../fixtures/Webhook_set_pinned.json';
import { importWorkflow, visitDemoPage } from '../pages/demo';
import { errorToast } from '../pages/notifications';
import { WorkflowPage } from '../pages/workflow';
const workflowPage = new WorkflowPage();
describe('Demo', () => {
beforeEach(() => {
cy.overrideSettings({ previewMode: true });
cy.signout();
});
it('can import template', () => {
visitDemoPage();
errorToast().should('not.exist');
importWorkflow(workflow);
workflowPage.getters.canvasNodes().should('have.length', 3);
importWorkflow(SIMPLE_WORKFLOW);
getCanvasNodes().should('have.length', 3);
});
it('can import workflow with pin data', () => {
visitDemoPage();
importWorkflow(WORKFLOW_WITH_PINNED);
getCanvasNodes().should('have.length', 2);
openNode('Webhook');
getOutputTableRow(0).should('include.text', 'headers');
getOutputTableRow(1).should('include.text', 'dragons');
});
it('can override theme to dark', () => {

View file

@ -56,10 +56,10 @@ describe('Template credentials setup', () => {
it('can be opened from template collection page', () => {
visitTemplateCollectionPage(testData.ecommerceStarterPack);
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag();
clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');
clickUseWorkflowButtonByTitle('Promote new Shopify products');
templateCredentialsSetupPage.getters
.title("Set up 'Promote new Shopify products on Twitter and Telegram' template")
.title("Set up 'Promote new Shopify products' template")
.should('be.visible');
});
@ -67,7 +67,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' template")
.should('be.visible');
templateCredentialsSetupPage.getters

View file

@ -15,6 +15,7 @@ import {
NDV,
MainSidebar,
} from '../pages';
import { clearNotifications, successToast } from '../pages/notifications';
import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';
const workflowsPage = new WorkflowsPage();
@ -50,7 +51,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
});
projects.getHomeButton().click();
projects.getProjectTabs().should('have.length', 2);
projects.getProjectTabs().should('have.length', 3);
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('not.have.length');
@ -100,7 +101,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects.getMenuItems().first().click();
workflowsPage.getters.workflowCards().should('not.have.length');
projects.getProjectTabs().should('have.length', 3);
projects.getProjectTabs().should('have.length', 4);
workflowsPage.getters.newWorkflowButtonCard().click();
@ -175,7 +176,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
let menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Home")[class*=active_]').should('exist');
menuItems.filter(':contains("Overview")[class*=active_]').should('exist');
projects.getMenuItems().first().click();
@ -185,7 +186,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
menuItems.filter(':contains("Development")[class*=active_]').should('exist');
cy.intercept('GET', '/rest/workflows/*').as('loadWorkflow');
workflowsPage.getters.workflowCards().first().click();
workflowsPage.getters.workflowCards().first().findChildByTestId('card-content').click();
cy.wait('@loadWorkflow');
menuItems = cy.getByTestId('menu-item');
@ -221,7 +222,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Home")[class*=active_]').should('exist');
menuItems.filter(':contains("Overview")[class*=active_]').should('exist');
workflowsPage.getters.workflowCards().should('have.length', 2).first().click();
@ -229,7 +230,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
cy.getByTestId('execute-workflow-button').should('be.visible');
menuItems = cy.getByTestId('menu-item');
menuItems.filter(':contains("Home")[class*=active_]').should('not.exist');
menuItems.filter(':contains("Overview")[class*=active_]').should('not.exist');
menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
@ -448,38 +449,48 @@ describe('Projects', { disableAutoLogin: true }, () => {
workflowsPage.getters.workflowCards().should('not.have.length');
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project');
clearNotifications();
projects.getHomeButton().click();
projects.getProjectTabCredentials().should('be.visible').click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Home project');
clearNotifications();
// Create a project and add a credential and a workflow to it
projects.createProject('Project 1');
clearNotifications();
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Project 1');
clearNotifications();
projects.getProjectTabWorkflows().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1');
clearNotifications();
// Create another project and add a credential and a workflow to it
projects.createProject('Project 2');
clearNotifications();
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Project 2');
clearNotifications();
projects.getProjectTabWorkflows().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2');
clearNotifications();
// Move the workflow owned by me from Home to Project 1
// Move the workflow Personal from Home to Project 1
projects.getHomeButton().click();
workflowsPage.getters
.workflowCards()
.should('have.length', 3)
.filter(':contains("Owned by me")')
.filter(':contains("Personal")')
.should('exist');
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
workflowsPage.getters.workflowMoveButton().click();
@ -487,7 +498,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move workflow")')
.contains('button', 'Move workflow')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
@ -495,12 +506,13 @@ describe('Projects', { disableAutoLogin: true }, () => {
.should('have.length', 5)
.filter(':contains("Project 1")')
.click();
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
projects.getResourceMoveModal().contains('button', 'Move workflow').click();
clearNotifications();
workflowsPage.getters
.workflowCards()
.should('have.length', 3)
.filter(':contains("Owned by me")')
.filter(':contains("Personal")')
.should('not.exist');
// Move the workflow from Project 1 to Project 2
@ -512,7 +524,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move workflow")')
.contains('button', 'Move workflow')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
@ -520,18 +532,19 @@ describe('Projects', { disableAutoLogin: true }, () => {
.should('have.length', 5)
.filter(':contains("Project 2")')
.click();
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
projects.getResourceMoveModal().contains('button', 'Move workflow').click();
// Move the workflow from Project 2 to a member user
projects.getMenuItems().last().click();
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
workflowsPage.getters.workflowMoveButton().click();
clearNotifications();
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move workflow")')
.contains('button', 'Move workflow')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
@ -540,7 +553,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
.filter(`:contains("${INSTANCE_MEMBERS[0].email}")`)
.click();
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
projects.getResourceMoveModal().contains('button', 'Move workflow').click();
workflowsPage.getters.workflowCards().should('have.length', 1);
// Move the workflow from member user back to Home
@ -556,7 +569,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move workflow")')
.contains('button', 'Move workflow')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
@ -565,11 +578,12 @@ describe('Projects', { disableAutoLogin: true }, () => {
.filter(`:contains("${INSTANCE_OWNER.email}")`)
.click();
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
projects.getResourceMoveModal().contains('button', 'Move workflow').click();
clearNotifications();
workflowsPage.getters
.workflowCards()
.should('have.length', 3)
.filter(':contains("Owned by me")')
.filter(':contains("Personal")')
.should('have.length', 1);
// Move the credential from Project 1 to Project 2
@ -582,7 +596,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move credential")')
.contains('button', 'Move credential')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
@ -590,8 +604,8 @@ describe('Projects', { disableAutoLogin: true }, () => {
.should('have.length', 5)
.filter(':contains("Project 2")')
.click();
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
projects.getResourceMoveModal().contains('button', 'Move credential').click();
clearNotifications();
credentialsPage.getters.credentialCards().should('not.have.length');
// Move the credential from Project 2 to admin user
@ -605,7 +619,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move credential")')
.contains('button', 'Move credential')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
@ -613,7 +627,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
.should('have.length', 5)
.filter(`:contains("${INSTANCE_ADMIN.email}")`)
.click();
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
projects.getResourceMoveModal().contains('button', 'Move credential').click();
credentialsPage.getters.credentialCards().should('have.length', 1);
// Move the credential from admin user back to instance owner
@ -627,7 +641,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move credential")')
.contains('button', 'Move credential')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
@ -635,12 +649,14 @@ describe('Projects', { disableAutoLogin: true }, () => {
.should('have.length', 5)
.filter(`:contains("${INSTANCE_OWNER.email}")`)
.click();
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
projects.getResourceMoveModal().contains('button', 'Move credential').click();
clearNotifications();
credentialsPage.getters
.credentialCards()
.should('have.length', 3)
.filter(':contains("Owned by me")')
.filter(':contains("Personal")')
.should('have.length', 2);
// Move the credential from admin user back to its original project (Project 1)
@ -650,7 +666,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move credential")')
.contains('button', 'Move credential')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
@ -658,7 +674,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
.should('have.length', 5)
.filter(':contains("Project 1")')
.click();
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
projects.getResourceMoveModal().contains('button', 'Move credential').click();
projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
@ -681,9 +697,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects.getHomeButton().click();
workflowsPage.getters.workflowCards().should('not.have.length');
workflowsPage.getters.newWorkflowButtonCard().click();
workflowsPage.getters.workflowCards().should('not.have.length');
workflowsPage.getters.newWorkflowButtonCard().click();
workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
ndv.getters.backToCanvas().click();
@ -699,7 +713,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
workflowsPage.getters
.workflowCards()
.should('have.length', 1)
.filter(':contains("Owned by me")')
.filter(':contains("Personal")')
.should('exist');
workflowsPage.getters.workflowCardActions('My workflow').click();
workflowsPage.getters.workflowMoveButton().click();
@ -707,7 +721,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move workflow")')
.contains('button', 'Move workflow')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
@ -715,12 +729,12 @@ describe('Projects', { disableAutoLogin: true }, () => {
.should('have.length', 4)
.filter(':contains("Project 1")')
.click();
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
projects.getResourceMoveModal().contains('button', 'Move workflow').click();
workflowsPage.getters
.workflowCards()
.should('have.length', 1)
.filter(':contains("Owned by me")')
.filter(':contains("Personal")')
.should('not.exist');
//Log out with instance owner and log in with the member user
@ -733,7 +747,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
// Open the moved workflow
workflowsPage.getters.workflowCards().should('have.length', 1);
workflowsPage.getters.workflowCards().first().click();
workflowsPage.getters.workflowCards().first().findChildByTestId('card-content').click();
// Check if the credential can be changed
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
@ -769,7 +783,8 @@ describe('Projects', { disableAutoLogin: true }, () => {
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
cy.getByTestId('form-submit-button').click();
mainSidebar.getters.executions().click();
projects.getMenuItems().last().click();
projects.getProjectTabExecutions().click();
cy.getByTestId('global-execution-list-item').first().find('td:last button').click();
getVisibleDropdown()
.find('li')
@ -815,4 +830,23 @@ describe('Projects', { disableAutoLogin: true }, () => {
.should('not.have.length');
});
});
it('should set and update project icon', () => {
const DEFAULT_ICON = 'fa-layer-group';
const NEW_PROJECT_NAME = 'Test Project';
cy.signinAsAdmin();
cy.visit(workflowsPage.url);
projects.createProject(NEW_PROJECT_NAME);
// New project should have default icon
projects.getIconPickerButton().find('svg').should('have.class', DEFAULT_ICON);
// Choose another icon
projects.getIconPickerButton().click();
projects.getIconPickerTab('Emojis').click();
projects.getIconPickerEmojis().first().click();
// Project should be updated with new icon
successToast().contains('Project icon updated successfully');
projects.getIconPickerButton().should('contain', '😀');
projects.getMenuItems().contains(NEW_PROJECT_NAME).should('contain', '😀');
});
});

View file

@ -23,6 +23,7 @@ describe('Manual partial execution', () => {
canvas.actions.openNode('Webhook1');
ndv.getters.nodeRunSuccessIndicator().should('exist');
ndv.getters.nodeRunTooltipIndicator().should('exist');
ndv.getters.outputRunSelector().should('not.exist'); // single run
});
});

View file

@ -10,7 +10,7 @@ import { WorkflowPage } from '../pages/workflow';
const workflowPage = new WorkflowPage();
const NOW = 1717771477012;
const NOW = Date.now();
const ONE_DAY = 24 * 60 * 60 * 1000;
const THREE_DAYS = ONE_DAY * 3;
const SEVEN_DAYS = ONE_DAY * 7;

View file

@ -1,3 +1,4 @@
import { getCredentialSaveButton } from '../composables/modals/credential-modal';
import { CredentialsPage, CredentialsModal } from '../pages';
const credentialsPage = new CredentialsPage();
@ -40,7 +41,7 @@ describe('Credentials', () => {
});
// Check that the credential was saved and connected successfully
credentialsModal.getters.saveButton().should('contain.text', 'Saved');
getCredentialSaveButton().should('contain.text', 'Saved');
credentialsModal.getters.oauthConnectSuccessBanner().should('be.visible');
});
});

View file

@ -4,6 +4,7 @@ import { clickCreateNewCredential, openCredentialSelect } from '../composables/n
import { GMAIL_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
import { AIAssistant } from '../pages/features/ai-assistant';
import { NodeCreator } from '../pages/features/node-creator';
import { getVisibleSelect } from '../utils';
const wf = new WorkflowPage();
@ -11,6 +12,7 @@ const ndv = new NDV();
const aiAssistant = new AIAssistant();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
const nodeCreatorFeature = new NodeCreator();
describe('AI Assistant::disabled', () => {
beforeEach(() => {
@ -222,6 +224,54 @@ describe('AI Assistant::enabled', () => {
.should('contain.text', 'item.json.myNewField = 1');
});
it('Should ignore node execution success and error messages after the node run successfully once', () => {
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
const getEditor = () => getParameter().find('.cm-content').should('exist');
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/code_diff_suggestion_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Code');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
cy.wait('@chatRequest');
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/node_execution_succeeded_response.json',
}).as('chatRequest2');
getEditor()
.type('{selectall}')
.paste(
'for (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();',
);
ndv.getters.nodeExecuteButton().click();
getEditor()
.type('{selectall}')
.paste(
'for (const item of $input.all()) {\n item.json.myNewField = 1aaaa!;\n}\n\nreturn $input.all();',
);
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.chatMessagesAssistant().should('have.length', 3);
aiAssistant.getters
.chatMessagesAssistant()
.eq(2)
.should(
'contain.text',
'Code node ran successfully, did my solution help resolve your issue?\nQuick reply 👇Yes, thanksNo, I am still stuck',
);
});
it('should end chat session when `end_session` event is received', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
@ -280,6 +330,20 @@ describe('AI Assistant::enabled', () => {
wf.getters.isWorkflowSaved();
aiAssistant.getters.placeholderMessage().should('not.exist');
});
it('should send message via enter even with global NodeCreator panel opened', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
aiAssistant.actions.openChat();
nodeCreatorFeature.actions.openNodeCreator();
aiAssistant.getters.chatInput().type('Hello{Enter}');
aiAssistant.getters.placeholderMessage().should('not.exist');
});
});
describe('AI Assistant Credential Help', () => {
@ -493,6 +557,8 @@ describe('General help', () => {
}).as('chatRequest');
aiAssistant.getters.askAssistantFloatingButton().click();
wf.getters.zoomToFitButton().click();
aiAssistant.actions.sendMessage('What is wrong with this workflow?');
cy.wait('@chatRequest');

View file

@ -27,7 +27,7 @@ describe('Workflow Selector Parameter', () => {
getVisiblePopper()
.should('have.length', 1)
.findChildByTestId('rlc-item')
.should('have.length', 2);
.should('have.length', 3);
});
it('should show required parameter warning', () => {
@ -44,7 +44,8 @@ describe('Workflow Selector Parameter', () => {
getVisiblePopper()
.should('have.length', 1)
.findChildByTestId('rlc-item')
.should('have.length', 1)
.should('have.length', 2)
.eq(1)
.click();
ndv.getters
@ -57,7 +58,7 @@ describe('Workflow Selector Parameter', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
getVisiblePopper().findChildByTestId('rlc-item').first().click();
getVisiblePopper().findChildByTestId('rlc-item').eq(1).click();
ndv.getters.resourceLocatorInput('workflowId').find('a').should('exist');
cy.getByTestId('radio-button-expression').eq(1).click();
@ -68,7 +69,7 @@ describe('Workflow Selector Parameter', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
getVisiblePopper().findChildByTestId('rlc-item').first().click();
getVisiblePopper().findChildByTestId('rlc-item').eq(1).click();
ndv.getters
.resourceLocatorModeSelector('workflowId')
.find('input')
@ -79,4 +80,28 @@ describe('Workflow Selector Parameter', () => {
.find('input')
.should('have.value', 'By ID');
});
it('should render add resource option and redirect to the correct route when clicked', () => {
cy.window().then((win) => {
cy.stub(win, 'open').as('windowOpen');
});
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist');
getVisiblePopper()
.findChildByTestId('rlc-item')
.eq(0)
.find('span')
.should('have.text', 'Create a new sub-workflow');
getVisiblePopper().findChildByTestId('rlc-item').eq(0).click();
const SAMPLE_SUBWORKFLOW_TEMPLATE_ID = 'VMiAxXa3lCAizGB5f7dVZQSFfg3FtHkdTKvLuupqBls=';
cy.get('@windowOpen').should(
'be.calledWith',
`/workflows/onboarding/${SAMPLE_SUBWORKFLOW_TEMPLATE_ID}?sampleSubWorkflows=0`,
);
});
});

View file

@ -0,0 +1,140 @@
import {
getExecutionPreviewOutputPanelRelatedExecutionLink,
getExecutionsSidebar,
getWorkflowExecutionPreviewIframe,
openExecutionPreviewNode,
} from '../composables/executions';
import {
changeOutputRunSelector,
getOutputPanelItemsCount,
getOutputPanelRelatedExecutionLink,
getOutputRunSelectorInput,
getOutputTableHeaders,
getOutputTableRows,
getOutputTbodyCell,
} from '../composables/ndv';
import {
clickExecuteWorkflowButton,
clickZoomToFit,
getCanvasNodes,
navigateToNewWorkflowPage,
openNode,
pasteWorkflow,
saveWorkflowOnButtonClick,
} from '../composables/workflow';
import SUBWORKFLOW_DEBUGGING_EXAMPLE from '../fixtures/Subworkflow-debugging-execute-workflow.json';
describe('Subworkflow debugging', () => {
beforeEach(() => {
navigateToNewWorkflowPage();
pasteWorkflow(SUBWORKFLOW_DEBUGGING_EXAMPLE);
saveWorkflowOnButtonClick();
getCanvasNodes().should('have.length', 11);
clickZoomToFit();
clickExecuteWorkflowButton();
});
describe('can inspect sub executed workflow', () => {
it('(Run once with all items/ Wait for Sub-workflow completion) (default behavior)', () => {
openNode('Execute Workflow with param');
getOutputPanelItemsCount().should('contain.text', '2 items, 1 sub-execution');
getOutputPanelRelatedExecutionLink().should('contain.text', 'Inspect Sub-Execution');
getOutputPanelRelatedExecutionLink().should('have.attr', 'href');
// ensure workflow executed and waited on output
getOutputTableHeaders().should('have.length', 2);
getOutputTbodyCell(1, 0).should('have.text', 'world Natalie Moore');
});
it('(Run once for each item/ Wait for Sub-workflow completion)', () => {
openNode('Execute Workflow with param1');
getOutputPanelItemsCount().should('contain.text', '2 items, 2 sub-execution');
getOutputPanelRelatedExecutionLink().should('not.exist');
// ensure workflow executed and waited on output
getOutputTableHeaders().should('have.length', 3);
getOutputTbodyCell(1, 0).find('a').should('have.attr', 'href');
getOutputTbodyCell(1, 1).should('have.text', 'world Natalie Moore');
});
it('(Run once with all items/ Wait for Sub-workflow completion)', () => {
openNode('Execute Workflow with param2');
getOutputPanelItemsCount().should('not.exist');
getOutputPanelRelatedExecutionLink().should('contain.text', 'Inspect Sub-Execution');
getOutputPanelRelatedExecutionLink().should('have.attr', 'href');
// ensure workflow executed but returned same data as input
getOutputRunSelectorInput().should('have.value', '2 of 2 (3 items, 1 sub-execution)');
getOutputTableHeaders().should('have.length', 6);
getOutputTableHeaders().eq(0).should('have.text', 'uid');
getOutputTableRows().should('have.length', 4);
getOutputTbodyCell(1, 1).should('include.text', 'Jon_Ebert@yahoo.com');
changeOutputRunSelector('1 of 2 (2 items, 1 sub-execution)');
getOutputRunSelectorInput().should('have.value', '1 of 2 (2 items, 1 sub-execution)');
getOutputTableHeaders().should('have.length', 6);
getOutputTableHeaders().eq(0).should('have.text', 'uid');
getOutputTableRows().should('have.length', 3);
getOutputTbodyCell(1, 1).should('include.text', 'Terry.Dach@hotmail.com');
});
it('(Run once for each item/ Wait for Sub-workflow completion)', () => {
openNode('Execute Workflow with param3');
// ensure workflow executed but returned same data as input
getOutputRunSelectorInput().should('have.value', '2 of 2 (3 items, 3 sub-executions)');
getOutputTableHeaders().should('have.length', 7);
getOutputTableHeaders().eq(1).should('have.text', 'uid');
getOutputTableRows().should('have.length', 4);
getOutputTbodyCell(1, 0).find('a').should('have.attr', 'href');
getOutputTbodyCell(1, 2).should('include.text', 'Jon_Ebert@yahoo.com');
changeOutputRunSelector('1 of 2 (2 items, 2 sub-executions)');
getOutputRunSelectorInput().should('have.value', '1 of 2 (2 items, 2 sub-executions)');
getOutputTableHeaders().should('have.length', 7);
getOutputTableHeaders().eq(1).should('have.text', 'uid');
getOutputTableRows().should('have.length', 3);
getOutputTbodyCell(1, 0).find('a').should('have.attr', 'href');
getOutputTbodyCell(1, 2).should('include.text', 'Terry.Dach@hotmail.com');
});
});
it('can inspect parent executions', () => {
cy.url().then((workflowUrl) => {
openNode('Execute Workflow with param');
getOutputPanelItemsCount().should('contain.text', '2 items, 1 sub-execution');
getOutputPanelRelatedExecutionLink().should('contain.text', 'Inspect Sub-Execution');
getOutputPanelRelatedExecutionLink().should('have.attr', 'href');
// ensure workflow executed and waited on output
getOutputTableHeaders().should('have.length', 2);
getOutputTbodyCell(1, 0).should('have.text', 'world Natalie Moore');
// cypress cannot handle new tabs so removing it
getOutputPanelRelatedExecutionLink().invoke('removeAttr', 'target').click();
getExecutionsSidebar().should('be.visible');
getWorkflowExecutionPreviewIframe().should('be.visible');
openExecutionPreviewNode('Execute Workflow Trigger');
getExecutionPreviewOutputPanelRelatedExecutionLink().should(
'include.text',
'Inspect Parent Execution',
);
getExecutionPreviewOutputPanelRelatedExecutionLink()
.invoke('removeAttr', 'target')
.click({ force: true });
cy.url().then((currentUrl) => {
expect(currentUrl === workflowUrl);
});
});
});
});

View file

@ -0,0 +1,288 @@
import { clickGetBackToCanvas, getOutputTableHeaders } from '../composables/ndv';
import {
clickZoomToFit,
navigateToNewWorkflowPage,
openNode,
pasteWorkflow,
saveWorkflowOnButtonClick,
} from '../composables/workflow';
import SUB_WORKFLOW_INPUTS from '../fixtures/Test_Subworkflow-Inputs.json';
import { NDV, WorkflowsPage, WorkflowPage } from '../pages';
import { errorToast, successToast } from '../pages/notifications';
import { getVisiblePopper } from '../utils';
const ndv = new NDV();
const workflowsPage = new WorkflowsPage();
const workflow = new WorkflowPage();
const DEFAULT_WORKFLOW_NAME = 'My workflow';
const DEFAULT_SUBWORKFLOW_NAME_1 = 'My Sub-Workflow 1';
const DEFAULT_SUBWORKFLOW_NAME_2 = 'My Sub-Workflow 2';
type FieldRow = readonly string[];
const exampleFields = [
['aNumber', 'Number'],
['aString', 'String'],
['aArray', 'Array'],
['aObject', 'Object'],
['aAny', 'Allow Any Type'],
// bool last since it's not an inputField so we'll skip it for some cases
['aBool', 'Boolean'],
] as const;
/**
* Populate multiValue fixedCollections. Only supports fixedCollections for which all fields can be defined via keyboard typing
*
* @param items - 2D array of items to populate, i.e. [["myField1", "String"], [""]
* @param collectionName - name of the fixedCollection to populate
* @param offset - amount of 'parameter-input's before the fixedCollection under test
* @returns
*/
function populateFixedCollection(
items: readonly FieldRow[],
collectionName: string,
offset: number,
) {
if (items.length === 0) return;
const n = items[0].length;
for (const [i, params] of items.entries()) {
ndv.actions.addItemToFixedCollection(collectionName);
for (const [j, param] of params.entries()) {
ndv.getters
.fixedCollectionParameter(collectionName)
.getByTestId('parameter-input')
.eq(offset + i * n + j)
.type(`${param}{downArrow}{enter}`);
}
}
}
function makeExample(type: TypeField) {
switch (type) {
case 'String':
return '"example"';
case 'Number':
return '42';
case 'Boolean':
return 'true';
case 'Array':
return '["example", 123, null]';
case 'Object':
return '{{}"example": [123]}';
case 'Allow Any Type':
return 'null';
}
}
type TypeField = 'Allow Any Type' | 'String' | 'Number' | 'Boolean' | 'Array' | 'Object';
function populateFields(items: ReadonlyArray<readonly [string, TypeField]>) {
populateFixedCollection(items, 'workflowInputs', 1);
}
function navigateWorkflowSelectionDropdown(index: number, expectedText: string) {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist');
getVisiblePopper()
.findChildByTestId('rlc-item')
.eq(index)
.find('span')
.should('have.text', expectedText)
.click();
}
function populateMapperFields(values: readonly string[], offset: number) {
for (const [i, value] of values.entries()) {
cy.getByTestId('parameter-input')
.eq(offset + i)
.type(value);
// Click on a parent to dismiss the pop up hiding the field below.
cy.getByTestId('parameter-input')
.eq(offset + i)
.parent()
.parent()
.click('topLeft');
}
}
// This function starts off in the Child Workflow Input Trigger, assuming we just defined the input fields
// It then navigates back to the parent and validates output
function validateAndReturnToParent(targetChild: string, offset: number, fields: string[]) {
ndv.actions.execute();
// + 1 to account for formatting-only column
getOutputTableHeaders().should('have.length', fields.length + 1);
for (const [i, name] of fields.entries()) {
getOutputTableHeaders().eq(i).should('have.text', name);
}
clickGetBackToCanvas();
saveWorkflowOnButtonClick();
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCardContent(DEFAULT_WORKFLOW_NAME).click();
openNode('Execute Workflow');
// Note that outside of e2e tests this will be pre-selected correctly.
// Due to our workaround to remain in the same tab we need to select the correct tab manually
navigateWorkflowSelectionDropdown(offset, targetChild);
// This fails, pointing to `usePushConnection` `const triggerNode = subWorkflow?.nodes.find` being `undefined.find()`I <think>
ndv.actions.execute();
getOutputTableHeaders().should('have.length', fields.length + 1);
for (const [i, name] of fields.entries()) {
getOutputTableHeaders().eq(i).should('have.text', name);
}
// todo: verify the fields appear and show the correct types
// todo: fill in the input fields (and mock previous node data in the json fixture to match)
// todo: validate the actual output data
}
function setWorkflowInputFieldValue(index: number, value: string) {
ndv.actions.addItemToFixedCollection('workflowInputs');
ndv.actions.typeIntoFixedCollectionItem('workflowInputs', index, value);
}
describe('Sub-workflow creation and typed usage', () => {
beforeEach(() => {
navigateToNewWorkflowPage();
pasteWorkflow(SUB_WORKFLOW_INPUTS);
saveWorkflowOnButtonClick();
clickZoomToFit();
openNode('Execute Workflow');
// Prevent sub-workflow from opening in new window
cy.window().then((win) => {
cy.stub(win, 'open').callsFake((url) => {
cy.visit(url);
});
});
navigateWorkflowSelectionDropdown(0, 'Create a new sub-workflow');
// **************************
// NAVIGATE TO CHILD WORKFLOW
// **************************
openNode('Workflow Input Trigger');
});
it('works with type-checked values', () => {
populateFields(exampleFields);
validateAndReturnToParent(
DEFAULT_SUBWORKFLOW_NAME_1,
1,
exampleFields.map((f) => f[0]),
);
const values = [
'-1', // number fields don't support `=` switch to expression, so let's test the Fixed case with it
...exampleFields.slice(1).map((x) => `={{}{{} $json.a${x[0]}`), // }} are added automatically
];
// this matches with the pinned data provided in the fixture
populateMapperFields(values, 2);
ndv.actions.execute();
// todo:
// - validate output lines up
// - change input to need casts
// - run
// - confirm error
// - switch `attemptToConvertTypes` flag
// - confirm success and changed output
// - change input to be invalid despite cast
// - run
// - confirm error
// - switch type option flags
// - run
// - confirm success
// - turn off attempt to cast flag
// - confirm a value was not cast
});
it('works with Fields input source into JSON input source', () => {
ndv.getters.nodeOutputHint().should('exist');
populateFields(exampleFields);
validateAndReturnToParent(
DEFAULT_SUBWORKFLOW_NAME_1,
1,
exampleFields.map((f) => f[0]),
);
cy.window().then((win) => {
cy.stub(win, 'open').callsFake((url) => {
cy.visit(url);
});
});
navigateWorkflowSelectionDropdown(0, 'Create a new sub-workflow');
openNode('Workflow Input Trigger');
cy.getByTestId('parameter-input').eq(0).click();
// Todo: Check if there's a better way to interact with option dropdowns
// This PR would add this child testId
getVisiblePopper()
.getByTestId('parameter-input')
.eq(0)
.type('Using JSON Example{downArrow}{enter}');
const exampleJson =
'{{}' + exampleFields.map((x) => `"${x[0]}": ${makeExample(x[1])}`).join(',') + '}';
cy.getByTestId('parameter-input-jsonExample')
.find('.cm-line')
.eq(0)
.type(`{selectAll}{backspace}${exampleJson}{enter}`);
// first one doesn't work for some reason, might need to wait for something?
ndv.actions.execute();
validateAndReturnToParent(
DEFAULT_SUBWORKFLOW_NAME_2,
2,
exampleFields.map((f) => f[0]),
);
// test for either InputSource mode and options combinations:
// + we're showing the notice in the output panel
// + we start with no fields
// + Test Step works and we create the fields
// + create field of each type (string, number, boolean, object, array, any)
// + exit ndv
// + save
// + go back to parent workflow
// - verify fields appear [needs Ivan's PR]
// - link fields [needs Ivan's PR]
// + run parent
// - verify output with `null` defaults exists
//
});
it('should show node issue when no fields are defined in manual mode', () => {
ndv.getters.nodeExecuteButton().should('be.disabled');
ndv.actions.close();
// Executing the workflow should show an error toast
workflow.actions.executeWorkflow();
errorToast().should('contain', 'The workflow has issues');
openNode('Workflow Input Trigger');
// Add a field to the workflowInputs fixedCollection
setWorkflowInputFieldValue(0, 'test');
// Executing the workflow should not show error now
ndv.actions.close();
workflow.actions.executeWorkflow();
successToast().should('contain', 'Workflow executed successfully');
});
});

View file

@ -65,26 +65,6 @@ describe('NDV', () => {
cy.shouldNotHaveConsoleErrors();
});
it('should disconect Switch outputs if rules order was changed', () => {
cy.createFixtureWorkflow('NDV-test-switch_reorder.json', 'NDV test switch reorder');
workflowPage.actions.zoomToFit();
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Merge');
ndv.getters.outputPanel().contains('2 items').should('exist');
cy.contains('span', 'first').should('exist');
ndv.getters.backToCanvas().click();
workflowPage.actions.openNode('Switch');
cy.get('.cm-line').realMouseMove(100, 100);
cy.get('.fa-angle-down').click();
ndv.getters.backToCanvas().click();
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Merge');
ndv.getters.outputPanel().contains('1 item').should('exist');
cy.contains('span', 'zero').should('exist');
});
it('should show correct validation state for resource locator params', () => {
workflowPage.actions.addNodeToCanvas('Typeform', true, true);
ndv.getters.container().should('be.visible');
@ -111,6 +91,7 @@ describe('NDV', () => {
cy.get('[class*=hasIssues]').should('have.length', 1);
});
// Correctly failing in V2 - node issues are only shows after execution
it('should show all validation errors when opening pasted node', () => {
cy.createFixtureWorkflow('Test_workflow_ndv_errors.json', 'Validation errors');
workflowPage.getters.canvasNodes().should('have.have.length', 1);
@ -133,9 +114,10 @@ describe('NDV', () => {
"An expression here won't work because it uses .item and n8n can't figure out the matching item.",
);
ndv.getters.nodeRunErrorIndicator().should('be.visible');
ndv.getters.nodeRunTooltipIndicator().should('be.visible');
// The error details should be hidden behind a tooltip
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Start Time');
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Execution Time');
ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Start Time');
ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Execution Time');
});
it('should save workflow using keyboard shortcut from NDV', () => {
@ -203,7 +185,7 @@ describe('NDV', () => {
.contains(key)
.should('be.visible');
});
getObjectValueItem().find('label').click({ force: true });
getObjectValueItem().find('.toggle').click({ force: true });
expandedObjectProps.forEach((key) => {
ndv.getters
.outputPanel()
@ -212,9 +194,11 @@ describe('NDV', () => {
.should('not.be.visible');
});
});
it('should not display pagination for schema', () => {
setupSchemaWorkflow();
ndv.getters.backToCanvas().click();
workflowPage.actions.deselectAll();
workflowPage.getters.canvasNodeByName('Set').click();
workflowPage.actions.addNodeToCanvas(
'Customer Datastore (n8n training)',
@ -244,8 +228,8 @@ describe('NDV', () => {
ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist');
ndv.getters
.outputPanel()
.find('[data-test-id=run-data-schema-item] [data-test-id=run-data-schema-item]')
.should('have.length', 20);
.find('[data-test-id=run-data-schema-item]')
.should('have.length.above', 10);
});
});
@ -406,8 +390,18 @@ describe('NDV', () => {
return cy.get(`[data-node-placement=${position}]`);
}
// Correctly failing in V2 - due to floating navigation not updating the selected node
it('should traverse floating nodes with mouse', () => {
cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes');
cy.ifCanvasVersion(
() => {},
() => {
// Needed in V2 as all nodes remain selected when clicking on a selected node
workflowPage.actions.deselectAll();
},
);
workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist');
@ -418,6 +412,7 @@ describe('NDV', () => {
getFloatingNodeByPosition('inputMain').should('exist');
getFloatingNodeByPosition('outputMain').should('exist');
ndv.actions.close();
// These two lines are broken in V2
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters
.selectedNodes()
@ -425,10 +420,8 @@ describe('NDV', () => {
.should('contain', `Node ${i + 1}`);
workflowPage.getters.selectedNodes().first().dblclick();
});
getFloatingNodeByPosition('outputMain').click({ force: true });
ndv.getters.nodeNameContainer().should('contain', 'Chain');
// Traverse 4 connected node backwards
Array.from(Array(4).keys()).forEach((i) => {
getFloatingNodeByPosition('inputMain').click({ force: true });
@ -452,8 +445,17 @@ describe('NDV', () => {
.should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
});
// Correctly failing in V2 - due to floating navigation not updating the selected node
it('should traverse floating nodes with keyboard', () => {
cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes');
cy.ifCanvasVersion(
() => {},
() => {
// Needed in V2 as all nodes remain selected when clicking on a selected node
workflowPage.actions.deselectAll();
},
);
workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist');
@ -464,6 +466,7 @@ describe('NDV', () => {
getFloatingNodeByPosition('inputMain').should('exist');
getFloatingNodeByPosition('outputMain').should('exist');
ndv.actions.close();
// These two lines are broken in V2
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters
.selectedNodes()
@ -491,6 +494,7 @@ describe('NDV', () => {
getFloatingNodeByPosition('inputSub').should('not.exist');
getFloatingNodeByPosition('outputSub').should('not.exist');
ndv.actions.close();
// These two lines are broken in V2
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters
.selectedNodes()
@ -617,8 +621,10 @@ describe('NDV', () => {
// Should not show run info before execution
ndv.getters.nodeRunSuccessIndicator().should('not.exist');
ndv.getters.nodeRunErrorIndicator().should('not.exist');
ndv.getters.nodeRunTooltipIndicator().should('not.exist');
ndv.getters.nodeExecuteButton().click();
ndv.getters.nodeRunSuccessIndicator().should('exist');
ndv.getters.nodeRunTooltipIndicator().should('exist');
});
it('should properly show node execution indicator for multiple nodes', () => {
@ -630,6 +636,7 @@ describe('NDV', () => {
// Manual tigger node should show success indicator
workflowPage.actions.openNode('When clicking Test workflow');
ndv.getters.nodeRunSuccessIndicator().should('exist');
ndv.getters.nodeRunTooltipIndicator().should('exist');
// Code node should show error
ndv.getters.backToCanvas().click();
workflowPage.actions.openNode('Code');
@ -713,6 +720,7 @@ describe('NDV', () => {
.should('have.value', 'Error fetching options from Notion');
});
// Correctly failing in V2 - NodeCreator is not opened after clicking on the link
it('Should open appropriate node creator after clicking on connection hint link', () => {
const nodeCreator = new NodeCreator();
const hintMapper = {
@ -730,6 +738,7 @@ describe('NDV', () => {
Object.entries(hintMapper).forEach(([node, group]) => {
workflowPage.actions.openNode(node);
// This fails to open the NodeCreator
cy.get('[data-action=openSelectiveNodeCreator]').contains('Insert one').click();
nodeCreator.getters.activeSubcategory().should('contain', group);
cy.realPress('Escape');
@ -791,4 +800,60 @@ describe('NDV', () => {
.find('[data-test-id=run-data-schema-item]')
.should('contain.text', 'onlyOnItem3');
});
it('should keep search expanded after Test step node run', () => {
cy.createFixtureWorkflow('Test_ndv_search.json');
workflowPage.actions.zoomToFit();
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Edit Fields');
ndv.getters.outputPanel().should('be.visible');
ndv.getters.outputPanel().findChildByTestId('ndv-search').click().type('US');
ndv.getters.outputTableRow(1).find('mark').should('have.text', 'US');
ndv.actions.execute();
ndv.getters
.outputPanel()
.findChildByTestId('ndv-search')
.should('be.visible')
.should('have.value', 'US');
});
it('should not show items count when seaching in schema view', () => {
cy.createFixtureWorkflow('Test_ndv_search.json');
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Edit Fields');
ndv.getters.outputPanel().should('be.visible');
ndv.actions.execute();
ndv.actions.switchOutputMode('Schema');
ndv.getters.outputPanel().find('[data-test-id=ndv-search]').click().type('US');
ndv.getters.outputPanel().find('[data-test-id=ndv-items-count]').should('not.exist');
});
it('should show additional tooltip when seaching in schema view if no matches', () => {
cy.createFixtureWorkflow('Test_ndv_search.json');
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Edit Fields');
ndv.getters.outputPanel().should('be.visible');
ndv.actions.execute();
ndv.actions.switchOutputMode('Schema');
ndv.getters.outputPanel().find('[data-test-id=ndv-search]').click().type('foo');
ndv.getters
.outputPanel()
.contains('To search field contents rather than just names, use Table or JSON view')
.should('exist');
});
it('ADO-2931 - should handle multiple branches of the same input with the first branch empty correctly', () => {
cy.createFixtureWorkflow('Test_ndv_two_branches_of_same_parent_false_populated.json');
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('DebugHelper');
ndv.getters.inputPanel().should('be.visible');
ndv.getters.outputPanel().should('be.visible');
ndv.actions.execute();
// This ensures we rendered the inputPanel
ndv.getters
.inputPanel()
.find('[data-test-id=run-data-schema-item]')
.should('contain.text', 'a1');
});
});

View file

@ -162,21 +162,21 @@ return []
cy.get('#tab-code').should('have.class', 'is-active');
});
it('should show error based on status code', () => {
const prompt = nanoid(20);
cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious();
const handledCodes = [
{ code: 400, message: 'Code generation failed due to an unknown reason' },
{ code: 413, message: 'Your workflow data is too large for AI to process' },
{ code: 429, message: "We've hit our rate limit with our AI partner" },
{ code: 500, message: 'Code generation failed due to an unknown reason' },
];
cy.getByTestId('ask-ai-prompt-input').type(prompt);
handledCodes.forEach(({ code, message }) => {
it(`should show error based on status code ${code}`, () => {
const prompt = nanoid(20);
cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious();
const handledCodes = [
{ code: 400, message: 'Code generation failed due to an unknown reason' },
{ code: 413, message: 'Your workflow data is too large for AI to process' },
{ code: 429, message: "We've hit our rate limit with our AI partner" },
{ code: 500, message: 'Code generation failed due to an unknown reason' },
];
cy.getByTestId('ask-ai-prompt-input').type(prompt);
handledCodes.forEach(({ code, message }) => {
cy.intercept('POST', '/rest/ai/ask-ai', {
statusCode: code,
status: code,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,354 @@
{
"meta": {
"instanceId": "08ce71ad998aeaade0abedb8dd96153d8eaa03fcb84cfccc1530095bf9ee478e"
},
"nodes": [
{
"parameters": {},
"id": "4535ce3e-280e-49b0-8854-373472ec86d1",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [80, 860]
},
{
"parameters": {
"category": "randomData",
"randomDataSeed": "0",
"randomDataCount": 2
},
"id": "d7fba18a-d51f-4509-af45-68cd9425ac6b",
"name": "DebugHelper1",
"type": "n8n-nodes-base.debugHelper",
"typeVersion": 1,
"position": [280, 860]
},
{
"parameters": {
"source": "parameter",
"workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}",
"mode": "each",
"options": {
"waitForSubWorkflow": false
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [680, 1540],
"id": "f90a25da-dd89-4bf8-8f5b-bf8ee1de0b70",
"name": "Execute Workflow with param3"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c93f26bd-3489-467b-909e-6462e1463707",
"name": "uid",
"value": "={{ $json.uid }}",
"type": "string"
},
{
"id": "3dd706ce-d925-4219-8531-ad12369972fe",
"name": "email",
"value": "={{ $json.email }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [900, 1540],
"id": "3be57648-3be8-4b0f-abfa-8fdcafee804d",
"name": "Edit Fields8"
},
{
"parameters": {
"source": "parameter",
"workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}",
"options": {
"waitForSubWorkflow": false
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [620, 1220],
"id": "dabc2356-3660-4d17-b305-936a002029ba",
"name": "Execute Workflow with param2"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c93f26bd-3489-467b-909e-6462e1463707",
"name": "uid",
"value": "={{ $json.uid }}",
"type": "string"
},
{
"id": "3dd706ce-d925-4219-8531-ad12369972fe",
"name": "email",
"value": "={{ $json.email }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [840, 1220],
"id": "9d2a9dda-e2a1-43e8-a66f-a8a555692e5f",
"name": "Edit Fields7"
},
{
"parameters": {
"source": "parameter",
"workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}",
"mode": "each",
"options": {
"waitForSubWorkflow": true
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [560, 900],
"id": "07e47f60-622a-484c-ab24-35f6f2280595",
"name": "Execute Workflow with param1"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c93f26bd-3489-467b-909e-6462e1463707",
"name": "uid",
"value": "={{ $json.uid }}",
"type": "string"
},
{
"id": "3dd706ce-d925-4219-8531-ad12369972fe",
"name": "email",
"value": "={{ $json.email }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [760, 900],
"id": "80563d0a-0bab-444f-a04c-4041a505d78b",
"name": "Edit Fields6"
},
{
"parameters": {
"source": "parameter",
"workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}",
"options": {
"waitForSubWorkflow": true
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [560, 580],
"id": "f04af481-f4d9-4d91-a60a-a377580e8393",
"name": "Execute Workflow with param"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c93f26bd-3489-467b-909e-6462e1463707",
"name": "uid",
"value": "={{ $json.uid }}",
"type": "string"
},
{
"id": "3dd706ce-d925-4219-8531-ad12369972fe",
"name": "email",
"value": "={{ $json.email }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [760, 580],
"id": "80c10607-a0ac-4090-86a1-890da0a2aa52",
"name": "Edit Fields2"
},
{
"parameters": {
"content": "## Execute Workflow (Run once with all items/ DONT Wait for Sub-workflow completion)",
"height": 254.84308966329985,
"width": 457.58120569815793
},
"id": "534ef523-3453-4a16-9ff0-8ac9f025d47d",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [500, 1080]
},
{
"parameters": {
"content": "## Execute Workflow (Run once with for each item/ DONT Wait for Sub-workflow completion) ",
"height": 284.59778445962905,
"width": 457.58120569815793
},
"id": "838f0fa3-5ee4-4d1a-afb8-42e009f1aa9e",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [580, 1400]
},
{
"parameters": {
"category": "randomData",
"randomDataSeed": "1",
"randomDataCount": 3
},
"id": "86699a49-2aa7-488e-8ea9-828404c98f08",
"name": "DebugHelper",
"type": "n8n-nodes-base.debugHelper",
"typeVersion": 1,
"position": [320, 1120]
},
{
"parameters": {
"content": "## Execute Workflow (Run once with for each item/ Wait for Sub-workflow completion) ",
"height": 284.59778445962905,
"width": 457.58120569815793
},
"id": "885d35f0-8ae6-45ec-821b-a82c27e7577a",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [480, 760]
},
{
"parameters": {
"content": "## Execute Workflow (Run once with all items/ Wait for Sub-workflow completion) (default behavior)",
"height": 254.84308966329985,
"width": 457.58120569815793
},
"id": "505bd7f2-767e-41b8-9325-77300aed5883",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [460, 460]
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "DebugHelper1",
"type": "main",
"index": 0
},
{
"node": "DebugHelper",
"type": "main",
"index": 0
}
]
]
},
"DebugHelper1": {
"main": [
[
{
"node": "Execute Workflow with param3",
"type": "main",
"index": 0
},
{
"node": "Execute Workflow with param2",
"type": "main",
"index": 0
},
{
"node": "Execute Workflow with param1",
"type": "main",
"index": 0
},
{
"node": "Execute Workflow with param",
"type": "main",
"index": 0
}
]
]
},
"Execute Workflow with param3": {
"main": [
[
{
"node": "Edit Fields8",
"type": "main",
"index": 0
}
]
]
},
"Execute Workflow with param2": {
"main": [
[
{
"node": "Edit Fields7",
"type": "main",
"index": 0
}
]
]
},
"Execute Workflow with param1": {
"main": [
[
{
"node": "Edit Fields6",
"type": "main",
"index": 0
}
]
]
},
"Execute Workflow with param": {
"main": [
[
{
"node": "Edit Fields2",
"type": "main",
"index": 0
}
]
]
},
"DebugHelper": {
"main": [
[
{
"node": "Execute Workflow with param2",
"type": "main",
"index": 0
},
{
"node": "Execute Workflow with param3",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}

View file

@ -0,0 +1,85 @@
{
"nodes": [
{
"parameters": {},
"id": "418350b8-b402-4d3b-93ba-3794d36c1ad5",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [440, 380]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "",
"rightValue": "",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{},
{}
]
},
"options": {}
},
"id": "b67ad46f-6b0d-4ff4-b2d2-dfbde44e287c",
"name": "Switch",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [660, 380]
},
{
"parameters": {
"options": {}
},
"id": "24731c11-e2a4-4854-81a6-277ce72e8a93",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3,
"position": [840, 480]
}
],
"connections": {
"When clicking \"Test workflow\"": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
},
"Switch": {
"main": [
null,
null,
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}

View file

@ -0,0 +1,150 @@
{
"meta": {
"instanceId": "777c68374367604fdf2a0bcfe9b1b574575ddea61aa8268e4bf034434bd7c894"
},
"nodes": [
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "0effebfc-fa8c-4d41-8a37-6d5695dfc9ee",
"name": "test",
"value": "test",
"type": "string"
},
{
"id": "beb8723f-6333-4186-ab88-41d4e2338866",
"name": "test",
"value": "test",
"type": "string"
},
{
"id": "85095836-4e94-442f-9270-e1a89008c129",
"name": "test",
"value": "test",
"type": "string"
},
{
"id": "b6163f8a-bca6-4364-8b38-182df37c55cd",
"name": "=should be visible!",
"value": "=not visible",
"type": "string"
}
]
},
"options": {}
},
"id": "950fcdc1-9e92-410f-8377-d4240e9bf6ff",
"name": "Edit Fields1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
680,
460
]
},
{
"parameters": {
"messageType": "block",
"blocksUi": "blocks",
"text": "=should be visible",
"otherOptions": {
"sendAsUser": "=not visible"
}
},
"id": "dcf7410d-0f8e-4cdb-9819-ae275558bdaa",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.2,
"position": [
900,
460
],
"webhookId": "002b502e-31e5-4fdb-ac43-a56cfde8f82a"
},
{
"parameters": {
"rule": {
"interval": [
{},
{
"field": "=should be visible"
},
{
"field": "=not visible"
}
]
}
},
"id": "4c948a3f-19d4-4b08-a8be-f7d2964a21f4",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
460,
460
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "5dcaab37-1146-49c6-97a3-3b2f73483270",
"name": "object",
"value": "=1 visible!\n2 {\n3 \"str\": \"two\",\n4 \"str_date\": \"{{ $now }}\",\n5 \"str_int\": \"1\",\n6 \"str_float\": \"1.234\",\n7 not visible!\n \"str_bool\": \"true\",\n \"str_email\": \"david@thedavid.com\",\n \"str_with_email\":\"My email is david@n8n.io\",\n \"str_json_single\":\"{'one':'two'}\",\n \"str_json_double\":\"{\\\"one\\\":\\\"two\\\"}\",\n \"bool\": true,\n \"list\": [1, 2, 3],\n \"decimal\": 1.234,\n \"timestamp1\": 1708695471,\n \"timestamp2\": 1708695471000,\n \"timestamp3\": 1708695471000000,\n \"num_one\": 1\n}",
"type": "object"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "a41dfb0d-38aa-42d2-b3e2-1854090bd319",
"name": "With long expression",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3,
"position": [
1100,
460
]
}
],
"connections": {
"Edit Fields1": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "With long expression",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}

View file

@ -0,0 +1,70 @@
{
"meta": {
"instanceId": "4d0676b62208d810ef035130bbfc9fd3afdc78d963ea8ccb9514dc89066efc94"
},
"nodes": [
{
"parameters": {},
"id": "bb7f8bb3-840a-464c-a7de-d3a80538c2be",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0]
},
{
"parameters": {
"workflowId": {},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"ignoreTypeMismatchErrors": false,
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [500, 240],
"id": "6b6e2e34-c6ab-4083-b8e3-6b0d56be5453",
"name": "Execute Workflow"
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Execute Workflow",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"When clicking Test workflow": [
{
"aaString": "A String",
"aaNumber": 1,
"aaArray": [1, true, "3"],
"aaObject": {
"aKey": -1
},
"aaAny": {}
},
{
"aaString": "Another String",
"aaNumber": 2,
"aaArray": [],
"aaObject": {
"aDifferentKey": -1
},
"aaAny": []
}
]
}
}

View file

@ -1,7 +1,7 @@
{
"workflow": {
"id": 1205,
"name": "Promote new Shopify products on Twitter and Telegram",
"name": "Promote new Shopify products",
"views": 478,
"recentViews": 9880,
"totalViews": 478,

View file

@ -0,0 +1,135 @@
{
"name": "NDV search bugs (introduced by schema view?)",
"nodes": [
{
"parameters": {},
"id": "55635c7b-92ee-4d2d-a0c0-baff9ab071da",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"position": [
800,
380
],
"typeVersion": 1
},
{
"parameters": {
"operation": "getAllPeople"
},
"id": "4737af43-e49b-4c92-b76f-32605c047114",
"name": "Customer Datastore (n8n training)",
"type": "n8n-nodes-base.n8nTrainingCustomerDatastore",
"typeVersion": 1,
"position": [
1020,
380
]
},
{
"parameters": {
"assignments": {
"assignments": []
},
"includeOtherFields": true,
"options": {}
},
"id": "8cc9b374-1856-4f3f-9315-08e6e27840d8",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1240,
380
]
}
],
"pinData": {
"Customer Datastore (n8n training)": [
{
"json": {
"id": "23423532",
"name": "Jay Gatsby",
"email": "gatsby@west-egg.com",
"notes": "Keeps asking about a green light??",
"country": "US",
"created": "1925-04-10"
}
},
{
"json": {
"id": "23423533",
"name": "José Arcadio Buendía",
"email": "jab@macondo.co",
"notes": "Lots of people named after him. Very confusing",
"country": "CO",
"created": "1967-05-05"
}
},
{
"json": {
"id": "23423534",
"name": "Max Sendak",
"email": "info@in-and-out-of-weeks.org",
"notes": "Keeps rolling his terrible eyes",
"country": "US",
"created": "1963-04-09"
}
},
{
"json": {
"id": "23423535",
"name": "Zaphod Beeblebrox",
"email": "captain@heartofgold.com",
"notes": "Felt like I was talking to more than one person",
"country": null,
"created": "1979-10-12"
}
},
{
"json": {
"id": "23423536",
"name": "Edmund Pevensie",
"email": "edmund@narnia.gov",
"notes": "Passionate sailor",
"country": "UK",
"created": "1950-10-16"
}
}
]
},
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Customer Datastore (n8n training)",
"type": "main",
"index": 0
}
]
]
},
"Customer Datastore (n8n training)": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "20178044-fb64-4443-88dd-e941517520d0",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "aBVnTRON9Y2cSmse",
"tags": []
}

View file

@ -0,0 +1,94 @@
{
"nodes": [
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6f0cf983-824b-4339-a5de-6b374a23b4b0",
"leftValue": "={{ $json.a }}",
"rightValue": 3,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [220, 0],
"id": "1755282a-ec4a-4d02-a833-0316ca413cc4",
"name": "If"
},
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "de1e7acf-12d8-4e56-ba42-709ffb397db2",
"name": "When clicking Test workflow"
},
{
"parameters": {
"category": "randomData"
},
"type": "n8n-nodes-base.debugHelper",
"typeVersion": 1,
"position": [580, 0],
"id": "86440d33-f833-453c-bcaa-fff7e0083501",
"name": "DebugHelper",
"alwaysOutputData": true
}
],
"connections": {
"If": {
"main": [
[
{
"node": "DebugHelper",
"type": "main",
"index": 0
}
],
[
{
"node": "DebugHelper",
"type": "main",
"index": 0
}
]
]
},
"When clicking Test workflow": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"When clicking Test workflow": [
{
"a": 1
},
{
"a": 2
}
]
}
}

View file

@ -0,0 +1,22 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "**Code** node ran successfully, did my solution help resolve your issue?",
"quickReplies": [
{
"text": "Yes, thanks",
"type": "all-good",
"isFeedback": true
},
{
"text": "No, I am still stuck",
"type": "still-stuck",
"isFeedback": true
}
]
}
]
}

View file

@ -1202,7 +1202,7 @@
},
{
"id": 1205,
"name": "Promote New Shopify Products on Social Media (Twitter and Telegram)",
"name": "Promote New Shopify Products",
"totalViews": 219,
"recentViews": 0,
"user": {

View file

@ -6,6 +6,7 @@
"cypress:install": "cypress install",
"test:e2e:ui": "scripts/run-e2e.js ui",
"test:e2e:dev": "scripts/run-e2e.js dev",
"test:e2e:dev:v2": "scripts/run-e2e.js dev:v2",
"test:e2e:all": "scripts/run-e2e.js all",
"format": "biome format --write .",
"format:check": "biome ci .",
@ -26,6 +27,7 @@
"cypress": "^13.14.2",
"cypress-otp": "^1.0.3",
"cypress-real-events": "^1.13.0",
"flatted": "catalog:",
"lodash": "catalog:",
"nanoid": "catalog:",
"start-server-and-test": "^2.0.8"

View file

@ -5,7 +5,11 @@ export class CredentialsPage extends BasePage {
getters = {
emptyListCreateCredentialButton: () => cy.getByTestId('empty-resources-list').find('button'),
createCredentialButton: () => cy.getByTestId('resources-list-add'),
createCredentialButton: () => {
cy.getByTestId('add-resource').should('be.visible').click();
cy.getByTestId('add-resource').getByTestId('action-credential').should('be.visible');
return cy.getByTestId('add-resource').getByTestId('action-credential');
},
searchInput: () => cy.getByTestId('resources-list-search'),
emptyList: () => cy.getByTestId('resources-list-empty'),
credentialCards: () => cy.getByTestId('resources-list-item'),

View file

@ -8,18 +8,18 @@ export class MfaLoginPage extends BasePage {
getters = {
form: () => cy.getByTestId('mfa-login-form'),
token: () => cy.getByTestId('token'),
recoveryCode: () => cy.getByTestId('recoveryCode'),
mfaCode: () => cy.getByTestId('mfaCode'),
mfaRecoveryCode: () => cy.getByTestId('mfaRecoveryCode'),
enterRecoveryCodeButton: () => cy.getByTestId('mfa-enter-recovery-code-button'),
};
actions = {
loginWithMfaToken: (email: string, password: string, mfaToken: string) => {
loginWithMfaCode: (email: string, password: string, mfaCode: string) => {
const signinPage = new SigninPage();
const workflowsPage = new WorkflowsPage();
cy.session(
[mfaToken],
[mfaCode],
() => {
cy.visit(signinPage.url);
@ -30,7 +30,7 @@ export class MfaLoginPage extends BasePage {
});
this.getters.form().within(() => {
this.getters.token().type(mfaToken);
this.getters.mfaCode().type(mfaCode);
});
// we should be redirected to /workflows
@ -43,12 +43,12 @@ export class MfaLoginPage extends BasePage {
},
);
},
loginWithRecoveryCode: (email: string, password: string, recoveryCode: string) => {
loginWithMfaRecoveryCode: (email: string, password: string, mfaRecoveryCode: string) => {
const signinPage = new SigninPage();
const workflowsPage = new WorkflowsPage();
cy.session(
[recoveryCode],
[mfaRecoveryCode],
() => {
cy.visit(signinPage.url);
@ -61,7 +61,7 @@ export class MfaLoginPage extends BasePage {
this.getters.enterRecoveryCodeButton().click();
this.getters.form().within(() => {
this.getters.recoveryCode().type(recoveryCode);
this.getters.mfaRecoveryCode().type(mfaRecoveryCode);
});
// we should be redirected to /workflows

View file

@ -1,3 +1,4 @@
import { getCredentialSaveButton, saveCredential } from '../../composables/modals/credential-modal';
import { getVisibleSelect } from '../../utils';
import { BasePage } from '../base';
@ -13,8 +14,6 @@ export class CredentialsModal extends BasePage {
this.getters.credentialInputs().find(`:contains('${fieldName}') .n8n-input input`),
name: () => cy.getByTestId('credential-name'),
nameInput: () => cy.getByTestId('credential-name').find('input'),
// Saving of the credentials takes a while on the CI so we need to increase the timeout
saveButton: () => cy.getByTestId('credential-save-button', { timeout: 5000 }),
deleteButton: () => cy.getByTestId('credential-delete-button'),
closeButton: () => this.getters.editCredentialModal().find('.el-dialog__close').first(),
oauthConnectButton: () => cy.getByTestId('oauth-connect-button'),
@ -41,17 +40,17 @@ export class CredentialsModal extends BasePage {
},
save: (test = false) => {
cy.intercept('POST', '/rest/credentials').as('saveCredential');
this.getters.saveButton().click({ force: true });
saveCredential();
cy.wait('@saveCredential');
if (test) cy.wait('@testCredential');
this.getters.saveButton().should('contain.text', 'Saved');
getCredentialSaveButton().should('contain.text', 'Saved');
},
saveSharing: () => {
cy.intercept('PUT', '/rest/credentials/*/share').as('shareCredential');
this.getters.saveButton().click({ force: true });
saveCredential();
cy.wait('@shareCredential');
this.getters.saveButton().should('contain.text', 'Saved');
getCredentialSaveButton().should('contain.text', 'Saved');
},
close: () => {
this.getters.closeButton().click();
@ -65,7 +64,7 @@ export class CredentialsModal extends BasePage {
.each(($el) => {
cy.wrap($el).type('test');
});
this.getters.saveButton().click();
saveCredential();
if (closeModal) {
this.getters.closeButton().click();
}

View file

@ -20,7 +20,8 @@ export class NDV extends BasePage {
outputDataContainer: () => this.getters.outputPanel().findChildByTestId('ndv-data-container'),
outputDisplayMode: () =>
this.getters.outputPanel().findChildByTestId('ndv-run-data-display-mode').first(),
pinDataButton: () => cy.getByTestId('ndv-pin-data'),
pinDataButton: () => this.getters.outputPanel().findChildByTestId('ndv-pin-data'),
unpinDataLink: () => this.getters.outputPanel().findChildByTestId('ndv-unpin-data'),
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller .cm-content'),
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
@ -63,6 +64,7 @@ export class NDV extends BasePage {
nodeRenameInput: () => cy.getByTestId('node-rename-input'),
executePrevious: () => cy.getByTestId('execute-previous-node'),
httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'),
nodeCredentialsLabel: () => cy.getByTestId('credentials-label'),
nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n),
inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'),
inputLinkRun: () => this.getters.inputPanel().findChildByTestId('link-run'),
@ -130,8 +132,9 @@ export class NDV extends BasePage {
codeEditorFullscreenButton: () => cy.getByTestId('code-editor-fullscreen-button'),
codeEditorDialog: () => cy.getByTestId('code-editor-fullscreen'),
codeEditorFullscreen: () => this.getters.codeEditorDialog().find('.cm-content'),
nodeRunSuccessIndicator: () => cy.getByTestId('node-run-info-success'),
nodeRunErrorIndicator: () => cy.getByTestId('node-run-info-danger'),
nodeRunTooltipIndicator: () => cy.getByTestId('node-run-info'),
nodeRunSuccessIndicator: () => cy.getByTestId('node-run-status-success'),
nodeRunErrorIndicator: () => cy.getByTestId('node-run-status-danger'),
nodeRunErrorMessage: () => cy.getByTestId('node-error-message'),
nodeRunErrorDescription: () => cy.getByTestId('node-error-description'),
fixedCollectionParameter: (paramName: string) =>
@ -146,6 +149,9 @@ export class NDV extends BasePage {
pinData: () => {
this.getters.pinDataButton().click({ force: true });
},
unPinData: () => {
this.getters.unpinDataLink().click({ force: true });
},
editPinnedData: () => {
this.getters.editPinnedDataButton().click();
},
@ -221,9 +227,6 @@ export class NDV extends BasePage {
this.getters.inputSelect().find('.el-select').click();
this.getters.inputOption().contains(nodeName).click();
},
expandSchemaViewNode: (nodeName: string) => {
this.getters.schemaViewNodeName().contains(nodeName).click();
},
addDefaultPinnedData: () => {
this.actions.editPinnedData();
this.actions.savePinnedData();
@ -317,6 +320,17 @@ export class NDV extends BasePage {
addItemToFixedCollection: (paramName: string) => {
this.getters.fixedCollectionParameter(paramName).getByTestId('fixed-collection-add').click();
},
typeIntoFixedCollectionItem: (fixedCollectionName: string, index: number, content: string) => {
this.getters.fixedCollectionParameter(fixedCollectionName).within(() => {
cy.getByTestId('parameter-input').eq(index).type(content);
});
},
dragMainPanelToLeft: () => {
cy.drag('[data-test-id=panel-drag-button]', [-1000, 0], { moveTwice: true });
},
dragMainPanelToRight: () => {
cy.drag('[data-test-id=panel-drag-button]', [1000, 0], { moveTwice: true });
},
};
}

View file

@ -22,6 +22,8 @@ export class PersonalSettingsPage extends BasePage {
saveSettingsButton: () => cy.getByTestId('save-settings-button'),
enableMfaButton: () => cy.getByTestId('enable-mfa-button'),
disableMfaButton: () => cy.getByTestId('disable-mfa-button'),
mfaCodeOrMfaRecoveryCodeInput: () => cy.getByTestId('mfa-code-or-recovery-code-input'),
mfaSaveButton: () => cy.getByTestId('mfa-save-button'),
themeSelector: () => cy.getByTestId('theme-select'),
selectOptionsVisible: () => cy.get('.el-select-dropdown:visible .el-select-dropdown__item'),
};
@ -83,9 +85,11 @@ export class PersonalSettingsPage extends BasePage {
mfaSetupModal.getters.saveButton().click();
});
},
disableMfa: () => {
disableMfa: (mfaCodeOrRecoveryCode: string) => {
cy.visit(this.url);
this.getters.disableMfaButton().click();
this.getters.mfaCodeOrMfaRecoveryCodeInput().type(mfaCodeOrRecoveryCode);
this.getters.mfaSaveButton().click();
},
};
}

View file

@ -30,6 +30,12 @@ export class WorkflowExecutionsTab extends BasePage {
actions = {
toggleNodeEnabled: (nodeName: string) => {
cy.ifCanvasVersion(
() => {},
() => {
cy.get('body').click(); // Cancel selection if it exists
},
);
workflowPage.getters.canvasNodeByName(nodeName).click();
cy.get('body').type('d', { force: true });
},

View file

@ -1,8 +1,9 @@
import { BasePage } from './base';
import { NodeCreator } from './features/node-creator';
import { META_KEY } from '../constants';
import type { OpenContextMenuOptions } from '../types';
import { getVisibleSelect } from '../utils';
import { getUniqueWorkflowName } from '../utils/workflowUtils';
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';
const nodeCreator = new NodeCreator();
export class WorkflowPage extends BasePage {
@ -17,7 +18,8 @@ export class WorkflowPage extends BasePage {
workflowTagsContainer: () => cy.getByTestId('workflow-tags-container'),
workflowTagsInput: () =>
this.getters.workflowTagsContainer().then(($el) => cy.wrap($el.find('input').first())),
tagPills: () => cy.get('[data-test-id="workflow-tags-container"] span.el-tag'),
tagPills: () =>
cy.get('[data-test-id="workflow-tags-container"] span.el-tag:not(.count-container)'),
nthTagPill: (n: number) =>
cy.get(`[data-test-id="workflow-tags-container"] span.el-tag:nth-child(${n})`),
tagsDropdown: () => cy.getByTestId('workflow-tags-dropdown'),
@ -27,7 +29,15 @@ export class WorkflowPage extends BasePage {
nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'),
nodeCreatorPlusButton: () => cy.getByTestId('node-creator-plus-button'),
canvasPlusButton: () => cy.getByTestId('canvas-plus-button'),
canvasNodes: () => cy.getByTestId('canvas-node'),
canvasNodes: () =>
cy.ifCanvasVersion(
() => cy.getByTestId('canvas-node'),
() =>
cy
.getByTestId('canvas-node')
.not('[data-node-type="n8n-nodes-internal.addNodes"]')
.not('[data-node-type="n8n-nodes-base.stickyNote"]'),
),
canvasNodeByName: (nodeName: string) =>
this.getters.canvasNodes().filter(`:contains(${nodeName})`),
nodeIssuesByName: (nodeName: string) =>
@ -37,6 +47,17 @@ export class WorkflowPage extends BasePage {
.should('have.length.greaterThan', 0)
.findChildByTestId('node-issues'),
getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => {
if (isCanvasV2()) {
if (type === 'input') {
return `[data-test-id="canvas-node-input-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`;
}
if (type === 'output') {
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`;
}
if (type === 'plus') {
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"] [data-test-id="canvas-handle-plus"] .clickable`;
}
}
return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`;
},
canvasNodeInputEndpointByName: (nodeName: string, index = 0) => {
@ -46,7 +67,15 @@ export class WorkflowPage extends BasePage {
return cy.get(this.getters.getEndpointSelector('output', nodeName, index));
},
canvasNodePlusEndpointByName: (nodeName: string, index = 0) => {
return cy.get(this.getters.getEndpointSelector('plus', nodeName, index));
return cy.ifCanvasVersion(
() => cy.get(this.getters.getEndpointSelector('plus', nodeName, index)),
() =>
cy
.get(
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"] .clickable`,
)
.eq(index),
);
},
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
workflowMenu: () => cy.getByTestId('workflow-menu'),
@ -56,13 +85,29 @@ export class WorkflowPage extends BasePage {
expressionModalInput: () => cy.getByTestId('expression-modal-input').find('[role=textbox]'),
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
nodeViewRoot: () => cy.getByTestId('node-view-root'),
nodeViewRoot: () =>
cy.ifCanvasVersion(
() => cy.getByTestId('node-view-root'),
() => this.getters.nodeView(),
),
copyPasteInput: () => cy.getByTestId('hidden-copy-paste'),
nodeConnections: () => cy.get('.jtk-connector'),
nodeConnections: () =>
cy.ifCanvasVersion(
() => cy.get('.jtk-connector'),
() => cy.getByTestId('edge-label-wrapper'),
),
zoomToFitButton: () => cy.getByTestId('zoom-to-fit'),
nodeEndpoints: () => cy.get('.jtk-endpoint-connected'),
disabledNodes: () => cy.get('.node-box.disabled'),
selectedNodes: () => this.getters.canvasNodes().filter('.jtk-drag-selected'),
disabledNodes: () =>
cy.ifCanvasVersion(
() => cy.get('.node-box.disabled'),
() => cy.get('[data-test-id*="node"][class*="disabled"]'),
),
selectedNodes: () =>
cy.ifCanvasVersion(
() => this.getters.canvasNodes().filter('.jtk-drag-selected'),
() => this.getters.canvasNodes().parent().filter('.selected'),
),
// Workflow menu items
workflowMenuItemDuplicate: () => cy.getByTestId('workflow-menu-item-duplicate'),
workflowMenuItemDownload: () => cy.getByTestId('workflow-menu-item-download'),
@ -92,8 +137,21 @@ export class WorkflowPage extends BasePage {
shareButton: () => cy.getByTestId('workflow-share-button'),
duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'),
nodeViewBackground: () => cy.getByTestId('node-view-background'),
nodeView: () => cy.getByTestId('node-view'),
nodeViewBackground: () =>
cy.ifCanvasVersion(
() => cy.getByTestId('node-view-background'),
() => cy.getByTestId('canvas'),
),
nodeView: () =>
cy.ifCanvasVersion(
() => cy.getByTestId('node-view'),
() => cy.get('[data-test-id="canvas-wrapper"]'),
),
canvasViewport: () =>
cy.ifCanvasVersion(
() => cy.getByTestId('node-view'),
() => cy.get('.vue-flow__transformationpane.vue-flow__container'),
),
inlineExpressionEditorInput: () =>
cy.getByTestId('inline-expression-editor-input').find('[role=textbox]'),
inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'),
@ -115,12 +173,26 @@ export class WorkflowPage extends BasePage {
ndvParameters: () => cy.getByTestId('parameter-item'),
nodeCredentialsLabel: () => cy.getByTestId('credentials-label'),
getConnectionBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
cy.get(
`.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
cy.ifCanvasVersion(
() =>
cy.get(
`.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
),
() =>
cy.get(
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`,
),
),
getConnectionActionsBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
cy.get(
`.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
cy.ifCanvasVersion(
() =>
cy.get(
`.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
),
() =>
cy.get(
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`,
),
),
addStickyButton: () => cy.getByTestId('add-sticky-button'),
stickies: () => cy.getByTestId('sticky'),
@ -128,6 +200,18 @@ export class WorkflowPage extends BasePage {
workflowHistoryButton: () => cy.getByTestId('workflow-history-button'),
colors: () => cy.getByTestId('color'),
contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`),
getNodeLeftPosition: (element: JQuery<HTMLElement>) => {
if (isCanvasV2()) {
return parseFloat(element.parent().css('transform').split(',')[4]);
}
return parseFloat(element.css('left'));
},
getNodeTopPosition: (element: JQuery<HTMLElement>) => {
if (isCanvasV2()) {
return parseFloat(element.parent().css('transform').split(',')[5]);
}
return parseFloat(element.css('top'));
},
};
actions = {
@ -193,14 +277,14 @@ export class WorkflowPage extends BasePage {
},
openContextMenu: (
nodeTypeName?: string,
method: 'right-click' | 'overflow-button' = 'right-click',
{ method = 'right-click', anchor = 'center' }: OpenContextMenuOptions = {},
) => {
const target = nodeTypeName
? this.getters.canvasNodeByName(nodeTypeName)
: this.getters.nodeViewBackground();
if (method === 'right-click') {
target.rightclick(nodeTypeName ? 'center' : 'topLeft', { force: true });
target.rightclick(nodeTypeName ? anchor : 'topLeft', { force: true });
} else {
target.realHover();
target.find('[data-test-id="overflow-node-button"]').click({ force: true });
@ -217,8 +301,8 @@ export class WorkflowPage extends BasePage {
this.actions.openContextMenu(nodeTypeName);
this.actions.contextMenuAction('delete');
},
executeNode: (nodeTypeName: string) => {
this.actions.openContextMenu(nodeTypeName);
executeNode: (nodeTypeName: string, options?: OpenContextMenuOptions) => {
this.actions.openContextMenu(nodeTypeName, options);
this.actions.contextMenuAction('execute');
},
addStickyFromContextMenu: () => {
@ -245,7 +329,7 @@ export class WorkflowPage extends BasePage {
this.actions.contextMenuAction('toggle_pin');
},
openNodeFromContextMenu: (nodeTypeName: string) => {
this.actions.openContextMenu(nodeTypeName, 'overflow-button');
this.actions.openContextMenu(nodeTypeName, { method: 'overflow-button' });
this.actions.contextMenuAction('open');
},
selectAllFromContextMenu: () => {
@ -253,8 +337,14 @@ export class WorkflowPage extends BasePage {
this.actions.contextMenuAction('select_all');
},
deselectAll: () => {
this.actions.openContextMenu();
this.actions.contextMenuAction('deselect_all');
cy.ifCanvasVersion(
() => {
this.actions.openContextMenu();
this.actions.contextMenuAction('deselect_all');
},
// rightclick doesn't work with vueFlow canvas
() => this.getters.nodeViewBackground().click('topLeft'),
);
},
openExpressionEditorModal: () => {
cy.contains('Expression').invoke('show').click();
@ -332,7 +422,7 @@ export class WorkflowPage extends BasePage {
pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => {
cy.window().then((win) => {
// Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling)
this.getters.nodeViewBackground().trigger('wheel', {
this.getters.nodeView().trigger('wheel', {
force: true,
bubbles: true,
ctrlKey: true,
@ -391,9 +481,12 @@ export class WorkflowPage extends BasePage {
action?: string,
) => {
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
this.getters
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
.find('.add')
const connectionsBetweenNodes = () =>
this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName);
cy.ifCanvasVersion(
() => connectionsBetweenNodes().find('.add'),
() => connectionsBetweenNodes().get('[data-test-id="add-connection-button"]'),
)
.first()
.click({ force: true });
@ -401,9 +494,12 @@ export class WorkflowPage extends BasePage {
},
deleteNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string) => {
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
this.getters
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
.find('.delete')
const connectionsBetweenNodes = () =>
this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName);
cy.ifCanvasVersion(
() => connectionsBetweenNodes().find('.delete'),
() => connectionsBetweenNodes().get('[data-test-id="delete-connection-button"]'),
)
.first()
.click({ force: true });
},

View file

@ -7,7 +7,10 @@ export class WorkflowsPage extends BasePage {
newWorkflowButtonCard: () => cy.getByTestId('new-workflow-card'),
newWorkflowTemplateCard: () => cy.getByTestId('new-workflow-template-card'),
searchBar: () => cy.getByTestId('resources-list-search'),
createWorkflowButton: () => cy.getByTestId('resources-list-add'),
createWorkflowButton: () => {
cy.getByTestId('add-resource-workflow').should('be.visible');
return cy.getByTestId('add-resource-workflow');
},
workflowCards: () => cy.getByTestId('resources-list-item'),
workflowCard: (workflowName: string) =>
this.getters
@ -16,6 +19,8 @@ export class WorkflowsPage extends BasePage {
.parents('[data-test-id="resources-list-item"]'),
workflowTags: (workflowName: string) =>
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-tags'),
workflowCardContent: (workflowName: string) =>
this.getters.workflowCard(workflowName).findChildByTestId('card-content'),
workflowActivator: (workflowName: string) =>
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-activator'),
workflowActivatorStatus: (workflowName: string) =>

View file

@ -57,6 +57,17 @@ switch (scenario) {
},
});
break;
case 'dev:v2':
runTests({
startCommand: 'develop',
url: 'http://localhost:8080/favicon.ico',
testCommand: 'cypress open',
customEnv: {
CYPRESS_NODE_VIEW_VERSION: 2,
CYPRESS_BASE_URL: 'http://localhost:8080',
},
});
break;
case 'all':
const specSuiteFilter = process.argv[3];
const specParam = specSuiteFilter ? ` --spec **/*${specSuiteFilter}*` : '';

View file

@ -10,7 +10,7 @@ import {
N8N_AUTH_COOKIE,
} from '../constants';
import { WorkflowPage } from '../pages';
import { getUniqueWorkflowName } from '../utils/workflowUtils';
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';
Cypress.Commands.add('setAppDate', (targetDate: number | Date) => {
cy.window().then((win) => {
@ -26,6 +26,10 @@ Cypress.Commands.add('getByTestId', (selector, ...args) => {
return cy.get(`[data-test-id="${selector}"]`, ...args);
});
Cypress.Commands.add('ifCanvasVersion', (getterV1, getterV2) => {
return isCanvasV2() ? getterV2() : getterV1();
});
Cypress.Commands.add(
'createFixtureWorkflow',
(fixtureKey: string, workflowName = getUniqueWorkflowName()) => {
@ -70,6 +74,15 @@ Cypress.Commands.add('signin', ({ email, password }) => {
})
.then((response) => {
Cypress.env('currentUserId', response.body.data.id);
// @TODO Remove this once the switcher is removed
cy.window().then((win) => {
win.localStorage.setItem('NodeView.migrated', 'true');
win.localStorage.setItem('NodeView.switcher.discovered.beta', 'true');
const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION');
win.localStorage.setItem('NodeView.version', nodeViewVersion ?? '1');
});
});
});
});
@ -169,6 +182,16 @@ Cypress.Commands.add('drag', (selector, pos, options) => {
pageY: newPosition.y,
force: true,
});
if (options?.moveTwice) {
// first move like hover to trigger object to be visible
// like in main panel in ndv
element.trigger('mousemove', {
which: 1,
pageX: newPosition.x,
pageY: newPosition.y,
force: true,
});
}
if (options?.clickToFinish) {
// Click to finish the drag
// For some reason, mouseup isn't working when moving nodes

View file

@ -1,7 +1,7 @@
// Load type definitions that come with Cypress module
/// <reference types="cypress" />
import type { FrontendSettings } from '@n8n/api-types';
import type { FrontendSettings, PushPayload, PushType } from '@n8n/api-types';
Cypress.Keyboard.defaults({
keystrokeDelay: 0,
@ -28,6 +28,7 @@ declare global {
selector: string,
...args: Array<Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined>
): Chainable<JQuery<HTMLElement>>;
ifCanvasVersion<T1, T2>(getterV1: () => T1, getterV2: () => T2): T1 | T2;
findChildByTestId(childTestId: string): Chainable<JQuery<HTMLElement>>;
/**
* Creates a workflow from the given fixture and optionally renames it.
@ -58,14 +59,20 @@ declare global {
drag(
selector: string | Chainable<JQuery<HTMLElement>>,
target: [number, number],
options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean },
options?: {
abs?: boolean;
index?: number;
realMouse?: boolean;
clickToFinish?: boolean;
moveTwice?: boolean;
},
): void;
draganddrop(
draggableSelector: string,
droppableSelector: string,
options?: Partial<DragAndDropOptions>,
): void;
push(type: string, data: unknown): void;
push<Type extends PushType>(type: Type, data: PushPayload<Type>): void;
shouldNotHaveConsoleErrors(): void;
window(): Chainable<
AUTWindow & {

View file

@ -22,3 +22,8 @@ export interface ExecutionResponse {
results: Execution[];
};
}
export type OpenContextMenuOptions = {
method?: 'right-click' | 'overflow-button';
anchor?: 'topRight' | 'topLeft' | 'center' | 'bottomRight' | 'bottomLeft';
};

View file

@ -1,4 +1,5 @@
import type { IDataObject, IPinData, ITaskData, ITaskDataConnections } from 'n8n-workflow';
import { stringify } from 'flatted';
import type { IDataObject, ITaskData, ITaskDataConnections } from 'n8n-workflow';
import { nanoid } from 'nanoid';
import { clickExecuteWorkflowButton } from '../composables/workflow';
@ -16,7 +17,7 @@ export function createMockNodeExecutionData(
return {
[name]: {
startTime: new Date().getTime(),
executionTime: 0,
executionTime: 1,
executionStatus,
data: jsonData
? Object.keys(jsonData).reduce((acc, key) => {
@ -33,61 +34,23 @@ export function createMockNodeExecutionData(
}, {} as ITaskDataConnections)
: data,
source: [null],
inputOverride,
...rest,
},
};
}
export function createMockWorkflowExecutionData({
executionId,
runData,
pinData = {},
lastNodeExecuted,
}: {
executionId: string;
runData: Record<string, ITaskData | ITaskData[]>;
pinData?: IPinData;
lastNodeExecuted: string;
}) {
return {
executionId,
data: {
data: {
startData: {},
resultData: {
runData,
pinData,
lastNodeExecuted,
},
executionData: {
contextData: {},
nodeExecutionStack: [],
metadata: {},
waitingExecution: {},
waitingExecutionSource: {},
},
},
mode: 'manual',
startedAt: new Date().toISOString(),
stoppedAt: new Date().toISOString(),
status: 'success',
finished: true,
},
};
}
export function runMockWorkflowExecution({
trigger,
lastNodeExecuted,
runData,
workflowExecutionData,
}: {
trigger?: () => void;
lastNodeExecuted: string;
runData: Array<ReturnType<typeof createMockNodeExecutionData>>;
workflowExecutionData?: ReturnType<typeof createMockWorkflowExecutionData>;
}) {
const executionId = nanoid(8);
const workflowId = nanoid();
const executionId = Math.floor(Math.random() * 1_000_000).toString();
cy.intercept('POST', '/rest/workflows/**/run?**', {
statusCode: 201,
@ -124,13 +87,24 @@ export function runMockWorkflowExecution({
resolvedRunData[nodeName] = nodeExecution[nodeName];
});
cy.push(
'executionFinished',
createMockWorkflowExecutionData({
executionId,
lastNodeExecuted,
runData: resolvedRunData,
...workflowExecutionData,
cy.push('executionFinished', {
executionId,
workflowId,
status: 'success',
rawData: stringify({
startData: {},
resultData: {
runData,
pinData: {},
lastNodeExecuted,
},
executionData: {
contextData: {},
nodeExecutionStack: [],
metadata: {},
waitingExecution: {},
waitingExecutionSource: {},
},
}),
);
});
}

View file

@ -3,3 +3,7 @@ import { nanoid } from 'nanoid';
export function getUniqueWorkflowName(workflowNamePrefix?: string) {
return workflowNamePrefix ? `${workflowNamePrefix} ${nanoid(12)}` : nanoid(12);
}
export function isCanvasV2() {
return Cypress.env('NODE_VIEW_VERSION') === 2;
}

View file

@ -33,27 +33,22 @@ COPY docker/images/n8n/docker-entrypoint.sh /
# Setup the Task Runner Launcher
ARG TARGETPLATFORM
ARG LAUNCHER_VERSION=0.1.1
ENV N8N_RUNNERS_USE_LAUNCHER=true \
N8N_RUNNERS_LAUNCHER_PATH=/usr/local/bin/task-runner-launcher
ARG LAUNCHER_VERSION=1.1.0
COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json
# First, download, verify, then extract the launcher binary
# Second, chmod with 4555 to allow the use of setuid
# Third, create a new user and group to execute the Task Runners under
# Download, verify, then extract the launcher binary
RUN \
if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="x86_64"; \
elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="aarch64"; fi; \
if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="amd64"; \
elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="arm64"; fi; \
mkdir /launcher-temp && \
cd /launcher-temp && \
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip && \
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
sha256sum -c task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
unzip -d $(dirname ${N8N_RUNNERS_LAUNCHER_PATH}) task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip task-runner-launcher && \
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz && \
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256 && \
# The .sha256 does not contain the filename --> Form the correct checksum file
echo "$(cat task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256) task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz" > checksum.sha256 && \
sha256sum -c checksum.sha256 && \
tar xvf task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz --directory=/usr/local/bin && \
cd - && \
rm -r /launcher-temp && \
chmod 4555 ${N8N_RUNNERS_LAUNCHER_PATH} && \
addgroup -g 2000 task-runner && \
adduser -D -u 2000 -g "Task Runner User" -G task-runner task-runner
rm -r /launcher-temp
RUN \
cd /usr/local/lib/node_modules/n8n && \

View file

@ -24,27 +24,22 @@ RUN set -eux; \
# Setup the Task Runner Launcher
ARG TARGETPLATFORM
ARG LAUNCHER_VERSION=0.1.1
ENV N8N_RUNNERS_USE_LAUNCHER=true \
N8N_RUNNERS_LAUNCHER_PATH=/usr/local/bin/task-runner-launcher
ARG LAUNCHER_VERSION=1.1.0
COPY n8n-task-runners.json /etc/n8n-task-runners.json
# First, download, verify, then extract the launcher binary
# Second, chmod with 4555 to allow the use of setuid
# Third, create a new user and group to execute the Task Runners under
# Download, verify, then extract the launcher binary
RUN \
if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="x86_64"; \
elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="aarch64"; fi; \
if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="amd64"; \
elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="arm64"; fi; \
mkdir /launcher-temp && \
cd /launcher-temp && \
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip && \
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
sha256sum -c task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
unzip -d $(dirname ${N8N_RUNNERS_LAUNCHER_PATH}) task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip task-runner-launcher && \
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz && \
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256 && \
# The .sha256 does not contain the filename --> Form the correct checksum file
echo "$(cat task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256) task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz" > checksum.sha256 && \
sha256sum -c checksum.sha256 && \
tar xvf task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz --directory=/usr/local/bin && \
cd - && \
rm -r /launcher-temp && \
chmod 4555 ${N8N_RUNNERS_LAUNCHER_PATH} && \
addgroup -g 2000 task-runner && \
adduser -D -u 2000 -g "Task Runner User" -G task-runner task-runner
rm -r /launcher-temp
COPY docker-entrypoint.sh /

View file

@ -2,18 +2,28 @@
"task-runners": [
{
"runner-type": "javascript",
"workdir": "/home/task-runner",
"workdir": "/home/node",
"command": "/usr/local/bin/node",
"args": ["/usr/local/lib/node_modules/n8n/node_modules/@n8n/task-runner/dist/start.js"],
"allowed-env": [
"PATH",
"GENERIC_TIMEZONE",
"N8N_RUNNERS_GRANT_TOKEN",
"N8N_RUNNERS_N8N_URI",
"N8N_RUNNERS_TASK_BROKER_URI",
"N8N_RUNNERS_MAX_PAYLOAD",
"N8N_RUNNERS_MAX_CONCURRENCY",
"N8N_RUNNERS_TASK_TIMEOUT",
"N8N_RUNNERS_HEALTH_CHECK_SERVER_ENABLED",
"N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST",
"N8N_RUNNERS_HEALTH_CHECK_SERVER_PORT",
"NODE_FUNCTION_ALLOW_BUILTIN",
"NODE_FUNCTION_ALLOW_EXTERNAL"
],
"uid": 2000,
"gid": 2000
"NODE_FUNCTION_ALLOW_EXTERNAL",
"NODE_OPTIONS",
"N8N_SENTRY_DSN",
"N8N_VERSION",
"ENVIRONMENT",
"DEPLOYMENT_NAME"
]
}
]
}

View file

@ -1,12 +1,12 @@
{
"name": "n8n-monorepo",
"version": "1.64.0",
"version": "1.73.0",
"private": true,
"engines": {
"node": ">=20.15",
"pnpm": ">=9.5"
"pnpm": ">=9.15"
},
"packageManager": "pnpm@9.6.0",
"packageManager": "pnpm@9.15.1",
"scripts": {
"prepare": "node scripts/prepare.mjs",
"preinstall": "node scripts/block-npm-install.js",
@ -16,6 +16,7 @@
"build:nodes": "turbo run build:nodes",
"typecheck": "turbo typecheck",
"dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner",
"dev:be": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui",
"dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core",
"clean": "turbo run clean --parallel",
"reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs",
@ -43,7 +44,9 @@
"@biomejs/biome": "^1.9.0",
"@n8n_io/eslint-config": "workspace:*",
"@types/jest": "^29.5.3",
"@types/node": "*",
"@types/supertest": "^6.0.2",
"cross-env": "^7.0.3",
"jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2",
"jest-expect-message": "^1.1.3",
@ -57,9 +60,9 @@
"run-script-os": "^1.0.7",
"supertest": "^7.0.0",
"ts-jest": "^29.1.1",
"tsc-alias": "^1.8.7",
"tsc-watch": "^6.0.4",
"turbo": "2.1.2",
"tsc-alias": "^1.8.10",
"tsc-watch": "^6.2.0",
"turbo": "2.3.3",
"typescript": "*",
"zx": "^8.1.4"
},
@ -76,18 +79,18 @@
"semver": "^7.5.4",
"tslib": "^2.6.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.6.2",
"vue-tsc": "^2.1.6",
"typescript": "^5.7.2",
"vue-tsc": "^2.1.10",
"ws": ">=8.17.1"
},
"patchedDependencies": {
"typedi@0.10.0": "patches/typedi@0.10.0.patch",
"@sentry/cli@2.36.2": "patches/@sentry__cli@2.36.2.patch",
"pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch",
"pyodide@0.23.4": "patches/pyodide@0.23.4.patch",
"@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch",
"@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch",
"@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch"
"@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch",
"vue-tsc@2.1.10": "patches/vue-tsc@2.1.10.patch"
}
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/api-types",
"version": "0.5.0",
"version": "0.11.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",
@ -21,11 +21,12 @@
"dist/**/*"
],
"devDependencies": {
"@n8n/config": "workspace:*",
"n8n-workflow": "workspace:*"
},
"dependencies": {
"xss": "catalog:",
"zod": "catalog:",
"zod-class": "0.0.15"
"zod-class": "0.0.16"
}
}

View file

@ -0,0 +1,36 @@
import { AiApplySuggestionRequestDto } from '../ai-apply-suggestion-request.dto';
describe('AiApplySuggestionRequestDto', () => {
it('should validate a valid suggestion application request', () => {
const validRequest = {
sessionId: 'session-123',
suggestionId: 'suggestion-456',
};
const result = AiApplySuggestionRequestDto.safeParse(validRequest);
expect(result.success).toBe(true);
});
it('should fail if sessionId is missing', () => {
const invalidRequest = {
suggestionId: 'suggestion-456',
};
const result = AiApplySuggestionRequestDto.safeParse(invalidRequest);
expect(result.success).toBe(false);
expect(result.error?.issues[0].path).toEqual(['sessionId']);
});
it('should fail if suggestionId is missing', () => {
const invalidRequest = {
sessionId: 'session-123',
};
const result = AiApplySuggestionRequestDto.safeParse(invalidRequest);
expect(result.success).toBe(false);
expect(result.error?.issues[0].path).toEqual(['suggestionId']);
});
});

View file

@ -0,0 +1,252 @@
import { AiAskRequestDto } from '../ai-ask-request.dto';
describe('AiAskRequestDto', () => {
const validRequest = {
question: 'How can I improve this workflow?',
context: {
schema: [
{
nodeName: 'TestNode',
schema: {
type: 'string',
key: 'testKey',
value: 'testValue',
path: '/test/path',
},
},
],
inputSchema: {
nodeName: 'InputNode',
schema: {
type: 'object',
key: 'inputKey',
value: [
{
type: 'string',
key: 'nestedKey',
value: 'nestedValue',
path: '/nested/path',
},
],
path: '/input/path',
},
},
pushRef: 'push-123',
ndvPushRef: 'ndv-push-456',
},
forNode: 'TestWorkflowNode',
};
it('should validate a valid AI ask request', () => {
const result = AiAskRequestDto.safeParse(validRequest);
expect(result.success).toBe(true);
});
it('should fail if question is missing', () => {
const invalidRequest = {
...validRequest,
question: undefined,
};
const result = AiAskRequestDto.safeParse(invalidRequest);
expect(result.success).toBe(false);
expect(result.error?.issues[0].path).toEqual(['question']);
});
it('should fail if context is invalid', () => {
const invalidRequest = {
...validRequest,
context: {
...validRequest.context,
schema: [
{
nodeName: 'TestNode',
schema: {
type: 'invalid-type', // Invalid type
value: 'testValue',
path: '/test/path',
},
},
],
},
};
const result = AiAskRequestDto.safeParse(invalidRequest);
expect(result.success).toBe(false);
});
it('should fail if forNode is missing', () => {
const invalidRequest = {
...validRequest,
forNode: undefined,
};
const result = AiAskRequestDto.safeParse(invalidRequest);
expect(result.success).toBe(false);
expect(result.error?.issues[0].path).toEqual(['forNode']);
});
it('should validate all possible schema types', () => {
const allTypesRequest = {
question: 'Test all possible types',
context: {
schema: [
{
nodeName: 'AllTypesNode',
schema: {
type: 'object',
key: 'typesRoot',
value: [
{ type: 'string', key: 'stringType', value: 'string', path: '/types/string' },
{ type: 'number', key: 'numberType', value: 'number', path: '/types/number' },
{ type: 'boolean', key: 'booleanType', value: 'boolean', path: '/types/boolean' },
{ type: 'bigint', key: 'bigintType', value: 'bigint', path: '/types/bigint' },
{ type: 'symbol', key: 'symbolType', value: 'symbol', path: '/types/symbol' },
{ type: 'array', key: 'arrayType', value: [], path: '/types/array' },
{ type: 'object', key: 'objectType', value: [], path: '/types/object' },
{
type: 'function',
key: 'functionType',
value: 'function',
path: '/types/function',
},
{ type: 'null', key: 'nullType', value: 'null', path: '/types/null' },
{
type: 'undefined',
key: 'undefinedType',
value: 'undefined',
path: '/types/undefined',
},
],
path: '/types/root',
},
},
],
inputSchema: {
nodeName: 'InputNode',
schema: {
type: 'object',
key: 'simpleInput',
value: [
{
type: 'string',
key: 'simpleKey',
value: 'simpleValue',
path: '/simple/path',
},
],
path: '/simple/input/path',
},
},
pushRef: 'push-types-123',
ndvPushRef: 'ndv-push-types-456',
},
forNode: 'TypeCheckNode',
};
const result = AiAskRequestDto.safeParse(allTypesRequest);
expect(result.success).toBe(true);
});
it('should fail with invalid type', () => {
const invalidTypeRequest = {
question: 'Test invalid type',
context: {
schema: [
{
nodeName: 'InvalidTypeNode',
schema: {
type: 'invalid-type', // This should fail
key: 'invalidKey',
value: 'invalidValue',
path: '/invalid/path',
},
},
],
inputSchema: {
nodeName: 'InputNode',
schema: {
type: 'object',
key: 'simpleInput',
value: [
{
type: 'string',
key: 'simpleKey',
value: 'simpleValue',
path: '/simple/path',
},
],
path: '/simple/input/path',
},
},
pushRef: 'push-invalid-123',
ndvPushRef: 'ndv-push-invalid-456',
},
forNode: 'InvalidTypeNode',
};
const result = AiAskRequestDto.safeParse(invalidTypeRequest);
expect(result.success).toBe(false);
});
it('should validate multiple schema entries', () => {
const multiSchemaRequest = {
question: 'Multiple schema test',
context: {
schema: [
{
nodeName: 'FirstNode',
schema: {
type: 'string',
key: 'firstKey',
value: 'firstValue',
path: '/first/path',
},
},
{
nodeName: 'SecondNode',
schema: {
type: 'object',
key: 'secondKey',
value: [
{
type: 'number',
key: 'nestedKey',
value: 'nestedValue',
path: '/second/nested/path',
},
],
path: '/second/path',
},
},
],
inputSchema: {
nodeName: 'InputNode',
schema: {
type: 'object',
key: 'simpleInput',
value: [
{
type: 'string',
key: 'simpleKey',
value: 'simpleValue',
path: '/simple/path',
},
],
path: '/simple/input/path',
},
},
pushRef: 'push-multi-123',
ndvPushRef: 'ndv-push-multi-456',
},
forNode: 'MultiSchemaNode',
};
const result = AiAskRequestDto.safeParse(multiSchemaRequest);
expect(result.success).toBe(true);
});
});

View file

@ -0,0 +1,34 @@
import { AiChatRequestDto } from '../ai-chat-request.dto';
describe('AiChatRequestDto', () => {
it('should validate a request with a payload and session ID', () => {
const validRequest = {
payload: { someKey: 'someValue' },
sessionId: 'session-123',
};
const result = AiChatRequestDto.safeParse(validRequest);
expect(result.success).toBe(true);
});
it('should validate a request with only a payload', () => {
const validRequest = {
payload: { complexObject: { nested: 'value' } },
};
const result = AiChatRequestDto.safeParse(validRequest);
expect(result.success).toBe(true);
});
it('should fail if payload is missing', () => {
const invalidRequest = {
sessionId: 'session-123',
};
const result = AiChatRequestDto.safeParse(invalidRequest);
expect(result.success).toBe(false);
});
});

View file

@ -0,0 +1,32 @@
import { nanoId } from 'minifaker';
import { AiFreeCreditsRequestDto } from '../ai-free-credits-request.dto';
import 'minifaker/locales/en';
describe('AiChatRequestDto', () => {
it('should succeed if projectId is a valid nanoid', () => {
const validRequest = {
projectId: nanoId.nanoid(),
};
const result = AiFreeCreditsRequestDto.safeParse(validRequest);
expect(result.success).toBe(true);
});
it('should succeed if no projectId is sent', () => {
const result = AiFreeCreditsRequestDto.safeParse({});
expect(result.success).toBe(true);
});
it('should fail is projectId invalid value', () => {
const validRequest = {
projectId: '',
};
const result = AiFreeCreditsRequestDto.safeParse(validRequest);
expect(result.success).toBe(false);
});
});

View file

@ -0,0 +1,7 @@
import { z } from 'zod';
import { Z } from 'zod-class';
export class AiApplySuggestionRequestDto extends Z.class({
sessionId: z.string(),
suggestionId: z.string(),
}) {}

View file

@ -0,0 +1,53 @@
import type { AiAssistantSDK, SchemaType } from '@n8n_io/ai-assistant-sdk';
import { z } from 'zod';
import { Z } from 'zod-class';
// Note: This is copied from the sdk, since this type is not exported
type Schema = {
type: SchemaType;
key?: string;
value: string | Schema[];
path: string;
};
// Create a lazy validator to handle the recursive type
const schemaValidator: z.ZodType<Schema> = z.lazy(() =>
z.object({
type: z.enum([
'string',
'number',
'boolean',
'bigint',
'symbol',
'array',
'object',
'function',
'null',
'undefined',
]),
key: z.string().optional(),
value: z.union([z.string(), z.lazy(() => schemaValidator.array())]),
path: z.string(),
}),
);
export class AiAskRequestDto
extends Z.class({
question: z.string(),
context: z.object({
schema: z.array(
z.object({
nodeName: z.string(),
schema: schemaValidator,
}),
),
inputSchema: z.object({
nodeName: z.string(),
schema: schemaValidator,
}),
pushRef: z.string(),
ndvPushRef: z.string(),
}),
forNode: z.string(),
})
implements AiAssistantSDK.AskAiRequestPayload {}

View file

@ -0,0 +1,10 @@
import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk';
import { z } from 'zod';
import { Z } from 'zod-class';
export class AiChatRequestDto
extends Z.class({
payload: z.object({}).passthrough(), // Allow any object shape
sessionId: z.string().optional(),
})
implements AiAssistantSDK.ChatRequestPayload {}

View file

@ -0,0 +1,6 @@
import { z } from 'zod';
import { Z } from 'zod-class';
export class AiFreeCreditsRequestDto extends Z.class({
projectId: z.string().min(1).optional(),
}) {}

View file

@ -0,0 +1,93 @@
import { LoginRequestDto } from '../login-request.dto';
describe('LoginRequestDto', () => {
describe('Valid requests', () => {
test.each([
{
name: 'complete valid login request',
request: {
email: 'test@example.com',
password: 'securePassword123',
mfaCode: '123456',
},
},
{
name: 'login request without optional MFA',
request: {
email: 'test@example.com',
password: 'securePassword123',
},
},
{
name: 'login request with both mfaCode and mfaRecoveryCode',
request: {
email: 'test@example.com',
password: 'securePassword123',
mfaCode: '123456',
mfaRecoveryCode: 'recovery-code-123',
},
},
{
name: 'login request with only mfaRecoveryCode',
request: {
email: 'test@example.com',
password: 'securePassword123',
mfaRecoveryCode: 'recovery-code-123',
},
},
])('should validate $name', ({ request }) => {
const result = LoginRequestDto.safeParse(request);
expect(result.success).toBe(true);
});
});
describe('Invalid requests', () => {
test.each([
{
name: 'invalid email',
request: {
email: 'invalid-email',
password: 'securePassword123',
},
expectedErrorPath: ['email'],
},
{
name: 'empty password',
request: {
email: 'test@example.com',
password: '',
},
expectedErrorPath: ['password'],
},
{
name: 'missing email',
request: {
password: 'securePassword123',
},
expectedErrorPath: ['email'],
},
{
name: 'missing password',
request: {
email: 'test@example.com',
},
expectedErrorPath: ['password'],
},
{
name: 'whitespace in email and password',
request: {
email: ' test@example.com ',
password: ' securePassword123 ',
},
expectedErrorPath: ['email'],
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = LoginRequestDto.safeParse(request);
expect(result.success).toBe(false);
if (expectedErrorPath) {
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
}
});
});
});

View file

@ -0,0 +1,87 @@
import { ResolveSignupTokenQueryDto } from '../resolve-signup-token-query.dto';
describe('ResolveSignupTokenQueryDto', () => {
const validUuid = '123e4567-e89b-12d3-a456-426614174000';
describe('Valid requests', () => {
test.each([
{
name: 'standard UUID',
request: {
inviterId: validUuid,
inviteeId: validUuid,
},
},
])('should validate $name', ({ request }) => {
const result = ResolveSignupTokenQueryDto.safeParse(request);
expect(result.success).toBe(true);
});
});
describe('Invalid requests', () => {
test.each([
{
name: 'invalid inviterId UUID',
request: {
inviterId: 'not-a-valid-uuid',
inviteeId: validUuid,
},
expectedErrorPath: ['inviterId'],
},
{
name: 'invalid inviteeId UUID',
request: {
inviterId: validUuid,
inviteeId: 'not-a-valid-uuid',
},
expectedErrorPath: ['inviteeId'],
},
{
name: 'missing inviterId',
request: {
inviteeId: validUuid,
},
expectedErrorPath: ['inviterId'],
},
{
name: 'missing inviteeId',
request: {
inviterId: validUuid,
},
expectedErrorPath: ['inviteeId'],
},
{
name: 'UUID with invalid characters',
request: {
inviterId: '123e4567-e89b-12d3-a456-42661417400G',
inviteeId: validUuid,
},
expectedErrorPath: ['inviterId'],
},
{
name: 'UUID too long',
request: {
inviterId: '123e4567-e89b-12d3-a456-426614174001234',
inviteeId: validUuid,
},
expectedErrorPath: ['inviterId'],
},
{
name: 'UUID too short',
request: {
inviterId: '123e4567-e89b-12d3-a456',
inviteeId: validUuid,
},
expectedErrorPath: ['inviterId'],
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = ResolveSignupTokenQueryDto.safeParse(request);
expect(result.success).toBe(false);
if (expectedErrorPath) {
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
}
});
});
});

Some files were not shown because too many files have changed in this diff Show more