Merge branch 'master' of github.com:n8n-io/n8n into invoiceninja-add-more-query-params

This commit is contained in:
Jonathan Bennetts 2024-08-01 10:36:25 +01:00
commit 8f2d1f3e4d
No known key found for this signature in database
3223 changed files with 136327 additions and 84630 deletions

7
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM n8nio/base:20
RUN apk add --no-cache --update openssh sudo shadow bash
RUN echo node ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/node && chmod 0440 /etc/sudoers.d/node
RUN mkdir /workspaces && chown node:node /workspaces
USER node
RUN mkdir -p ~/.pnpm-store && pnpm config set store-dir ~/.pnpm-store --global

View file

@ -0,0 +1,19 @@
{
"name": "n8n",
"dockerComposeFile": "docker-compose.yml",
"service": "n8n",
"workspaceFolder": "/workspaces",
"mounts": [
"type=bind,source=${localWorkspaceFolder},target=/workspaces,consistency=cached",
"type=bind,source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,consistency=cached",
"type=bind,source=${localEnv:HOME}/.n8n,target=/home/node/.n8n,consistency=cached"
],
"forwardPorts": [8080, 5678],
"postCreateCommand": "corepack prepare --activate && pnpm install ",
"postAttachCommand": "pnpm build",
"customizations": {
"codespaces": {
"openFiles": ["CONTRIBUTING.md"]
}
}
}

View file

@ -0,0 +1,24 @@
volumes:
postgres-data:
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=n8n
- POSTGRES_PASSWORD=password
n8n:
build:
context: .
dockerfile: Dockerfile
volumes:
- ..:/workspaces:cached
command: sleep infinity
environment:
DB_POSTGRESDB_HOST: postgres
DB_TYPE: postgresdb
DB_POSTGRESDB_PASSWORD: password

View file

@ -10,6 +10,7 @@ packages/**/.turbo
packages/**/*.test.*
.git
.github
!.github/scripts
*.tsbuildinfo
packages/cli/dist/**/e2e.*
docker/compose

View file

@ -19,7 +19,7 @@ services:
restart: always
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=root
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
ports:
- 5432:5432

View file

@ -1,16 +1,26 @@
## Summary
> Describe what the PR does and how to test. Photos and videos are recommended.
<!--
Describe what the PR does and how to test.
Photos and videos are recommended.
-->
## Related Linear tickets, Github issues, and Community forum posts
## Related tickets and issues
> Include links to **Linear ticket** or Github issue or Community forum post. Important in order to close *automatically* and provide context to reviewers.
<!--
Include links to **Linear ticket** or Github issue or Community forum post.
Important in order to close *automatically* and provide context to reviewers.
-->
## Review / Merge checklist
- [ ] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md))
- [ ] PR title and summary are descriptive. ([conventions](../blob/master/.github/pull_request_title_conventions.md)) <!--
**Remember, the title automatically goes into the changelog.
Use `(no-changelog)` otherwise.**
-->
- [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created.
- [ ] Tests included.
> A bug is not considered fixed, unless a test is added to prevent it from happening again.
> A feature is not complete without tests.
- [ ] Tests included. <!--
A bug is not considered fixed, unless a test is added to prevent it from happening again.
A feature is not complete without tests.
-->
- [ ] PR Labeled with `release/backport` (if the PR is an urgent fix that needs to be backported)

View file

@ -37,7 +37,7 @@ Must be one of the following:
- `test` - Adding missing tests or correcting existing tests
- `docs` - Documentation only changes
- `refactor` - A code change that neither fixes a bug nor adds a feature
- `build` - Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
- `build` - Changes that affect the build system or external dependencies (example scopes: broccoli, npm)
- `ci` - Changes to our CI configuration files and scripts (e.g. Github actions)
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any BREAKING CHANGE (see Footer section below), the commit will always appear in the changelog.

View file

@ -0,0 +1,44 @@
import { writeFile, readFile, copyFile } from 'fs/promises';
import { resolve, dirname } from 'path';
import child_process from 'child_process';
import { fileURLToPath } from 'url';
import { promisify } from 'util';
const exec = promisify(child_process.exec);
const commonFiles = ['LICENSE.md', 'LICENSE_EE.md'];
const baseDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const packages = JSON.parse((await exec('pnpm ls -r --only-projects --json')).stdout);
for (let { name, path, version, private: isPrivate } of packages) {
if (isPrivate) continue;
const packageFile = resolve(path, 'package.json');
const packageJson = {
...JSON.parse(await readFile(packageFile, 'utf-8')),
// Add these fields to all published package.json files to ensure provenance checks pass
license: 'SEE LICENSE IN LICENSE.md',
homepage: 'https://n8n.io',
author: {
name: 'Jan Oberhauser',
email: 'jan@n8n.io',
},
repository: {
type: 'git',
url: 'git+https://github.com/n8n-io/n8n.git',
},
};
// Copy over LICENSE.md and LICENSE_EE.md into every published package, and ensure they get included in the published package
await Promise.all(
commonFiles.map(async (file) => {
await copyFile(resolve(baseDir, file), resolve(path, file));
if (packageJson.files && !packageJson.files.includes(file)) {
packageJson.files.push(file);
}
}),
);
await writeFile(packageFile, JSON.stringify(packageJson, null, 2) + '\n');
}

View file

@ -1,12 +1,15 @@
const { writeFileSync } = require('fs');
const { resolve } = require('path');
const baseDir = resolve(__dirname, '..');
const baseDir = resolve(__dirname, '../..');
const trimPackageJson = (packageName) => {
const filePath = resolve(baseDir, 'packages', packageName, 'package.json');
const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require(
filePath,
);
if (packageName === '@n8n/chat') {
packageJson.dependencies = dependencies;
}
writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
};

View file

@ -17,9 +17,9 @@ jobs:
- uses: actions/checkout@v4.1.1
- run: corepack enable
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies

View file

@ -19,9 +19,9 @@ jobs:
uses: actions/checkout@v4.1.1
- run: corepack enable
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies

View file

@ -20,9 +20,9 @@ jobs:
fetch-depth: 0
- name: Use Node.js
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
node-version: 20.x
- run: npm install --prefix=.github/scripts --no-package-lock

View file

@ -1,26 +1,59 @@
name: Chromatic
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
pull_request_review:
types: [submitted]
branch:
- 'master'
paths:
- packages/design-system/**
- .github/workflows/chromatic.yml
concurrency:
group: chromatic-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
chromatic:
if: ${{ github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'community') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
node-version: 20.x
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Publish to Chromatic
uses: chromaui/action@latest
uses: chromaui/action@v11
id: chromatic_tests
continue-on-error: true
with:
workingDir: packages/design-system
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: false
- name: Success comment
if: steps.chromatic_tests.outcome == 'success'
uses: peter-evans/create-or-update-comment@v4.0.0
with:
issue-number: ${{ github.event.pull_request.number }}
token: ${{ secrets.GITHUB_TOKEN }}
edit-mode: replace
body: |
:white_check_mark: No visual regressions found.
- name: Fail comment
if: steps.chromatic_tests.outcome != 'success'
uses: peter-evans/create-or-update-comment@v4.0.0
with:
issue-number: ${{ github.event.pull_request.number }}
token: ${{ secrets.GITHUB_TOKEN }}
edit-mode: replace
body: |
[:warning: Visual regressions found](${{steps.chromatic_tests.outputs.url}}): ${{steps.chromatic_tests.outputs.changeCount}}

View file

@ -9,25 +9,23 @@ jobs:
install-and-build:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
node-version: [18.x, 20.x]
timeout-minutes: 10
steps:
- uses: actions/checkout@v4.1.1
- run: corepack enable
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: ${{ matrix.node-version }}
node-version: 20.x
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build
run: pnpm build
@ -35,7 +33,7 @@ jobs:
uses: actions/cache/save@v4.0.0
with:
path: ./packages/**/dist
key: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint
key: ${{ github.sha }}-base:build
unit-test:
name: Unit tests
@ -43,46 +41,22 @@ jobs:
needs: install-and-build
strategy:
matrix:
node-version: [18.x, 20.x]
node-version: [18.x, 20.x, 22.4]
with:
ref: ${{ inputs.branch }}
nodeVersion: ${{ matrix.node-version }}
cacheKey: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint
collectCoverage: true
cacheKey: ${{ github.sha }}-base:build
collectCoverage: ${{ matrix.node-version == '20.x' }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
lint:
name: Lint changes
runs-on: ubuntu-latest
name: Lint
uses: ./.github/workflows/linting-reusable.yml
needs: install-and-build
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v4.1.1
with:
repository: n8n-io/n8n
ref: ${{ inputs.branch }}
- run: corepack enable
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Restore cached build artifacts
uses: actions/cache/restore@v4.0.0
with:
path: ./packages/**/dist
key: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint
- name: Lint
env:
CI_LINT_MASTER: true
run: pnpm lint
with:
ref: ${{ inputs.branch }}
cacheKey: ${{ github.sha }}-base:build
notify-on-failure:
name: Notify Slack on failure

View file

@ -7,6 +7,7 @@ on:
pull_request:
paths:
- packages/cli/src/databases/**
- .github/workflows/ci-postgres-mysql.yml
concurrency:
group: db-${{ github.event.pull_request.number || github.ref }}
@ -19,12 +20,15 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- run: corepack enable
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
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
@ -45,12 +49,15 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- run: corepack enable
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
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:
@ -59,7 +66,7 @@ jobs:
- name: Test SQLite Pooled
working-directory: packages/cli
run: pnpm jest --coverage
run: pnpm jest
mysql:
name: MySQL
@ -71,12 +78,15 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- run: corepack enable
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
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:
@ -92,7 +102,7 @@ jobs:
- name: Test MySQL
working-directory: packages/cli
run: pnpm test:mysql
run: pnpm test:mysql --testTimeout 20000
postgres:
name: Postgres
@ -101,15 +111,19 @@ jobs:
timeout-minutes: 20
env:
DB_POSTGRESDB_PASSWORD: password
DB_POSTGRESDB_POOL_SIZE: 1 # Detect connection pooling deadlocks
steps:
- uses: actions/checkout@v4.1.1
- run: corepack enable
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
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:

View file

@ -3,7 +3,7 @@ name: Build, unit test and lint branch
on: [pull_request]
jobs:
install:
install-and-build:
name: Install & Build
runs-on: ubuntu-latest
steps:
@ -13,57 +13,41 @@ jobs:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- run: corepack enable
- name: Use Node.js 18
uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
node-version: 20.x
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build
run: pnpm build
- name: Run typecheck
run: pnpm typecheck
- name: Cache build artifacts
uses: actions/cache/save@v4.0.0
with:
path: ./packages/**/dist
key: ${{ github.sha }}-base:18-test-lint
key: ${{ github.sha }}-base:build
unit-test:
name: Unit tests
uses: ./.github/workflows/units-tests-reusable.yml
needs: install
needs: install-and-build
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
cacheKey: ${{ github.sha }}-base:18-test-lint
cacheKey: ${{ github.sha }}-base:build
lint:
name: Lint changes
runs-on: ubuntu-latest
needs: install
steps:
- uses: actions/checkout@v4.1.1
with:
repository: n8n-io/n8n
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- run: corepack enable
- name: Use Node.js 18
uses: actions/setup-node@v4.0.1
with:
node-version: 18.x
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Restore cached build artifacts
uses: actions/cache/restore@v4.0.0
with:
path: ./packages/**/dist
key: ${{ github.sha }}-base:18-test-lint
- name: Lint
run: pnpm lint
name: Lint
uses: ./.github/workflows/linting-reusable.yml
needs: install-and-build
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
cacheKey: ${{ github.sha }}-base:build

View file

@ -7,10 +7,11 @@ on:
description: 'Node.js version to build this image with.'
type: choice
required: true
default: '18'
default: '20'
options:
- '18'
- '20'
- '22'
jobs:
build:

View file

@ -40,7 +40,7 @@ on:
containers:
description: 'Number of containers to run tests in.'
required: false
default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]'
default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]'
type: string
pr_number:
description: 'PR number to run tests for.'
@ -87,7 +87,7 @@ jobs:
git fetch origin pull/${{ inputs.pr_number }}/head
git checkout FETCH_HEAD
- uses: pnpm/action-setup@v2.4.0
- uses: pnpm/action-setup@v4.0.0
- name: Install dependencies
run: pnpm install --frozen-lockfile
@ -99,10 +99,9 @@ jobs:
runTests: false
install: false
build: pnpm build
env:
VUE_APP_MAX_PINNED_DATA_SIZE: 16384
- name: Cypress install
working-directory: cypress
run: pnpm cypress:install
- name: Cache build artifacts
@ -138,7 +137,7 @@ jobs:
git fetch origin pull/${{ inputs.pr_number }}/head
git checkout FETCH_HEAD
- uses: pnpm/action-setup@v2.4.0
- uses: pnpm/action-setup@v4.0.0
- name: Restore cached pnpm modules
uses: actions/cache/restore@v4.0.0
@ -155,6 +154,7 @@ jobs:
- name: Cypress run
uses: cypress-io/github-action@v6.6.1
with:
working-directory: cypress
install: false
start: pnpm start
wait-on: 'http://localhost:5678'
@ -164,8 +164,7 @@ jobs:
# We have to provide custom ci-build-id key to make sure that this workflow could be run multiple times
# in the same parent workflow
ci-build-id: ${{ needs.prepare.outputs.uuid }}
spec: '/__w/n8n/n8n/cypress/${{ inputs.spec }}'
config-file: /__w/n8n/n8n/cypress.config.js
spec: '${{ inputs.spec }}'
env:
NODE_OPTIONS: --dns-result-order=ipv4first
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

57
.github/workflows/linting-reusable.yml vendored Normal file
View file

@ -0,0 +1,57 @@
name: Reusable linting workflow
on:
workflow_call:
inputs:
ref:
description: GitHub ref to lint.
required: false
type: string
default: master
cacheKey:
description: Cache key for modules and build artifacts.
required: false
default: ''
type: string
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
with:
repository: n8n-io/n8n
ref: ${{ inputs.ref }}
- run: corepack enable
- uses: actions/setup-node@v4.0.2
with:
node-version: 20.x
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build
if: ${{ inputs.cacheKey == '' }}
run: pnpm build
- name: Restore cached build artifacts
if: ${{ inputs.cacheKey != '' }}
uses: actions/cache/restore@v4.0.0
with:
path: ./packages/**/dist
key: ${{ inputs.cacheKey }}
- name: Lint Backend
run: pnpm lint:backend
- name: Lint Nodes
run: pnpm lint:nodes
- name: Lint Frontend
run: pnpm lint:frontend

27
.github/workflows/notify-pr-status.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Notify PR status changed
on:
pull_request_review:
types: [submitted, dismissed]
pull_request:
types: [closed]
jobs:
notify:
runs-on: ubuntu-latest
if: >-
(github.event_name == 'pull_request_review' && github.event.review.state == 'approved') ||
(github.event_name == 'pull_request_review' && github.event.review.state == 'dismissed') ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true) ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == false && github.event.action == 'closed')
steps:
- uses: fjogeleit/http-request-action@dea46570591713c7de04a5b556bf2ff7bdf0aa9c # v1
if: ${{!contains(github.event.pull_request.labels.*.name, 'community')}}
name: Notify
env:
PR_URL: ${{ github.event.pull_request.html_url }}
with:
url: ${{ secrets.N8N_NOTIFY_PR_STATUS_CHANGED_URL }}
method: 'POST'
customHeaders: '{ "x-api-token": "${{ secrets.N8N_NOTIFY_PR_STATUS_CHANGED_TOKEN }}" }'
data: '{ "event_name": "${{ github.event_name }}", "pr_url": "${{ env.PR_URL }}", "event": ${{ toJSON(github.event) }} }'

View file

@ -36,9 +36,9 @@ jobs:
ref: ${{ github.event.inputs.base-branch }}
- run: corepack enable
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
node-version: 20.x
- run: npm install --prefix=.github/scripts --no-package-lock

View file

@ -14,8 +14,11 @@ jobs:
permissions:
contents: write
id-token: write
timeout-minutes: 60
env:
NPM_CONFIG_PROVENANCE: true
steps:
- name: Checkout
@ -24,9 +27,9 @@ jobs:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
node-version: 20.x
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
@ -42,7 +45,8 @@ jobs:
- name: Publish to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
node scripts/trim-fe-packageJson.js
node .github/scripts/trim-fe-packageJson.js
node .github/scripts/ensure-provenance-fields.mjs
sed -i "s/default: 'dev'/default: 'stable'/g" packages/cli/dist/config/schema.js
pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks
npm dist-tag rm n8n rc

View file

@ -22,9 +22,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
node-version: 20.x
- run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm dist-tag add n8n@${{ github.event.inputs.version }} ${{ github.event.inputs.release-channel }}
@ -53,3 +53,11 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- run: docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.release-channel }} ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }}
update-docs:
name: Update latest and next in the docs
runs-on: ubuntu-latest
needs: [release-to-npm, release-to-docker-hub]
steps:
- continue-on-error: true
run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/update-latest-next'

View file

@ -26,9 +26,9 @@ jobs:
- run: corepack enable
working-directory: n8n
- uses: actions/setup-node@v4.0.1
- uses: actions/setup-node@v4.0.2
with:
node-version: 18.x
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'n8n/pnpm-lock.yaml'

View file

@ -4,24 +4,28 @@ on:
workflow_call:
inputs:
ref:
description: 'GitHub ref to test.'
description: GitHub ref to test.
required: false
type: string
default: 'master'
default: master
nodeVersion:
description: 'Version of node to use.'
description: Version of node to use.
required: false
type: string
default: '18.x'
default: 20.x
cacheKey:
description: 'Cache key for modules and build artifacts.'
description: Cache key for modules and build artifacts.
required: false
default: ''
type: string
collectCoverage:
required: false
default: 'false'
type: string
default: false
type: boolean
secrets:
CODECOV_TOKEN:
description: 'Codecov upload token.'
required: false
jobs:
unit-test:
@ -37,7 +41,7 @@ jobs:
- run: corepack enable
- name: Use Node.js ${{ inputs.nodeVersion }}
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version: ${{ inputs.nodeVersion }}
cache: pnpm
@ -45,6 +49,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build
if: ${{ inputs.cacheKey == '' }}
run: pnpm build
@ -66,7 +73,7 @@ jobs:
run: pnpm test:frontend
- name: Upload coverage to Codecov
if: ${{ inputs.collectCoverage == 'true' }}
uses: codecov/codecov-action@v3
if: inputs.collectCoverage
uses: codecov/codecov-action@v4.5.0
with:
files: packages/@n8n/chat/coverage/cobertura-coverage.xml,packages/@n8n/nodes-langchain/coverage/cobertura-coverage.xml,packages/@n8n/permissions/coverage/cobertura-coverage.xml,packages/@n8n/client-oauth2/coverage/cobertura-coverage.xml,packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}

4
.gitignore vendored
View file

@ -16,10 +16,8 @@ _START_PACKAGE
nodelinter.config.json
**/package-lock.json
packages/**/.turbo
.turbo
*.tsbuildinfo
cypress/videos/*
cypress/screenshots/*
cypress/downloads/*
*.swp
CHANGELOG-*.md
*.mdx

4
.npmrc
View file

@ -7,4 +7,8 @@ prefer-workspace-packages = true
link-workspace-packages = deep
hoist = true
shamefully-hoist = true
hoist-workspace-packages = false
loglevel = warn
package-manager-strict=false
# https://github.com/pnpm/pnpm/issues/7024
package-import-method=clone-or-copy

View file

@ -1,3 +1,532 @@
# [1.53.0](https://github.com/n8n-io/n8n/compare/n8n@1.52.0...n8n@1.53.0) (2024-07-31)
### Bug Fixes
* Better error message when calling data transformation functions on a null value ([#10210](https://github.com/n8n-io/n8n/issues/10210)) ([1718125](https://github.com/n8n-io/n8n/commit/1718125c6d8589cf24dc8d34f6808dd6f1802691))
* **core:** Fix missing successful items on continueErrorOutput with multiple outputs ([#10218](https://github.com/n8n-io/n8n/issues/10218)) ([1a7713e](https://github.com/n8n-io/n8n/commit/1a7713ef263680da43f08b6c8a15aee7a0341493))
* **core:** Flush instance stopped event immediately ([#10238](https://github.com/n8n-io/n8n/issues/10238)) ([d6770b5](https://github.com/n8n-io/n8n/commit/d6770b5fcaec6438d677b918aaeb1669ad7424c2))
* **core:** Restore log event `n8n.workflow.failed` ([#10253](https://github.com/n8n-io/n8n/issues/10253)) ([3e96b29](https://github.com/n8n-io/n8n/commit/3e96b293329525c9d4b2fcef87b3803e458c8e7f))
* **core:** Upgrade @n8n/vm2 to address CVE202337466 ([#10265](https://github.com/n8n-io/n8n/issues/10265)) ([2a09a03](https://github.com/n8n-io/n8n/commit/2a09a036d2e916acff7ee50904f1d011a93758e1))
* **editor:** Defer `User saved credentials` telemetry event for OAuth credentials ([#10215](https://github.com/n8n-io/n8n/issues/10215)) ([40a5226](https://github.com/n8n-io/n8n/commit/40a5226e24448a4428143e69d80ebc78238365a1))
* **editor:** Fix custom API call notice ([#10227](https://github.com/n8n-io/n8n/issues/10227)) ([5b47c8b](https://github.com/n8n-io/n8n/commit/5b47c8b57b25528cd2d6f97bc6d98707d47f35bc))
* **editor:** Fix issue with existing credential not opening in HTTP agent tool ([#10167](https://github.com/n8n-io/n8n/issues/10167)) ([906b4c3](https://github.com/n8n-io/n8n/commit/906b4c3c7b2919111cf23eaa12b3c4d507969179))
* **editor:** Fix parameter input glitch when there was an error loading remote options ([#10209](https://github.com/n8n-io/n8n/issues/10209)) ([c0e3743](https://github.com/n8n-io/n8n/commit/c0e37439a87105a0e66c8ebced42c06dab30dc5e))
* **editor:** Fix workflow execution list scrolling after filter change ([#10226](https://github.com/n8n-io/n8n/issues/10226)) ([7e64358](https://github.com/n8n-io/n8n/commit/7e643589c67adc0218216ec4b89a95f0edfedbee))
* **Google BigQuery Node:** Send timeoutMs in query, pagination support ([#10205](https://github.com/n8n-io/n8n/issues/10205)) ([f5722e8](https://github.com/n8n-io/n8n/commit/f5722e8823ccd2bc2b5f43ba3c849797d5690a93))
* **Google Sheets Node:** Add column names row if sheet is empty ([#10200](https://github.com/n8n-io/n8n/issues/10200)) ([82eba9f](https://github.com/n8n-io/n8n/commit/82eba9fc5ff49b8e2a9db93c10b253fb67a8c644))
* **Google Sheets Node:** Do not insert row_number as a new column, do not checkForSchemaChanges in update operation ([#10201](https://github.com/n8n-io/n8n/issues/10201)) ([5136d10](https://github.com/n8n-io/n8n/commit/5136d10ca3492f92af67d4a1d4abc774419580cc))
* **Google Sheets Node:** Fix Google Sheet URL regex ([#10195](https://github.com/n8n-io/n8n/issues/10195)) ([e6fd996](https://github.com/n8n-io/n8n/commit/e6fd996973d4f40facf0ebf1eea3cc26acd0603d))
* **HTTP Request Node:** Resolve max pages expression ([#10192](https://github.com/n8n-io/n8n/issues/10192)) ([bfc8e1b](https://github.com/n8n-io/n8n/commit/bfc8e1b56f7714e1f52aae747d58d686b86e60f0))
* **LinkedIn Node:** Fix issue with some characters cutting off posts early ([#10185](https://github.com/n8n-io/n8n/issues/10185)) ([361b5e7](https://github.com/n8n-io/n8n/commit/361b5e7c37ba49b68dcf5b8122621aad4d8d96e0))
* **Postgres Node:** Expressions in query parameters for Postgres executeQuery operation ([#10217](https://github.com/n8n-io/n8n/issues/10217)) ([519fc4d](https://github.com/n8n-io/n8n/commit/519fc4d75325a80b84cc4dcacf52d6f4c02e3a44))
* **Postgres Node:** Option to treat query parameters enclosed in single quotas as text ([#10214](https://github.com/n8n-io/n8n/issues/10214)) ([00ec253](https://github.com/n8n-io/n8n/commit/00ec2533374d3def465efee718592fc4001d5602))
* **Read/Write Files from Disk Node:** Notice update in file selector, replace backslashes with forward slashes if windows path ([#10186](https://github.com/n8n-io/n8n/issues/10186)) ([3eac673](https://github.com/n8n-io/n8n/commit/3eac673b17986c5c74bd2adb5ad589ba0ca55319))
* **Text Classifier Node:** Use proper documentation URL and respect continueOnFail ([#10216](https://github.com/n8n-io/n8n/issues/10216)) ([452f52c](https://github.com/n8n-io/n8n/commit/452f52c124017e002e86c547ba42b1633b14beed))
* **Trello Node:** Use body for POST requests ([#10189](https://github.com/n8n-io/n8n/issues/10189)) ([7775d50](https://github.com/n8n-io/n8n/commit/7775d5059b7f69d9af22e7ad7d12c6cf9092a4e5))
* **Wait Node:** Authentication fix ([#10236](https://github.com/n8n-io/n8n/issues/10236)) ([f87854f](https://github.com/n8n-io/n8n/commit/f87854f8db360b7b870583753fcfb4af95adab8c))
### Features
* **Calendly Trigger Node:** Add OAuth Credentials Support ([#10251](https://github.com/n8n-io/n8n/issues/10251)) ([326c983](https://github.com/n8n-io/n8n/commit/326c983915a2c382e32398358e7dcadd022c0b77))
* **core:** Allow filtering workflows by project and transferring workflows in Public API ([#10231](https://github.com/n8n-io/n8n/issues/10231)) ([d719899](https://github.com/n8n-io/n8n/commit/d719899223907b20a17883a35e4ef637a3453532))
* **editor:** Show new executions as `Queued` in the UI, until they actually start ([#10204](https://github.com/n8n-io/n8n/issues/10204)) ([44728d7](https://github.com/n8n-io/n8n/commit/44728d72423f5549dda09589f4a618ebd80899cb))
* **HTTP Request Node:** Add option to disable lowercase headers ([#10154](https://github.com/n8n-io/n8n/issues/10154)) ([5aba69b](https://github.com/n8n-io/n8n/commit/5aba69bcf4d232d9860f3cd9fe57cb8839a2f96f))
* **Information Extractor Node:** Add new simplified AI-node for information extraction ([#10149](https://github.com/n8n-io/n8n/issues/10149)) ([3d235b0](https://github.com/n8n-io/n8n/commit/3d235b0b2df756df35ac60e3dcd87ad183a07167))
* Introduce Google Cloud Platform as external secrets provider ([#10146](https://github.com/n8n-io/n8n/issues/10146)) ([3ccb9df](https://github.com/n8n-io/n8n/commit/3ccb9df2f902e46f8cbb9c46c0727f29d752a773))
* **n8n Form Trigger Node:** Improvements ([#10092](https://github.com/n8n-io/n8n/issues/10092)) ([711b667](https://github.com/n8n-io/n8n/commit/711b667ebefe55740e5eb39f1f0f24ceee10e7b0))
* Recovery option for jsonParse helper ([#10182](https://github.com/n8n-io/n8n/issues/10182)) ([d165b33](https://github.com/n8n-io/n8n/commit/d165b33ceac4d24d0fc290bffe63b5f551204e38))
* **Sentiment Analysis Node:** Implement Sentiment Analysis node ([#10184](https://github.com/n8n-io/n8n/issues/10184)) ([8ef0a0c](https://github.com/n8n-io/n8n/commit/8ef0a0c58ac2a84aad649ccbe72aa907d005cc44))
* **Shopify Node:** Update Shopify API version ([#10155](https://github.com/n8n-io/n8n/issues/10155)) ([e2ee915](https://github.com/n8n-io/n8n/commit/e2ee91569a382bfbf787cf45204c72c821a860a0))
* Support create, read, delete variables in Public API ([#10241](https://github.com/n8n-io/n8n/issues/10241)) ([af695eb](https://github.com/n8n-io/n8n/commit/af695ebf934526d926ea87fe87df61aa73d70979))
# [1.52.0](https://github.com/n8n-io/n8n/compare/n8n@1.51.0...n8n@1.52.0) (2024-07-24)
### Bug Fixes
* **core:** Fix handling of common events for relays ([#10135](https://github.com/n8n-io/n8n/issues/10135)) ([d2a3a4a](https://github.com/n8n-io/n8n/commit/d2a3a4a080cdcc04f50fa33fd81d361efce3f709))
* **core:** Fix SSH Tunnels when using private key ([#10148](https://github.com/n8n-io/n8n/issues/10148)) ([a96db34](https://github.com/n8n-io/n8n/commit/a96db344e54658787426d967dfa299c7a6dd14e7))
* **core:** Metadata inserts using existing IDs and failing with postgres ([#10108](https://github.com/n8n-io/n8n/issues/10108)) ([4547a49](https://github.com/n8n-io/n8n/commit/4547a49db15a20f5f147e859b6c2c01f60f9565c))
* **core:** Respect prefix for all Prometheus metrics ([#10130](https://github.com/n8n-io/n8n/issues/10130)) ([b1816db](https://github.com/n8n-io/n8n/commit/b1816db449ed451443f353b69166b7ca700ba51e))
* **core:** Support branches containing slashes in source control ([#10109](https://github.com/n8n-io/n8n/issues/10109)) ([03a833d](https://github.com/n8n-io/n8n/commit/03a833db51a25dda6cf0d8494f06c6704f6f3c7f))
* **core:** Support execution recovery when saving execution progress ([#10104](https://github.com/n8n-io/n8n/issues/10104)) ([d887c82](https://github.com/n8n-io/n8n/commit/d887c82d808a79babc726fc789cc014194ae2ac6))
* **editor:** Allow `$secrets` to resolve on credentials ([#10093](https://github.com/n8n-io/n8n/issues/10093)) ([bf57f38](https://github.com/n8n-io/n8n/commit/bf57f38d1c417ba8b20144934c8e97a75c1f51cc))
* **editor:** Fix saving and connecting on LDAP setup form ([#10163](https://github.com/n8n-io/n8n/issues/10163)) ([30784fb](https://github.com/n8n-io/n8n/commit/30784fb76cec790a782fae40973a956a8d81c0b2))
* **editor:** Fix updating/uninstalling community nodes ([#10138](https://github.com/n8n-io/n8n/issues/10138)) ([de015ff](https://github.com/n8n-io/n8n/commit/de015ff2978a5ee3345449626025c6d0793b6f5a))
* **editor:** Remove "move" action from workflow and credential on community plan ([#10057](https://github.com/n8n-io/n8n/issues/10057)) ([5a9a271](https://github.com/n8n-io/n8n/commit/5a9a2713b499cc7dcddb500a54e24bbf7145b504))
* **editor:** UX Improvements to RBAC feature set ([#9683](https://github.com/n8n-io/n8n/issues/9683)) ([028a8a2](https://github.com/n8n-io/n8n/commit/028a8a2c754e4f6d6a5f0918a656eb4554eb869f))
* **HelpScout Node:** Fix issue with thread types not working correctly ([#10084](https://github.com/n8n-io/n8n/issues/10084)) ([68d3beb](https://github.com/n8n-io/n8n/commit/68d3bebfeebea9054bbbaebac31c2e3fa34336bb))
* **MQTT Node:** Node hangs forever on failed connection ([#10048](https://github.com/n8n-io/n8n/issues/10048)) ([76c2906](https://github.com/n8n-io/n8n/commit/76c290655de7d4e626725a05fd991a0858cca0d7))
* **n8n Form Trigger Node:** Execution from canvas ([#10132](https://github.com/n8n-io/n8n/issues/10132)) ([b07c5e2](https://github.com/n8n-io/n8n/commit/b07c5e201165165c4e91ddd19b6fa79703ba2a9c))
* **Notion Node:** Fix issue preventing some database page urls from working ([#10070](https://github.com/n8n-io/n8n/issues/10070)) ([7848c19](https://github.com/n8n-io/n8n/commit/7848c19f543d5f5f62b89cc5644639c6afdb8fa6))
* **RabbitMQ Node:** Fix issue with arguments not being sent ([#9397](https://github.com/n8n-io/n8n/issues/9397)) ([1c666e6](https://github.com/n8n-io/n8n/commit/1c666e6e7c2be2e2d0dcc528870fddfa8b02318b))
### Features
* **editor:** Split Tools and Models into sub-sections ([#10159](https://github.com/n8n-io/n8n/issues/10159)) ([3846eb9](https://github.com/n8n-io/n8n/commit/3846eb967afd77dba6f037e8185ed94494454d5a))
* Introduce Azure Key Vault as external secrets provider ([#10054](https://github.com/n8n-io/n8n/issues/10054)) ([1b6c2d3](https://github.com/n8n-io/n8n/commit/1b6c2d3a37a78ed07ada93be2a57e4b7f7149e58))
* **Pinecone Vector Store Node, Supabase Vector Store Node:** Add update operation to vector store nodes ([#10060](https://github.com/n8n-io/n8n/issues/10060)) ([7e1eeb4](https://github.com/n8n-io/n8n/commit/7e1eeb4c31d3f25ec31baa7390b11a7e3280ce01))
* **Send Email Node:** Smtp credential improvements ([#10147](https://github.com/n8n-io/n8n/issues/10147)) ([dc13ceb](https://github.com/n8n-io/n8n/commit/dc13ceb41649eab42ef073247f3b52c040826e98))
# [1.51.0](https://github.com/n8n-io/n8n/compare/n8n@1.50.0...n8n@1.51.0) (2024-07-17)
### Bug Fixes
* **AMQP Sender Node:** Node hangs forever on disconnect ([#10026](https://github.com/n8n-io/n8n/issues/10026)) ([27410ab](https://github.com/n8n-io/n8n/commit/27410ab2af87573045f38e14e7e20bedd3b0365d))
* **AMQP Trigger Node:** Manual execution updated error reduced wait time ([#10035](https://github.com/n8n-io/n8n/issues/10035)) ([f78f4ea](https://github.com/n8n-io/n8n/commit/f78f4ea3492560bc7056023fd0276990f3ac9b00))
* **AWS Comprehend Node:** Add paired item support ([#10015](https://github.com/n8n-io/n8n/issues/10015)) ([470d496](https://github.com/n8n-io/n8n/commit/470d4966c67a3e4155d59e6fadab467b73134ec4))
* **core:** Ensure executions cannot resume if already running ([#10014](https://github.com/n8n-io/n8n/issues/10014)) ([d651be4](https://github.com/n8n-io/n8n/commit/d651be4e01a869a6f7d70e691e0f5e244f59490e))
* **core:** Redact `csrfSecret` when returning oauth credentials to the frontend ([#10075](https://github.com/n8n-io/n8n/issues/10075)) ([48f047e](https://github.com/n8n-io/n8n/commit/48f047ee2ecbfbd364151816df5fc21e09ca72a6))
* **core:** Stopping an execution should reject any response promises ([#9992](https://github.com/n8n-io/n8n/issues/9992)) ([36b314d](https://github.com/n8n-io/n8n/commit/36b314d0311ef84f275efbc20997c6a77db81b31))
* **editor:** Ensure all static assets are accessible from the server ([#10062](https://github.com/n8n-io/n8n/issues/10062)) ([3bde845](https://github.com/n8n-io/n8n/commit/3bde8453efa9a4d14404c63bdc061c87843d49d2))
* **editor:** Handle disabled nodes in schema view ([#10052](https://github.com/n8n-io/n8n/issues/10052)) ([ab5688c](https://github.com/n8n-io/n8n/commit/ab5688c582c05afd7d3e0967eda0f5dc73d6d3ed))
* **editor:** Make schema view use the correct output ([#10016](https://github.com/n8n-io/n8n/issues/10016)) ([c29664d](https://github.com/n8n-io/n8n/commit/c29664d68851ec33e4d810fa24aba72bb6cecc86))
* **editor:** Provide autocomplete for nodes, even when intermediate node has not run ([#10036](https://github.com/n8n-io/n8n/issues/10036)) ([46d6edc](https://github.com/n8n-io/n8n/commit/46d6edc2a4edd49ae58c0c60977809554e07f4ee))
* **editor:** Remove push event listeners when migrating away from the canvas ([#10063](https://github.com/n8n-io/n8n/issues/10063)) ([0d12f0a](https://github.com/n8n-io/n8n/commit/0d12f0a6b36aaaae5e1f9fab8ad73feeba9ec5ed))
* **editor:** Use selected input item for autocomplete ([#10019](https://github.com/n8n-io/n8n/issues/10019)) ([1d2b403](https://github.com/n8n-io/n8n/commit/1d2b403644278fa6158272edc4295d4565554e37))
* **Email Trigger (IMAP) Node:** Reconnect not working correctly ([#10064](https://github.com/n8n-io/n8n/issues/10064)) ([68d5d7e](https://github.com/n8n-io/n8n/commit/68d5d7e2e90ede5d021a12304dd665247dde5243))
* Filter component - array contains comparison not correct when ignore case option set to true ([#10012](https://github.com/n8n-io/n8n/issues/10012)) ([4a3b97c](https://github.com/n8n-io/n8n/commit/4a3b97cede531adbf81274c1ec2ce4ee400cb48e))
* **GitHub Node:** File Create operation prevent duplicated base64 encoding ([#10040](https://github.com/n8n-io/n8n/issues/10040)) ([9bcc926](https://github.com/n8n-io/n8n/commit/9bcc926a91d7afab0c2ef6eb57e818ef79e3a8f7))
* **HTTP Request Node:** Respect the original encoding of the incoming response ([#9869](https://github.com/n8n-io/n8n/issues/9869)) ([2d19aef](https://github.com/n8n-io/n8n/commit/2d19aef54083d97e94e50a1ee58e8525bbf28548))
* HTTP Request tool - allow hyphens in placeholders ([#10037](https://github.com/n8n-io/n8n/issues/10037)) ([8cd9370](https://github.com/n8n-io/n8n/commit/8cd93704bee116eceb0e3bd5fa849c4b314454ec))
* HTTP Request tool - do not error on missing headers ([#10044](https://github.com/n8n-io/n8n/issues/10044)) ([04b62e0](https://github.com/n8n-io/n8n/commit/04b62e0398eafd923d5f27a3e1c71b925ddb8817))
* **HubSpot Node:** Migrate from v2 owners api ([#10013](https://github.com/n8n-io/n8n/issues/10013)) ([56dd491](https://github.com/n8n-io/n8n/commit/56dd491bcaeab1d11d7874f190eaf20d2e315ca1))
* Number input defaults to 0 not allowing to have arbitrary precision ([#10021](https://github.com/n8n-io/n8n/issues/10021)) ([e4e66ab](https://github.com/n8n-io/n8n/commit/e4e66ab7da5651fede8b3065419ffb393a2fd16d))
* **OpenAI Chat Model Node:** Respect baseURL override for /models ([#10076](https://github.com/n8n-io/n8n/issues/10076)) ([e5dda57](https://github.com/n8n-io/n8n/commit/e5dda5731dfbb50f5aaf2b152f9c5bc89b1d80a6))
* **Telegram Trigger Node:** Fix issue with videos not being downloaded ([#10007](https://github.com/n8n-io/n8n/issues/10007)) ([e84ab35](https://github.com/n8n-io/n8n/commit/e84ab35c4ab0ec47bdbd4343e58c62bbb70f3ec9))
* **Webhook Node:** Binary property option name and description update ([#10043](https://github.com/n8n-io/n8n/issues/10043)) ([9302e33](https://github.com/n8n-io/n8n/commit/9302e33d558564bb5ba172eaeb8c300693b87286))
### Features
* **Asana Node:** Add support for project privacy settings ([#10027](https://github.com/n8n-io/n8n/issues/10027)) ([429481c](https://github.com/n8n-io/n8n/commit/429481c5c4b7f448739a561596873038185ba467))
* Better error when calling expression function on input that is undefined or null ([#10009](https://github.com/n8n-io/n8n/issues/10009)) ([519e57b](https://github.com/n8n-io/n8n/commit/519e57bda5115149357fb2b1c2270e481ea09e38))
* **editor:** Make expression autocomplete search case-insensitive ([#10017](https://github.com/n8n-io/n8n/issues/10017)) ([cde6fe9](https://github.com/n8n-io/n8n/commit/cde6fe90e5c8a9c5983e27f0d82599425fba915b))
* **editor:** Tweak node creator search logic for AI sub-nodes ([#10025](https://github.com/n8n-io/n8n/issues/10025)) ([7db1656](https://github.com/n8n-io/n8n/commit/7db16561dc890849e2d5742bb73f9d5b8e79e37d))
* **Google Vertex Chat Model Node:** Add support for Google Vertex AI Chat models ([#9970](https://github.com/n8n-io/n8n/issues/9970)) ([071130a](https://github.com/n8n-io/n8n/commit/071130a2dc0b450eb6ce6d39fe28cfeefd05633c))
* **Postgres Chat Memory Node:** Implement Postgres Chat Memory node ([#10071](https://github.com/n8n-io/n8n/issues/10071)) ([9cbbb63](https://github.com/n8n-io/n8n/commit/9cbbb6335df0d36f66f22c18041d12f14dc59b32))
* **Text Classifier Node:** Add Text Classifier Node ([#9997](https://github.com/n8n-io/n8n/issues/9997)) ([28ca7d6](https://github.com/n8n-io/n8n/commit/28ca7d6a2dd818c8795acda6ddf7329b8621d9de))
# [1.50.0](https://github.com/n8n-io/n8n/compare/n8n@1.49.0...n8n@1.50.0) (2024-07-10)
### Bug Fixes
* **core:** Aborting manual trigger tests should call `closeFunction` ([#9980](https://github.com/n8n-io/n8n/issues/9980)) ([6107798](https://github.com/n8n-io/n8n/commit/61077985163037ed3c6a8e9e7476cd6c525ff5f2))
* **core:** Allow owner and admin to edit nodes with credentials that haven't been shared with them explicitly ([#9922](https://github.com/n8n-io/n8n/issues/9922)) ([0f49598](https://github.com/n8n-io/n8n/commit/0f495986f89b60ec9bb86801f9779ee9aa87ccfb))
* **core:** Clear active execution on cancellation in scaling mode ([#9979](https://github.com/n8n-io/n8n/issues/9979)) ([7e972c7](https://github.com/n8n-io/n8n/commit/7e972c78afaf950effec17d8eee16cbf86101d03))
* **core:** Disconnect Redis after pausing queue during worker shutdown ([#9928](https://github.com/n8n-io/n8n/issues/9928)) ([c82579b](https://github.com/n8n-io/n8n/commit/c82579bf760cc4b5a2670b14e4e48fc37e2e2263))
* **core:** Don't execute 'workflowExecuteBefore' hook on execution continuations ([#9905](https://github.com/n8n-io/n8n/issues/9905)) ([adb8315](https://github.com/n8n-io/n8n/commit/adb83155ca9478a548e6fe926735d5872de10fea))
* **core:** Prevent multiple values in the execution metadata for the same key and executionId ([#9953](https://github.com/n8n-io/n8n/issues/9953)) ([2e6b03b](https://github.com/n8n-io/n8n/commit/2e6b03b2cb471aefa8104b7b80cf12e64f16e4fb))
* **Google Sheets Node:** Append fails if cells have some default values added by data validation rules ([#9950](https://github.com/n8n-io/n8n/issues/9950)) ([d1821eb](https://github.com/n8n-io/n8n/commit/d1821eba9221eb243b62ad561193102b24dd05a5))
* **Invoice Ninja Node:** Fix assigning an invoice to a payment ([#9590](https://github.com/n8n-io/n8n/issues/9590)) ([7a3c127](https://github.com/n8n-io/n8n/commit/7a3c127b2cbea01f9a21c8d517d1dc919bc8121f))
* **Invoice Ninja Node:** Fix emailing and marking invoice as paid / sent ([#9589](https://github.com/n8n-io/n8n/issues/9589)) ([908ddd8](https://github.com/n8n-io/n8n/commit/908ddd8a24e8a858d9c1eddf2f727234e66a62f7))
### Features
* **Chat Trigger Node:** Add support for file uploads & harmonize public and development chat ([#9802](https://github.com/n8n-io/n8n/issues/9802)) ([df78315](https://github.com/n8n-io/n8n/commit/df783151b86e2db3e325d3b9d85f4abb71d3d246))
* **Google Cloud Firestore Node:** Add support for service account and document creation with id ([#9713](https://github.com/n8n-io/n8n/issues/9713)) ([cb1bbf5](https://github.com/n8n-io/n8n/commit/cb1bbf5fd395ec4855ac21d851b180c8526b698a))
* **Orbit Node:** Deprecate Orbit nicely ([#9962](https://github.com/n8n-io/n8n/issues/9962)) ([9577d9c](https://github.com/n8n-io/n8n/commit/9577d9c847b56d9907d2bbe9ec85127bb8f67cfa))
* Qdrant Vector Store search filter ([#9900](https://github.com/n8n-io/n8n/issues/9900)) ([fbe4bca](https://github.com/n8n-io/n8n/commit/fbe4bca634e8e03c9455843e1a1f89706d1557d2))
* **Splunk Node:** Overhaul ([#9813](https://github.com/n8n-io/n8n/issues/9813)) ([e5c3247](https://github.com/n8n-io/n8n/commit/e5c324753fb41752f9722d61c5d336d6e5c67cca))
* **Telegram Node:** Add support to Keyboard Button Mini Apps ([#9511](https://github.com/n8n-io/n8n/issues/9511)) ([3a17943](https://github.com/n8n-io/n8n/commit/3a179439c7586189b8264131fd16da9d14f074b6))
# [1.49.0](https://github.com/n8n-io/n8n/compare/n8n@1.48.0...n8n@1.49.0) (2024-07-03)
### Bug Fixes
* **core:** Add a WebCrypto Polyfill for older versions of Node.js 18 ([#9894](https://github.com/n8n-io/n8n/issues/9894)) ([59c8bf1](https://github.com/n8n-io/n8n/commit/59c8bf1c44057b3f798645a22ad16362401ebeed))
* **core:** Don't allow using credentials that are not part of the same project ([#9916](https://github.com/n8n-io/n8n/issues/9916)) ([ab2a548](https://github.com/n8n-io/n8n/commit/ab2a5488560a814fc72c0c5cd71e5f62f05cd235))
* **core:** Filter out certain executions from crash recovery ([#9904](https://github.com/n8n-io/n8n/issues/9904)) ([7044d1c](https://github.com/n8n-io/n8n/commit/7044d1ca2841b6d87ae929072bb94dda82909795))
* **core:** Fix AddActivatedAtUserSetting migration on MariaDB ([#9910](https://github.com/n8n-io/n8n/issues/9910)) ([db29e84](https://github.com/n8n-io/n8n/commit/db29e84666b814fd4710dc3ade6e53304216fad5))
* **core:** Fix execution cancellation in scaling mode ([#9841](https://github.com/n8n-io/n8n/issues/9841)) ([e613de2](https://github.com/n8n-io/n8n/commit/e613de28ca2db23746b586e0a0b33f1c1ee1abe5))
* **core:** Fix worker logs relay ([#9919](https://github.com/n8n-io/n8n/issues/9919)) ([7c53433](https://github.com/n8n-io/n8n/commit/7c5343319144ce3524b14018eef77eace221b608))
* **core:** Throw on adding execution without execution data ([#9903](https://github.com/n8n-io/n8n/issues/9903)) ([abb7458](https://github.com/n8n-io/n8n/commit/abb74587db88a56453b269826885df0d01766290))
* **editor:** Don't try to load credentials on the demo route ([#9926](https://github.com/n8n-io/n8n/issues/9926)) ([b80df2a](https://github.com/n8n-io/n8n/commit/b80df2a47ebe4450862e200c9cf47f6e94012c91))
* **editor:** Enable expression preview in SQL node when looking at executions ([#9733](https://github.com/n8n-io/n8n/issues/9733)) ([d9747d5](https://github.com/n8n-io/n8n/commit/d9747d5e9b42d7f379f6f4219b960893b7b153b3))
* **editor:** Fix frontend project roles ([#9901](https://github.com/n8n-io/n8n/issues/9901)) ([f229577](https://github.com/n8n-io/n8n/commit/f2295772094ff936e210f52ebcbc938915d1c129))
* **editor:** Fix new node credential creation via Resource Locator Component ([#9896](https://github.com/n8n-io/n8n/issues/9896)) ([55cbc90](https://github.com/n8n-io/n8n/commit/55cbc900a48c579b712dddfa74e133e1d9c11799))
* **editor:** Fix performance issues related to expressions and pinned data ([#9882](https://github.com/n8n-io/n8n/issues/9882)) ([13d83f2](https://github.com/n8n-io/n8n/commit/13d83f2037d659fccc8889dd994ddd984467d987))
* **editor:** Improve text wrapping in schema view ([#9888](https://github.com/n8n-io/n8n/issues/9888)) ([dc1c5fc](https://github.com/n8n-io/n8n/commit/dc1c5fce8af732c438d2f1698ee08f18d2358a6c))
* **Execute Workflow Node:** Continue on fail behaviour not correctly implemented ([#9890](https://github.com/n8n-io/n8n/issues/9890)) ([16b1a09](https://github.com/n8n-io/n8n/commit/16b1a094b19e5f803a460b99c6062a1175bec153))
* **LinkedIn Node:** Fix issue with legacy credential no longer working ([#9912](https://github.com/n8n-io/n8n/issues/9912)) ([873b7e5](https://github.com/n8n-io/n8n/commit/873b7e59dcea276c9f792570805a6de8ad4607a3))
### Features
* Add Zep Cloud Memory component ([#9657](https://github.com/n8n-io/n8n/issues/9657)) ([41c47a2](https://github.com/n8n-io/n8n/commit/41c47a28a9d4502287ca1bbbb4704f2763288a11))
* **Copper Node:** Update credential to support HTTP Request node ([#9837](https://github.com/n8n-io/n8n/issues/9837)) ([e6ad5a7](https://github.com/n8n-io/n8n/commit/e6ad5a71935a5f82168bf300246ccb3535648b0b))
* **editor:** Add docs sidebar to credential modal ([#9914](https://github.com/n8n-io/n8n/issues/9914)) ([b2f8ea7](https://github.com/n8n-io/n8n/commit/b2f8ea7918d7e10e91db0e04ef5b7ad40a5bdbb5))
* **editor:** Remove Segment ([#9878](https://github.com/n8n-io/n8n/issues/9878)) ([10f7d4b](https://github.com/n8n-io/n8n/commit/10f7d4b5b92013407c9a4eb9edd619d385efe10f))
* **Embeddings Cohere Node:** Add v3 Cohere models ([#9887](https://github.com/n8n-io/n8n/issues/9887)) ([403e19b](https://github.com/n8n-io/n8n/commit/403e19b3e316db81b62eb456b38e7325bf13529c))
* **GitHub Node:** Add support for state reasons when editing an issue ([#9848](https://github.com/n8n-io/n8n/issues/9848)) ([61c20d1](https://github.com/n8n-io/n8n/commit/61c20d1ae3c65b04c767c5b704c4fc4efd356ccf))
* Introduce debug info button ([#9895](https://github.com/n8n-io/n8n/issues/9895)) ([be9a247](https://github.com/n8n-io/n8n/commit/be9a247577ffc28559a23fea2db9b2c598dca036))
* **Merge Node:** Overhaul, v3 ([#9528](https://github.com/n8n-io/n8n/issues/9528)) ([af69c80](https://github.com/n8n-io/n8n/commit/af69c80bf5a22f80979405041210dc77d2682c51))
* **Vector Store Tool Node:** Add Vector Store Tool ([#9865](https://github.com/n8n-io/n8n/issues/9865)) ([df2bc84](https://github.com/n8n-io/n8n/commit/df2bc84d2b3830d31319c108f1b01256de95e774))
* **Zammad Node:** Add reply_to and sender fields to article on ticket creation ([#9911](https://github.com/n8n-io/n8n/issues/9911)) ([957b2d6](https://github.com/n8n-io/n8n/commit/957b2d6108dccd9495291c4764816cc27e112e87))
# [1.48.0](https://github.com/n8n-io/n8n/compare/n8n@1.47.0...n8n@1.48.0) (2024-06-27)
### Bug Fixes
* **core:** Fix init for `AuditEventRelay` ([#9839](https://github.com/n8n-io/n8n/issues/9839)) ([16d3083](https://github.com/n8n-io/n8n/commit/16d3083af7465d0788f25d843e497b4c7d69de92))
* **core:** Fix telemetry for concurrency control ([#9845](https://github.com/n8n-io/n8n/issues/9845)) ([e25682d](https://github.com/n8n-io/n8n/commit/e25682ddad6ee961a1afe5365d7bbad871a20a4c))
* **editor:** Fix initialize authenticated features ([#9867](https://github.com/n8n-io/n8n/issues/9867)) ([4de58dc](https://github.com/n8n-io/n8n/commit/4de58dcbf5f29bf5414414aa4703356f69a29356))
* **editor:** Load credentials for workflow before determining credentials errors ([#9876](https://github.com/n8n-io/n8n/issues/9876)) ([4008c14](https://github.com/n8n-io/n8n/commit/4008c147d76daa6ff6d43f30c9a18bf1cef7e5d5))
* **editor:** Optimizing main sidebar to have more space for Projects ([#9686](https://github.com/n8n-io/n8n/issues/9686)) ([5cdcb61](https://github.com/n8n-io/n8n/commit/5cdcb61f668a6e00829bee25f40cc869376a9cd7))
* **editor:** Properly update workflow info in main header ([#9789](https://github.com/n8n-io/n8n/issues/9789)) ([1ba656e](https://github.com/n8n-io/n8n/commit/1ba656ef4aae97c78162114ad8de533b275db280))
* **editor:** Show error state correctly in options parameter with remote options ([#9836](https://github.com/n8n-io/n8n/issues/9836)) ([5bc58ef](https://github.com/n8n-io/n8n/commit/5bc58efde9c127eef8082b23cf5d8dcd91162cf4))
* **editor:** Use pinned data to resolve expressions in unexecuted nodes ([#9693](https://github.com/n8n-io/n8n/issues/9693)) ([6cb3072](https://github.com/n8n-io/n8n/commit/6cb3072a5db366404f3d16323498371d28582c06))
* Fix missing node logos ([#9844](https://github.com/n8n-io/n8n/issues/9844)) ([1eeaf32](https://github.com/n8n-io/n8n/commit/1eeaf32523c30f000a1bb8f362c478a086ca7928))
* **Zulip Node:** Fix a typo preventing some messages from updating ([#7078](https://github.com/n8n-io/n8n/issues/7078)) ([553b135](https://github.com/n8n-io/n8n/commit/553b135b0b73fa29062d2b6ef28f98c47bcd186b))
### Features
* Add RS client to hooks service ([#9834](https://github.com/n8n-io/n8n/issues/9834)) ([b807e67](https://github.com/n8n-io/n8n/commit/b807e6726f6ac86df9078c25275b6360a4fcee42))
* **Anthropic Chat Model Node:** Add support for Claude 3.5 Sonnet ([#9832](https://github.com/n8n-io/n8n/issues/9832)) ([2ce97be](https://github.com/n8n-io/n8n/commit/2ce97be33e8aa4f3023d486441ccc4860a0e07ca))
* **editor:** Show multiple nodes in input pane schema view ([#9816](https://github.com/n8n-io/n8n/issues/9816)) ([e51de9d](https://github.com/n8n-io/n8n/commit/e51de9d3916e3fcaa05e92dfb8b9b5c722bff33c))
# [1.47.0](https://github.com/n8n-io/n8n/compare/n8n@1.46.0...n8n@1.47.0) (2024-06-20)
### Bug Fixes
* **AI Agent Node:** Exclude tools agent from unsupported node versions ([#9728](https://github.com/n8n-io/n8n/issues/9728)) ([28d1a5d](https://github.com/n8n-io/n8n/commit/28d1a5d00d9f8a3bb2f812bb11d9d31c1cbadb24))
* **Airtable Node:** Make multipleRecordLinks editable in fields ([#9608](https://github.com/n8n-io/n8n/issues/9608)) ([fdde995](https://github.com/n8n-io/n8n/commit/fdde9957c80613a27762eeb54272cc492f499dbf))
* **AWS SES Node:** Fix issue with email aliases not working for sending from or sending to ([#9811](https://github.com/n8n-io/n8n/issues/9811)) ([e1e8a75](https://github.com/n8n-io/n8n/commit/e1e8a7576308cbc0833cdae35d51810f63b98382))
* Changes to workflow staticData erroneously updating updatedAt ([#9790](https://github.com/n8n-io/n8n/issues/9790)) ([adbd0d1](https://github.com/n8n-io/n8n/commit/adbd0d17abcf8d46bdef44ff45cecbc3bb6c8755))
* **core:** Ensure execution recovery skips successful executions ([#9793](https://github.com/n8n-io/n8n/issues/9793)) ([4131408](https://github.com/n8n-io/n8n/commit/4131408e5e28e4f40287c4880a4b5347e3cdc169))
* **core:** Ensure followers do not recover executions from logs ([#9785](https://github.com/n8n-io/n8n/issues/9785)) ([7c358e5](https://github.com/n8n-io/n8n/commit/7c358e5baafa295f826f891266457cc6c61cd6de))
* **core:** Update transactional email links for RBAC ([#9727](https://github.com/n8n-io/n8n/issues/9727)) ([ceb7f07](https://github.com/n8n-io/n8n/commit/ceb7f074eb1b22ebc698fc168f73a0da6a3d9769))
* **core:** Upgrade `ws` to address CVE-2024-37890 ([#9801](https://github.com/n8n-io/n8n/issues/9801)) ([f98c4b8](https://github.com/n8n-io/n8n/commit/f98c4b8ac033133e4897b5d42326b0d21e2e96be))
* **editor:** Active toggle incorrectly displayed as inactive in execution view ([#9778](https://github.com/n8n-io/n8n/issues/9778)) ([551fb6d](https://github.com/n8n-io/n8n/commit/551fb6d7a2e59fe1b93183745962d9eff4741d44))
* **editor:** Add telemetry to resource moving ([#9720](https://github.com/n8n-io/n8n/issues/9720)) ([e84d253](https://github.com/n8n-io/n8n/commit/e84d2538b6f59e424d92b1f622edb7d6cff756e8))
* **editor:** Error dropdown in resource locator disappears when search filter is required ([#9681](https://github.com/n8n-io/n8n/issues/9681)) ([1a3f72b](https://github.com/n8n-io/n8n/commit/1a3f72b751bf82b1f537882d692ccd6cff7c3f94))
* **editor:** Fix node icon in node creator header ([#9782](https://github.com/n8n-io/n8n/issues/9782)) ([b7d356f](https://github.com/n8n-io/n8n/commit/b7d356f49cdd5d9e63e1aeffecb25da0fc906d6a))
* **editor:** Improve touch device detection ([#9675](https://github.com/n8n-io/n8n/issues/9675)) ([3b86f52](https://github.com/n8n-io/n8n/commit/3b86f52b0290c98ce371be90b2aea699efedbc73))
* **editor:** Revert header toggle fix ([#9800](https://github.com/n8n-io/n8n/issues/9800)) ([11fe48b](https://github.com/n8n-io/n8n/commit/11fe48b3dc91375140a53b73093733536e48d4cb))
* **editor:** Use BroadcastChannel instead of window.opener for OAuth callback window ([#9779](https://github.com/n8n-io/n8n/issues/9779)) ([87cb199](https://github.com/n8n-io/n8n/commit/87cb199745ae4ae9d73f3dfdf5c2bd95acfb9c9e))
* **editor:** Use segments/graphemes when creating the compact sidebar entries ([#9776](https://github.com/n8n-io/n8n/issues/9776)) ([be7249f](https://github.com/n8n-io/n8n/commit/be7249f568d922238c1a95c9d182a01b25ac0ddb))
* **Elasticsearch Node:** Fix issue with self signed certificates ([#9805](https://github.com/n8n-io/n8n/issues/9805)) ([77bf166](https://github.com/n8n-io/n8n/commit/77bf16667b4c9a70ce23e88106b6b9da3d9f0e27))
* Fix sending pin data twice causing payload too large errors ([#9710](https://github.com/n8n-io/n8n/issues/9710)) ([6c1a4c8](https://github.com/n8n-io/n8n/commit/6c1a4c8ebfd60c769bba9441ef732b726ab8d9db))
* **Google Sheets Node:** Check for column names changes before upsert, append, update ([#9649](https://github.com/n8n-io/n8n/issues/9649)) ([223488f](https://github.com/n8n-io/n8n/commit/223488f190223596d9ec634dd0ecb3cce1ea442b))
* **Slack Node:** Do not try to parse block if it's already object ([#9643](https://github.com/n8n-io/n8n/issues/9643)) ([8f94dcc](https://github.com/n8n-io/n8n/commit/8f94dcc0e9dee141d3ea922328abd81c0c6d1707))
* When editing nodes only show the credentials in the dropdown that the user is allowed to use in that workflow ([#9718](https://github.com/n8n-io/n8n/issues/9718)) ([2cf4364](https://github.com/n8n-io/n8n/commit/2cf4364ee0d4343e952e9571574a17ef6122b482))
### Features
* Add custom data to public API execution endpoints ([#9705](https://github.com/n8n-io/n8n/issues/9705)) ([a104660](https://github.com/n8n-io/n8n/commit/a1046607bf6b136c9e1047350007901e695cb52f))
* **core:** Expand crash recovery to cover queue mode ([#9676](https://github.com/n8n-io/n8n/issues/9676)) ([c58621a](https://github.com/n8n-io/n8n/commit/c58621ab79181c0b76d4102af6c76adc4ebdc69c))
* **core:** Use WebCrypto to generate all random numbers and strings ([#9786](https://github.com/n8n-io/n8n/issues/9786)) ([65c5609](https://github.com/n8n-io/n8n/commit/65c5609ab51881c223dcbf5ee567dbc83e6dd4e5))
* HTTP request tool ([#9228](https://github.com/n8n-io/n8n/issues/9228)) ([be2635e](https://github.com/n8n-io/n8n/commit/be2635e50e922be6a3f9984d641ac57b78c86874))
* **JWT Node:** Add an option to allow a "kid" (key ID) header claim ([#9797](https://github.com/n8n-io/n8n/issues/9797)) ([15d631c](https://github.com/n8n-io/n8n/commit/15d631c412b3c13c8d996d409a524d1061286cf4))
* **Pipedrive Node:** Add sort field for get all persons ([#8138](https://github.com/n8n-io/n8n/issues/8138)) ([4e89343](https://github.com/n8n-io/n8n/commit/4e893436fb2347859616a583eab2a412b193e392))
* **Set Node:** Preserve binary data by default ([#9668](https://github.com/n8n-io/n8n/issues/9668)) ([d116353](https://github.com/n8n-io/n8n/commit/d1163533a6a262074526a6514789e3d011e3b864))
### Performance Improvements
* **core:** Introduce concurrency control for main mode ([#9453](https://github.com/n8n-io/n8n/issues/9453)) ([7973423](https://github.com/n8n-io/n8n/commit/797342343f5ef560e8333e2ad67b4395bc0aad0a))
# [1.46.0](https://github.com/n8n-io/n8n/compare/n8n@1.45.0...n8n@1.46.0) (2024-06-12)
### Bug Fixes
* **Chat Trigger Node:** Fix public chat container dimensions ([#9664](https://github.com/n8n-io/n8n/issues/9664)) ([3b10c0f](https://github.com/n8n-io/n8n/commit/3b10c0f6aa87969965ed8a4ec339b295d6fe6199))
* **core:** Allow graceful shutdown for main with active executions ([#9661](https://github.com/n8n-io/n8n/issues/9661)) ([4b345be](https://github.com/n8n-io/n8n/commit/4b345bec0326f0fb874afb0f62ec246cca70344f))
* **core:** Fix optional chaining in continue on fail check ([#9667](https://github.com/n8n-io/n8n/issues/9667)) ([6ae6a5e](https://github.com/n8n-io/n8n/commit/6ae6a5ebdf9e8d23ffd2bb4a230665088a2c269b))
* **editor:** Color node connections correctly in execution preview for nodes that have pinned data ([#9669](https://github.com/n8n-io/n8n/issues/9669)) ([ebba7c8](https://github.com/n8n-io/n8n/commit/ebba7c87cdc96b08f8a2075d6f4907f7671dea4b))
* **editor:** Fix node connection showing incorrect item count during … ([#9684](https://github.com/n8n-io/n8n/issues/9684)) ([99b54bb](https://github.com/n8n-io/n8n/commit/99b54bb0296a855f6bbaf1183b8a554dcf072bb7))
* **editor:** Improve dragndrop of input pills with spaces ([#9656](https://github.com/n8n-io/n8n/issues/9656)) ([291d46a](https://github.com/n8n-io/n8n/commit/291d46af155cd5c512f5e7d4597e31d7ea02bc54))
* **editor:** Improve large data warning in input/output panel ([#9671](https://github.com/n8n-io/n8n/issues/9671)) ([4918ac8](https://github.com/n8n-io/n8n/commit/4918ac81dee2ad950ea0088c99b687a5e7e447b4))
* **editor:** Indent on tabs in expression fields ([#9659](https://github.com/n8n-io/n8n/issues/9659)) ([bb7227d](https://github.com/n8n-io/n8n/commit/bb7227d18d574af35871c2d2f2a2d1310932e0ff))
* **editor:** Node background for executing nodes in dark mode ([#9682](https://github.com/n8n-io/n8n/issues/9682)) ([ae00b44](https://github.com/n8n-io/n8n/commit/ae00b446a79e86cf570287c904fd6dde41ddf71a))
* **editor:** Persist tag filter when clicking tag directly in workflows page ([#9709](https://github.com/n8n-io/n8n/issues/9709)) ([0502738](https://github.com/n8n-io/n8n/commit/0502738c0d63d2da5cca4d9e857ce3b4bec2f8c8))
* **editor:** Prevent running workflows using keyboard shortcuts if execution is disabled ([#9644](https://github.com/n8n-io/n8n/issues/9644)) ([e9e3b25](https://github.com/n8n-io/n8n/commit/e9e3b254fe10e6b9b1783e931caadf792866d3fc))
* **editor:** Prevent saving already saved workflows ([#9670](https://github.com/n8n-io/n8n/issues/9670)) ([b652405](https://github.com/n8n-io/n8n/commit/b652405a0614e45d051268bb05051b454da21d0a))
* **editor:** Remove transparency from dark mode callouts ([#9650](https://github.com/n8n-io/n8n/issues/9650)) ([566b52c](https://github.com/n8n-io/n8n/commit/566b52c4e1b438f10aa6290aa6486ddd095708c9))
* **editor:** Render credentials editable when opening them from the node view ([#9678](https://github.com/n8n-io/n8n/issues/9678)) ([dc17cf3](https://github.com/n8n-io/n8n/commit/dc17cf3a490ea0dc0a3612f41a7d35e2723c15f9))
* **Gotify Node:** Fix issue with self signed certificates not working ([#9647](https://github.com/n8n-io/n8n/issues/9647)) ([68e856d](https://github.com/n8n-io/n8n/commit/68e856d1556d487bc1d5cd3c85dd09d7445b2bc9))
* Introduce `HooksService` ([#8962](https://github.com/n8n-io/n8n/issues/8962)) ([dda7901](https://github.com/n8n-io/n8n/commit/dda7901398cd7dc81297884f186b9f98f41278b4))
* **Jira Software Node:** Fix the order by feature ([#9639](https://github.com/n8n-io/n8n/issues/9639)) ([7aea824](https://github.com/n8n-io/n8n/commit/7aea8243fe32876158c9db6807f654554bf9555e))
* **n8n Form Trigger Node:** Error if Respond to Webhook and respond node not in workflow ([#9641](https://github.com/n8n-io/n8n/issues/9641)) ([b45f3dc](https://github.com/n8n-io/n8n/commit/b45f3dc9fbfbf190cec4f283b05dac66db5fe8f9))
* **Remove Duplicates Node:** Tolerate null fields ([#9642](https://github.com/n8n-io/n8n/issues/9642)) ([a684681](https://github.com/n8n-io/n8n/commit/a684681ea12329a821bdba9a665d79a365dacd9d))
* Reset pagination when output size changes ([#9652](https://github.com/n8n-io/n8n/issues/9652)) ([e520f8a](https://github.com/n8n-io/n8n/commit/e520f8a98f186ecefca8555afdbc08cbc19ef4b0))
* **X (Formerly Twitter) Node:** Change how tweet id is retrieved from quote URL ([#9635](https://github.com/n8n-io/n8n/issues/9635)) ([9853ecc](https://github.com/n8n-io/n8n/commit/9853ecc5bc84a64dc334668fb1c5dd632ebbb56d))
### Features
* Add support for dark mode node icons and colors ([#9412](https://github.com/n8n-io/n8n/issues/9412)) ([600013a](https://github.com/n8n-io/n8n/commit/600013a1ab770c0ff508aae930802f3f8f48ffb4))
* **core:** Add batching and other options to declarative nodes ([#8885](https://github.com/n8n-io/n8n/issues/8885)) ([4e56863](https://github.com/n8n-io/n8n/commit/4e568631bebb8db41a8ec9b4651abb0e8903eeed))
* **core:** Implement `project:viewer` role ([#9611](https://github.com/n8n-io/n8n/issues/9611)) ([6187cc5](https://github.com/n8n-io/n8n/commit/6187cc5762fe2156504041f41020d0fdad063f49))
* **editor:** Add isEmpty on DateTime, add is empty to all types in filter component ([#9645](https://github.com/n8n-io/n8n/issues/9645)) ([eccc637](https://github.com/n8n-io/n8n/commit/eccc637b63cbc2581f29feb27f148ba437bcf5d4))
* **editor:** Add move resources option to workflows and credentials on ([#9654](https://github.com/n8n-io/n8n/issues/9654)) ([bc35e8c](https://github.com/n8n-io/n8n/commit/bc35e8c33d470399466514b4d4874c965d7edc08))
* **editor:** Harmonize rendering of new-lines in RunData ([#9614](https://github.com/n8n-io/n8n/issues/9614)) ([bc3dcf7](https://github.com/n8n-io/n8n/commit/bc3dcf706f578837e8d6fe6473d414d9dd58e3c4))
* **OpenAI Node:** Allow to select Image analyze model & improve types ([#9660](https://github.com/n8n-io/n8n/issues/9660)) ([1fdd657](https://github.com/n8n-io/n8n/commit/1fdd657a0ce0b9722ee697d05bbada7ecf4cdf05))
* Update NPS Value Survey ([#9638](https://github.com/n8n-io/n8n/issues/9638)) ([50bd5b9](https://github.com/n8n-io/n8n/commit/50bd5b9080213d4286c37b93f598753dbee32eb4))
# [1.45.0](https://github.com/n8n-io/n8n/compare/n8n@1.44.0...n8n@1.45.0) (2024-06-05)
### Bug Fixes
* **AI Agent Node:** Improve Tools agent empty tool input message ([#9622](https://github.com/n8n-io/n8n/issues/9622)) ([e7f6162](https://github.com/n8n-io/n8n/commit/e7f616290f20c37121f554303f775a102569bdc7))
* **core:** Ensure graceful shutdown for workers ([#9547](https://github.com/n8n-io/n8n/issues/9547)) ([7fc00d8](https://github.com/n8n-io/n8n/commit/7fc00d8d104c2ceebf56f897c8d54fc292003811))
* **core:** Ensure ID is a positive integer when fetching execution ([#9629](https://github.com/n8n-io/n8n/issues/9629)) ([411ffbd](https://github.com/n8n-io/n8n/commit/411ffbda7f6a82e2ee249daa39e614c184df8643))
* **core:** Start WaitTracker only in the main container ([#9600](https://github.com/n8n-io/n8n/issues/9600)) ([08d9c9a](https://github.com/n8n-io/n8n/commit/08d9c9a7876bd0fd0d087cdc9175d94a33de0cc9))
* **core:** Upgrade mysql2 to address CVE-2024-21512 ([#9565](https://github.com/n8n-io/n8n/issues/9565)) ([4b6e5f0](https://github.com/n8n-io/n8n/commit/4b6e5f09e6770938de5e590a7e0d4565e3dc865c))
* **editor:** Commit theme change from Save button ([#9619](https://github.com/n8n-io/n8n/issues/9619)) ([744c94d](https://github.com/n8n-io/n8n/commit/744c94d94b3576f2a1d4227e49185be77b8c6954))
* **editor:** Filter credentials by project ID also for new workflow ([#9615](https://github.com/n8n-io/n8n/issues/9615)) ([c92765d](https://github.com/n8n-io/n8n/commit/c92765dcdb48789aa111ace29165a4b811fea710))
* **editor:** Improve error messages around pinned data ([#9632](https://github.com/n8n-io/n8n/issues/9632)) ([a8bb53f](https://github.com/n8n-io/n8n/commit/a8bb53f4e3dd5aee8f3b707cb0ee92ccc98e960e))
* **editor:** Render checkboxes in markdown ([#9549](https://github.com/n8n-io/n8n/issues/9549)) ([47d7741](https://github.com/n8n-io/n8n/commit/47d774100bd7a120de50d679e0052d6a1ae5e88a))
* **editor:** Replace more variants of BASE_PATH in static assets ([#9564](https://github.com/n8n-io/n8n/issues/9564)) ([d361b42](https://github.com/n8n-io/n8n/commit/d361b42c7035a3edbdd999a322c9327a8f565f77))
* **editor:** Show correct schema for output with falsy keys ([#9556](https://github.com/n8n-io/n8n/issues/9556)) ([020bd36](https://github.com/n8n-io/n8n/commit/020bd3635444d83f1aef310714470140dcac7c6e))
* **editor:** Show owner email in the owner badge if the resource owner is a pending user ([#9560](https://github.com/n8n-io/n8n/issues/9560)) ([2e9bd67](https://github.com/n8n-io/n8n/commit/2e9bd6739b5a510b6726bbe55dfe09267107e70f))
* **editor:** Show workflow data in header when execution page is hard reloaded ([#9529](https://github.com/n8n-io/n8n/issues/9529)) ([e68a3fd](https://github.com/n8n-io/n8n/commit/e68a3fd6ce7c710c398171b3deb8d8eb565e23ba))
* **editor:** Skip disabled nodes when detecting workflow issues ([#9610](https://github.com/n8n-io/n8n/issues/9610)) ([245c63f](https://github.com/n8n-io/n8n/commit/245c63f216c1074f8857f123e1dfae9b2b2b29bc))
* **HTTP Request Node:** Sanitize secrets of predefined credentials ([#9612](https://github.com/n8n-io/n8n/issues/9612)) ([84f091d](https://github.com/n8n-io/n8n/commit/84f091d3e5f9c661e373acd0c058ee158965b6e8))
* **Jira Software Node:** Fix comments limit and add sorting ([#9634](https://github.com/n8n-io/n8n/issues/9634)) ([a946ead](https://github.com/n8n-io/n8n/commit/a946ead46efecf6864505d465b0369ed67a1f2c7))
* Make AWS credential work with global AWS services ([#9631](https://github.com/n8n-io/n8n/issues/9631)) ([9dbea73](https://github.com/n8n-io/n8n/commit/9dbea7393a9e55edeb5cf9646f5068891e14f84c))
### Features
* **core:** Allow customizing rate limits on a per-route basis, and add rate limiting to more endpoints ([#9522](https://github.com/n8n-io/n8n/issues/9522)) ([7be616e](https://github.com/n8n-io/n8n/commit/7be616e5831678b42deb7de98c974369f7bf8967))
* **core:** Allow transferring credentials from any project to any team project ([#9563](https://github.com/n8n-io/n8n/issues/9563)) ([202c91e](https://github.com/n8n-io/n8n/commit/202c91e7edc2a99eec56436f94f0e552ac4816b5))
* **core:** Allow transferring workflows from any project to any team project ([#9534](https://github.com/n8n-io/n8n/issues/9534)) ([d6db8cb](https://github.com/n8n-io/n8n/commit/d6db8cbf23b46fa2f93c7460bf1df9047b2cfab2))
* **editor:** Add remove node and connections functionality to canvas v2 ([#9602](https://github.com/n8n-io/n8n/issues/9602)) ([f6a466c](https://github.com/n8n-io/n8n/commit/f6a466cd8750930eb7ea717e5113c5a4a477af26))
* **editor:** Chat Trigger tweaks ([#9618](https://github.com/n8n-io/n8n/issues/9618)) ([5322802](https://github.com/n8n-io/n8n/commit/5322802992032e4e5f7c528a1b0668dcbed49db2))
* **editor:** Node Creator AI nodes improvements ([#9484](https://github.com/n8n-io/n8n/issues/9484)) ([be4f54d](https://github.com/n8n-io/n8n/commit/be4f54de157dde60e7ae6b0611fa599a059cd17f))
* **editor:** Overhaul input selector in NDV ([#9520](https://github.com/n8n-io/n8n/issues/9520)) ([c0ec990](https://github.com/n8n-io/n8n/commit/c0ec990f4cc78909e963b82f1492dafafab23b5a))
* **editor:** Update sticky content when checkbox state changes ([#9596](https://github.com/n8n-io/n8n/issues/9596)) ([5361e9f](https://github.com/n8n-io/n8n/commit/5361e9f69ae2211beda2f760ee215cd89c1d77e9))
* **HighLevel Node:** Api v2 support, new node version ([#9554](https://github.com/n8n-io/n8n/issues/9554)) ([19e5c03](https://github.com/n8n-io/n8n/commit/19e5c0397ad75b47c6068db194a3f938722095c8))
* Run once for each item tooltip ([#9486](https://github.com/n8n-io/n8n/issues/9486)) ([b91e50f](https://github.com/n8n-io/n8n/commit/b91e50fc92e3e41f2b4529caa054557309d891d0))
# [1.44.0](https://github.com/n8n-io/n8n/compare/n8n@1.43.0...n8n@1.44.0) (2024-05-30)
### Bug Fixes
* **core:** Block Public API related REST calls when Public API is not enabled ([#9521](https://github.com/n8n-io/n8n/issues/9521)) ([ac4e0fb](https://github.com/n8n-io/n8n/commit/ac4e0fbb47b818973958e37e6b80201ad2ffed6f))
* **core:** Prevent re-parsing of dynamically loaded options ([#9503](https://github.com/n8n-io/n8n/issues/9503)) ([a58be17](https://github.com/n8n-io/n8n/commit/a58be175cc8a65975b7aac15fc3143c38cf3682b))
* **core:** Set source control repository to track remote if ready ([#9532](https://github.com/n8n-io/n8n/issues/9532)) ([dbaac82](https://github.com/n8n-io/n8n/commit/dbaac82f79fd73d5dc11b29faa0e2cee4c55cc3f))
* **core:** Try setting postgres search_path on the database ([#9530](https://github.com/n8n-io/n8n/issues/9530)) ([e55bf03](https://github.com/n8n-io/n8n/commit/e55bf0393ae625ff34d41f1e861008cf7916dbdf))
* **core:** Upgrade sheetjs to address CVE-2024-22363 ([#9498](https://github.com/n8n-io/n8n/issues/9498)) ([8737c09](https://github.com/n8n-io/n8n/commit/8737c0965e3dd2d6eec0f05737cc96c0f12c43c5))
* Don't throw errors for NaN in number operators in the filter component ([#9506](https://github.com/n8n-io/n8n/issues/9506)) ([936bbb2](https://github.com/n8n-io/n8n/commit/936bbb20684ac6f0d376f5a4ee3760e9587223f7))
* **editor:** Executions view popup in dark mode ([#9517](https://github.com/n8n-io/n8n/issues/9517)) ([1abb26e](https://github.com/n8n-io/n8n/commit/1abb26e2dacc2891417ea66f6a5f3dccc4b784cd))
* **editor:** Fix empty node name handling ([#9548](https://github.com/n8n-io/n8n/issues/9548)) ([da41d31](https://github.com/n8n-io/n8n/commit/da41d31bc0e19667a7fef7fac4008c7cb1c6c470))
* **editor:** Make sure auto loading and auto scrolling works in executions tab ([#9505](https://github.com/n8n-io/n8n/issues/9505)) ([3a2e545](https://github.com/n8n-io/n8n/commit/3a2e5455a98dae35ba1a52ec98f67a1fb27fac96))
* **editor:** Prevent expression editor focus being lost when user is selecting ([#9525](https://github.com/n8n-io/n8n/issues/9525)) ([6698179](https://github.com/n8n-io/n8n/commit/6698179a69511d8f009100c66c062218a26cfaad))
* **editor:** Prevent updating node parameter value if it hasn't changed ([#9535](https://github.com/n8n-io/n8n/issues/9535)) ([63990f1](https://github.com/n8n-io/n8n/commit/63990f14e3991770c1b9fbfd56edd6d0f3abd54b))
* **editor:** Prevent XSS in node-issues tooltip ([#9490](https://github.com/n8n-io/n8n/issues/9490)) ([301e846](https://github.com/n8n-io/n8n/commit/301e846cf64a7fce8191696e828eaf1c3fc82e88))
* **editor:** Redirect to workflows list after deleting a workflow ([#9546](https://github.com/n8n-io/n8n/issues/9546)) ([cadb59f](https://github.com/n8n-io/n8n/commit/cadb59fecbf1adeb1c226f9decd92a334656a895))
* **editor:** Send only execution id in postMessage when previewing an execution ([#9514](https://github.com/n8n-io/n8n/issues/9514)) ([49b5bd7](https://github.com/n8n-io/n8n/commit/49b5bd70f0d1c0dce46ea85d23deb75dbea6c51c))
* **editor:** Show execution error toast also if there is no error stack just message ([#9526](https://github.com/n8n-io/n8n/issues/9526)) ([f914c97](https://github.com/n8n-io/n8n/commit/f914c97d11d471aff1dbf66f9334ec98df613d6e))
* **editor:** Show input panel with not connected message ([#9495](https://github.com/n8n-io/n8n/issues/9495)) ([8566301](https://github.com/n8n-io/n8n/commit/85663017313a707c95b63c734942a29ef4473740))
* **editor:** Update webhook paths when duplicating workflow ([#9516](https://github.com/n8n-io/n8n/issues/9516)) ([3be7bb8](https://github.com/n8n-io/n8n/commit/3be7bb898bc2ecc0c2553df2a3e48bd125867ced))
### Features
* **core:** Print the name of the migration that cannot be reverted when using `n8n db:revert` ([#9473](https://github.com/n8n-io/n8n/issues/9473)) ([3b93aae](https://github.com/n8n-io/n8n/commit/3b93aae6dce7827dfb36279447327dfd89fddff5))
* **core:** Upgrade all langchain related dependencies ([#9504](https://github.com/n8n-io/n8n/issues/9504)) ([a77e8dd](https://github.com/n8n-io/n8n/commit/a77e8dd79ec7cbeb357ad03747fe2e4270d91a63))
* **editor:** Show expression infobox on hover and cursor position ([#9507](https://github.com/n8n-io/n8n/issues/9507)) ([ec0373f](https://github.com/n8n-io/n8n/commit/ec0373f666ed7d5c416fdef44afd8dd748755c9f))
* HighLevel oauth2 api credentials ([#9542](https://github.com/n8n-io/n8n/issues/9542)) ([be58905](https://github.com/n8n-io/n8n/commit/be5890536f9b99916de20ae3c771776149132026))
### Performance Improvements
* **core:** Optimize executions filtering by metadata ([#9477](https://github.com/n8n-io/n8n/issues/9477)) ([9bdc83a](https://github.com/n8n-io/n8n/commit/9bdc83a399592a2ca0761070f0e7074a6a3ffa7d))
# [1.43.0](https://github.com/n8n-io/n8n/compare/n8n@1.42.0...n8n@1.43.0) (2024-05-22)
### Bug Fixes
* **core:** Account for retry of execution aborted by pre-execute hook ([#9474](https://github.com/n8n-io/n8n/issues/9474)) ([a217866](https://github.com/n8n-io/n8n/commit/a217866cef6caaef9244f3d16d90f7027adc0c12))
* **core:** Add an option to disable STARTTLS for SMTP connections ([#9415](https://github.com/n8n-io/n8n/issues/9415)) ([0d73588](https://github.com/n8n-io/n8n/commit/0d7358807b4244be574060726388bd49fc90dc64))
* **core:** Do not allow admins to delete the instance owner ([#9489](https://github.com/n8n-io/n8n/issues/9489)) ([fc83005](https://github.com/n8n-io/n8n/commit/fc83005ba0876ebea70f93de700adbd6e3095c96))
* **core:** Do not allow admins to generate password-reset links for instance owner ([#9488](https://github.com/n8n-io/n8n/issues/9488)) ([88b9a40](https://github.com/n8n-io/n8n/commit/88b9a4070b7df943c3ba22047c0656a5d0a2111c))
* **core:** Fix 431 for large dynamic node parameters ([#9384](https://github.com/n8n-io/n8n/issues/9384)) ([d21ad15](https://github.com/n8n-io/n8n/commit/d21ad15c1f12739af6a28983a6469347c26f1e08))
* **core:** Handle credential in body for oauth2 refresh token ([#9179](https://github.com/n8n-io/n8n/issues/9179)) ([c9855e3](https://github.com/n8n-io/n8n/commit/c9855e3dce42f8830636914458d1061668a466a8))
* **core:** Remove excess args from routing error ([#9377](https://github.com/n8n-io/n8n/issues/9377)) ([b1f977e](https://github.com/n8n-io/n8n/commit/b1f977ebd084ab3a8fb1d13109063de7d2a15296))
* **core:** Retry before continue on fail ([#9395](https://github.com/n8n-io/n8n/issues/9395)) ([9b2ce81](https://github.com/n8n-io/n8n/commit/9b2ce819d42c4a541ae94956aaab608a989ec588))
* **editor:** Emit change events from filter component on update ([#9479](https://github.com/n8n-io/n8n/issues/9479)) ([62df433](https://github.com/n8n-io/n8n/commit/62df4331d448dfdabd51db33560a87dd5d805a13))
* **editor:** Fix blank Public API page ([#9409](https://github.com/n8n-io/n8n/issues/9409)) ([14fe9f2](https://github.com/n8n-io/n8n/commit/14fe9f268feeb0ca106ddaaa94c69cb356011524))
* **editor:** Fix i18n translation addition ([#9451](https://github.com/n8n-io/n8n/issues/9451)) ([04dd476](https://github.com/n8n-io/n8n/commit/04dd4760e173bfc8a938413a5915d63291da8afe))
* **editor:** Fix node execution errors showing undefined ([#9487](https://github.com/n8n-io/n8n/issues/9487)) ([62ee796](https://github.com/n8n-io/n8n/commit/62ee79689569b5d2c9823afac238e66e4c645d9b))
* **editor:** Fix outdated roles in variables labels ([#9411](https://github.com/n8n-io/n8n/issues/9411)) ([38b498e](https://github.com/n8n-io/n8n/commit/38b498e73a71a9ca8b10a89e498aa8330acf2626))
* **editor:** Fix project settings layout ([#9475](https://github.com/n8n-io/n8n/issues/9475)) ([96cf41f](https://github.com/n8n-io/n8n/commit/96cf41f8516881f0ba15b0b01dda7712f1edc845))
* **editor:** Fix type errors in `components/executions/workflow` ([#9448](https://github.com/n8n-io/n8n/issues/9448)) ([9c768a0](https://github.com/n8n-io/n8n/commit/9c768a0443520f0c031d4d807d955d7778a00997))
* **editor:** Fix type errors in i18n plugin ([#9441](https://github.com/n8n-io/n8n/issues/9441)) ([a7d3e59](https://github.com/n8n-io/n8n/commit/a7d3e59aef36dd65429ad0b2ea4696b107620eeb))
* **editor:** Fix workflow history TS errors ([#9433](https://github.com/n8n-io/n8n/issues/9433)) ([bc05faf](https://github.com/n8n-io/n8n/commit/bc05faf0a6a0913013e4d46eefb1e45abc390883))
* **editor:** Secondary button in dark mode ([#9401](https://github.com/n8n-io/n8n/issues/9401)) ([aad43d8](https://github.com/n8n-io/n8n/commit/aad43d8cdcc9621fbd864fbe0235c9ff4ddbfe3e))
* **Email Trigger (IMAP) Node:** Handle attachments correctly ([#9410](https://github.com/n8n-io/n8n/issues/9410)) ([68a6c81](https://github.com/n8n-io/n8n/commit/68a6c8172973091e8474a9f173fa4a5e97284f18))
* Fix color picker type errors ([#9436](https://github.com/n8n-io/n8n/issues/9436)) ([2967df2](https://github.com/n8n-io/n8n/commit/2967df2fe098278dd20126dc033b03cbb4b903ce))
* Fix type errors in community nodes components ([#9445](https://github.com/n8n-io/n8n/issues/9445)) ([aac19d3](https://github.com/n8n-io/n8n/commit/aac19d328564bfecda53b338e2c56e5e30e5c0c1))
* **Gmail Trigger Node:** Fetching duplicate emails ([#9424](https://github.com/n8n-io/n8n/issues/9424)) ([3761537](https://github.com/n8n-io/n8n/commit/3761537880f53d9e54b0200a63b067dc3d154787))
* **HTML Node:** Fix typo preventing row attributes from being set in tables ([#9440](https://github.com/n8n-io/n8n/issues/9440)) ([28e3e21](https://github.com/n8n-io/n8n/commit/28e3e211771fd73a88e34b81858188156fca5fbb))
* **HubSpot Trigger Node:** Fix issue with ticketId not being set ([#9403](https://github.com/n8n-io/n8n/issues/9403)) ([b5c7c06](https://github.com/n8n-io/n8n/commit/b5c7c061b7e854a06bd725f7905a7f3ac8dfedc2))
* **Mattermost Node:** Change loadOptions to fetch all items ([#9413](https://github.com/n8n-io/n8n/issues/9413)) ([1377e21](https://github.com/n8n-io/n8n/commit/1377e212c709bc9ca6586c030ec083e89a3d8c37))
* **Microsoft OneDrive Trigger Node:** Fix issue with test run failing ([#9386](https://github.com/n8n-io/n8n/issues/9386)) ([92a1d65](https://github.com/n8n-io/n8n/commit/92a1d65c4b00683cc334c70f183e5f8c99bfae65))
* **RSS Feed Trigger Node:** Use newest date instead of first item for new items ([#9182](https://github.com/n8n-io/n8n/issues/9182)) ([7236a55](https://github.com/n8n-io/n8n/commit/7236a558b945c69fa5680e42c538af7c5276cc31))
* Update operations to run per item ([#8967](https://github.com/n8n-io/n8n/issues/8967)) ([ef9d4ab](https://github.com/n8n-io/n8n/commit/ef9d4aba90c92f9b72a17de242a4ffeb7c034802))
### Features
* Add Slack trigger node ([#9190](https://github.com/n8n-io/n8n/issues/9190)) ([bf54930](https://github.com/n8n-io/n8n/commit/bf549301df541c43931fe4493b4bad7905fb0c8a))
* **Custom n8n Workflow Tool Node:** Add support for tool input schema ([#9470](https://github.com/n8n-io/n8n/issues/9470)) ([2fa46b6](https://github.com/n8n-io/n8n/commit/2fa46b6faac5618a10403066c3dddf4ea9def12c))
* **editor:** Add examples for Luxon DateTime expression methods ([#9361](https://github.com/n8n-io/n8n/issues/9361)) ([40bce7f](https://github.com/n8n-io/n8n/commit/40bce7f44332042bf8dba0442044acd76cc9bf21))
* **editor:** Add examples for root expression methods ([#9373](https://github.com/n8n-io/n8n/issues/9373)) ([a591f63](https://github.com/n8n-io/n8n/commit/a591f63e3ff51c19fe48185144725e881c418b23))
* **editor:** Expand supported Unicode range for expressions ([#9420](https://github.com/n8n-io/n8n/issues/9420)) ([2118236](https://github.com/n8n-io/n8n/commit/211823650ba298aac899ff944819290f0bd4654a))
* **editor:** Update Node Details View header tabs structure ([#9425](https://github.com/n8n-io/n8n/issues/9425)) ([2782534](https://github.com/n8n-io/n8n/commit/2782534d78e9613bda41675b4574c8016b10b0a4))
* **Extract from File Node:** Add option to set encoding for CSV files ([#9392](https://github.com/n8n-io/n8n/issues/9392)) ([f13dbc9](https://github.com/n8n-io/n8n/commit/f13dbc9cc31fba20b4cb0bedf11e56e16079f946))
* **Linear Node:** Add identifier to outputs ([#9469](https://github.com/n8n-io/n8n/issues/9469)) ([ffe034c](https://github.com/n8n-io/n8n/commit/ffe034c72e07346cdbea4dda96c7e2c38ea73c45))
* **OpenAI Node:** Use v2 assistants API and add support for memory ([#9406](https://github.com/n8n-io/n8n/issues/9406)) ([ce3eb12](https://github.com/n8n-io/n8n/commit/ce3eb12a6ba325d3785d54d90ff5a32152afd4c0))
* RBAC ([#8922](https://github.com/n8n-io/n8n/issues/8922)) ([596c472](https://github.com/n8n-io/n8n/commit/596c472ecc756bf934c51e7efae0075fb23313b4))
* **Strava Node:** Update to use sport type ([#9462](https://github.com/n8n-io/n8n/issues/9462)) ([9da9368](https://github.com/n8n-io/n8n/commit/9da93680c28f9191eac7edc452e5123749e5c148))
* **Telegram Node:** Add support for local bot api server ([#8437](https://github.com/n8n-io/n8n/issues/8437)) ([87f965e](https://github.com/n8n-io/n8n/commit/87f965e9055904486f5fd815c060abb4376296a0))
# [1.42.0](https://github.com/n8n-io/n8n/compare/n8n@1.41.0...n8n@1.42.0) (2024-05-15)
### Bug Fixes
* **Code Node:** Bind helper methods to the correct context ([#9380](https://github.com/n8n-io/n8n/issues/9380)) ([82c8801](https://github.com/n8n-io/n8n/commit/82c8801f25446085bc8da5055d9932eed4321f47))
* **Cortex Node:** Fix issue with analyzer response not working for file observables ([#9374](https://github.com/n8n-io/n8n/issues/9374)) ([ed22dcd](https://github.com/n8n-io/n8n/commit/ed22dcd88ac7f8433b9ed5dc2139d8779b0e1d4c))
* **editor:** Render backticks as code segments in error view ([#9352](https://github.com/n8n-io/n8n/issues/9352)) ([4ed5850](https://github.com/n8n-io/n8n/commit/4ed585040b20c50919e2ec2252216639c85194cb))
* **Mattermost Node:** Fix issue when fetching reactions ([#9375](https://github.com/n8n-io/n8n/issues/9375)) ([78e7c7a](https://github.com/n8n-io/n8n/commit/78e7c7a9da96a293262cea5304509261ad10020c))
### Features
* **AI Agent Node:** Implement Tool calling agent ([#9339](https://github.com/n8n-io/n8n/issues/9339)) ([677f534](https://github.com/n8n-io/n8n/commit/677f534661634c74340f50723e55e241570d5a56))
* **core:** Allow using a custom certificates in docker containers ([#8705](https://github.com/n8n-io/n8n/issues/8705)) ([6059722](https://github.com/n8n-io/n8n/commit/6059722fbfeeca31addfc31ed287f79f40aaad18))
* **core:** Node hints(warnings) system ([#8954](https://github.com/n8n-io/n8n/issues/8954)) ([da6088d](https://github.com/n8n-io/n8n/commit/da6088d0bbb952fcdf595a650e1e01b7b02a2b7e))
* **core:** Node version available in expression ([#9350](https://github.com/n8n-io/n8n/issues/9350)) ([a00467c](https://github.com/n8n-io/n8n/commit/a00467c9fa57d740de9eccfcd136267bc9e9559d))
* **editor:** Add examples for number & boolean, add new methods ([#9358](https://github.com/n8n-io/n8n/issues/9358)) ([7b45dc3](https://github.com/n8n-io/n8n/commit/7b45dc313f42317f894469c6aa8abecc55704e3a))
* **editor:** Add examples for object and array expression methods ([#9360](https://github.com/n8n-io/n8n/issues/9360)) ([5293663](https://github.com/n8n-io/n8n/commit/52936633af9c71dff1957ee43a5eda48f7fc1bf1))
* **editor:** Add item selector to expression output ([#9281](https://github.com/n8n-io/n8n/issues/9281)) ([dc5994b](https://github.com/n8n-io/n8n/commit/dc5994b18580b9326574c5208d9beaf01c746f33))
* **editor:** Autocomplete info box: improve structure and add examples ([#9019](https://github.com/n8n-io/n8n/issues/9019)) ([c92c870](https://github.com/n8n-io/n8n/commit/c92c870c7335f4e2af63fa1c6bcfd086b2957ef8))
* **editor:** Remove AI Error Debugging ([#9337](https://github.com/n8n-io/n8n/issues/9337)) ([cda062b](https://github.com/n8n-io/n8n/commit/cda062bde63bcbfdd599d0662ddbe89c27a71686))
* **Slack Node:** Add block support for message updates ([#8925](https://github.com/n8n-io/n8n/issues/8925)) ([1081429](https://github.com/n8n-io/n8n/commit/1081429a4d0f7e2d1fc1841303448035b46e44d1))
### Performance Improvements
* Add tailwind to editor and design system ([#9032](https://github.com/n8n-io/n8n/issues/9032)) ([1c1e444](https://github.com/n8n-io/n8n/commit/1c1e4443f41dd39da8d5fa3951c8dffb0fbfce10))
# [1.41.0](https://github.com/n8n-io/n8n/compare/n8n@1.40.0...n8n@1.41.0) (2024-05-08)
### Bug Fixes
* Cast boolean values in filter parameter ([#9260](https://github.com/n8n-io/n8n/issues/9260)) ([30c8efc](https://github.com/n8n-io/n8n/commit/30c8efc4cc9b25fabc8d9c56e8c29e7e77c04325))
* **core:** Prevent occassional 429s on license init in multi-main setup ([#9284](https://github.com/n8n-io/n8n/issues/9284)) ([22b6f90](https://github.com/n8n-io/n8n/commit/22b6f909505d7c3d9c0583a90599e6e9c244e21e))
* **core:** Report missing SAML attributes early with an actionable error message ([#9316](https://github.com/n8n-io/n8n/issues/9316)) ([225fdbb](https://github.com/n8n-io/n8n/commit/225fdbb379f6dd0005bd4ccb3791c96de35b1653))
* **core:** Webhooks responding with binary data should not prematurely end the response stream ([#9063](https://github.com/n8n-io/n8n/issues/9063)) ([23b676d](https://github.com/n8n-io/n8n/commit/23b676d7cb9708d7a99fc031cfeec22b854be1d9))
* **editor:** Fix multi-select parameters with load options getting cleared ([#9324](https://github.com/n8n-io/n8n/issues/9324)) ([0ee4b6c](https://github.com/n8n-io/n8n/commit/0ee4b6c86000ab164211c1ebed90306cd144af1b))
* **editor:** Fix shortcut issue on save buttons ([#9309](https://github.com/n8n-io/n8n/issues/9309)) ([e74c14f](https://github.com/n8n-io/n8n/commit/e74c14ffbe088ac74dc6358068cd54af9a850cad))
* **editor:** Resolve `$vars` and `$secrets` in expressions in credentials fields ([#9289](https://github.com/n8n-io/n8n/issues/9289)) ([d92f994](https://github.com/n8n-io/n8n/commit/d92f994913befd31aec409ef8e40b290ac4185ba))
* **editor:** Show MFA section to instance owner, even when external auth is enabled ([#9301](https://github.com/n8n-io/n8n/issues/9301)) ([b65e0e2](https://github.com/n8n-io/n8n/commit/b65e0e28114f576f89e271ab8ffdb8550e1be60f))
* **Gmail Node:** Remove duplicate options when creating drafts ([#9299](https://github.com/n8n-io/n8n/issues/9299)) ([bfb0eb7](https://github.com/n8n-io/n8n/commit/bfb0eb7a06f219424486a55256ecca46c14a85ba))
* **Linear Node:** Fix issue with data not always being returned ([#9273](https://github.com/n8n-io/n8n/issues/9273)) ([435272b](https://github.com/n8n-io/n8n/commit/435272b568826edf899dbaba9d10077fbe134ea6))
* **n8n Form Trigger Node:** Fix missing options when using respond to webhook ([#9282](https://github.com/n8n-io/n8n/issues/9282)) ([6ab3781](https://github.com/n8n-io/n8n/commit/6ab378157041abfc918ae1d9408821f8fd5cfb34))
* **Pipedrive Node:** Improve type-safety in custom-property handling ([#9319](https://github.com/n8n-io/n8n/issues/9319)) ([c8895c5](https://github.com/n8n-io/n8n/commit/c8895c540e5c8edfb576960a5ba4ec9ac4426d5b))
* **Read PDF Node:** Disable JS evaluation from PDFs ([#9336](https://github.com/n8n-io/n8n/issues/9336)) ([c4bf5b2](https://github.com/n8n-io/n8n/commit/c4bf5b2b9285402ae09960eb64a5d6f20356eeaf))
### Features
* **editor:** Implement AI Assistant chat UI ([#9300](https://github.com/n8n-io/n8n/issues/9300)) ([491c6ec](https://github.com/n8n-io/n8n/commit/491c6ec546c4ec8ab4eb88d020c13820071bf6dc))
* **editor:** Temporarily disable AI error helper ([#9329](https://github.com/n8n-io/n8n/issues/9329)) ([35b983b](https://github.com/n8n-io/n8n/commit/35b983b6dfbb6ab02367801a15581e80a2d87340))
* **LinkedIn Node:** Upgrade LinkedIn API version ([#9307](https://github.com/n8n-io/n8n/issues/9307)) ([3860077](https://github.com/n8n-io/n8n/commit/3860077f8100fb790acf1d930839e86719a454fd))
* **Redis Node:** Add support for TLS ([#9266](https://github.com/n8n-io/n8n/issues/9266)) ([0a2de09](https://github.com/n8n-io/n8n/commit/0a2de093c01689b8f179b3f4413a4ce29ccf279a))
* **Send Email Node:** Add an option to customize client host-name on SMTP connections ([#9322](https://github.com/n8n-io/n8n/issues/9322)) ([d0d52de](https://github.com/n8n-io/n8n/commit/d0d52def8fb4113a7a4866d30f2e9c7bfe11075e))
* **Slack Node:** Update to use the new API method for file uploads ([#9323](https://github.com/n8n-io/n8n/issues/9323)) ([695e762](https://github.com/n8n-io/n8n/commit/695e762663fde79b9555be8cf075ee4144f380f1))
# [1.40.0](https://github.com/n8n-io/n8n/compare/n8n@1.39.0...n8n@1.40.0) (2024-05-02)
### Bug Fixes
* **Airtable Node:** Do not allow to use deprecated api keys in v1 ([#9171](https://github.com/n8n-io/n8n/issues/9171)) ([017ae6e](https://github.com/n8n-io/n8n/commit/017ae6e1025fb4ae28b46b9c411e4b5c70e280e9))
* **core:** Add `view engine` to webhook server to support forms ([#9224](https://github.com/n8n-io/n8n/issues/9224)) ([24c3150](https://github.com/n8n-io/n8n/commit/24c3150056401ddcf49f7266897b6c73ccc06253))
* **core:** Fix browser session refreshes not working ([#9212](https://github.com/n8n-io/n8n/issues/9212)) ([1efeecc](https://github.com/n8n-io/n8n/commit/1efeeccc5bae306a798a66a8cf3e669ad3689262))
* **core:** Prevent node param resolution from failing telemetry graph generation ([#9257](https://github.com/n8n-io/n8n/issues/9257)) ([f6c9493](https://github.com/n8n-io/n8n/commit/f6c9493355726ddf516fb54a37adf49a2ce0efd0))
* **core:** Stop relying on filesystem for SSH keys ([#9217](https://github.com/n8n-io/n8n/issues/9217)) ([093dcef](https://github.com/n8n-io/n8n/commit/093dcefafc5a09f7622391d8b01b9aecfa9c8f2f))
* **Discord Node:** When using OAuth2 authentication, check if user is a guild member when sending direct message ([#9183](https://github.com/n8n-io/n8n/issues/9183)) ([00dfad3](https://github.com/n8n-io/n8n/commit/00dfad3279bd2a45a8331e734b331f4ab3fce75c))
* **editor:** Fix read-only mode in inline expression editor ([#9232](https://github.com/n8n-io/n8n/issues/9232)) ([99f384e](https://github.com/n8n-io/n8n/commit/99f384e2cf6b16d08a8bdc150a2833463b35f14b))
* **editor:** Prevent excess runs in manual execution with run data ([#9259](https://github.com/n8n-io/n8n/issues/9259)) ([426a12a](https://github.com/n8n-io/n8n/commit/426a12ac0ec1d637063828db008a2fb9c32ddfff))
* **editor:** Throw expression error on attempting to set variables at runtime ([#9229](https://github.com/n8n-io/n8n/issues/9229)) ([fec04d5](https://github.com/n8n-io/n8n/commit/fec04d5f796c677b6127addcb700d6442c2c3a26))
* Elaborate scope of Sustainable Use License ([#9233](https://github.com/n8n-io/n8n/issues/9233)) ([442aaba](https://github.com/n8n-io/n8n/commit/442aaba116cf0cfe7c1e7b8d570e321cc6a14143))
* **Google BigQuery Node:** Better error messages, transform timestamps ([#9255](https://github.com/n8n-io/n8n/issues/9255)) ([7ff24f1](https://github.com/n8n-io/n8n/commit/7ff24f134b706d0b5b7d7c13d3e69bd1a0f4c5b8))
* **Google Drive Node:** Create from text operation ([#9185](https://github.com/n8n-io/n8n/issues/9185)) ([d9e7494](https://github.com/n8n-io/n8n/commit/d9e74949c4db7282c3ab42bd6825aa5acc042400))
* **Jira Trigger Node:** Update credentials UI ([#9198](https://github.com/n8n-io/n8n/issues/9198)) ([ed98ca2](https://github.com/n8n-io/n8n/commit/ed98ca2fb77fc81362e6480ee6a12a64915418f9))
* **LangChain Code Node:** Fix execution of custom n8n tools called via LC code node ([#9265](https://github.com/n8n-io/n8n/issues/9265)) ([741e829](https://github.com/n8n-io/n8n/commit/741e8299d64cd774cc35ea312433f50d865f1318))
* **LangChain Code Node:** Fix resolution of scoped langchain modules ([#9258](https://github.com/n8n-io/n8n/issues/9258)) ([445c05d](https://github.com/n8n-io/n8n/commit/445c05dca46225e195ab122cf77d6d1088460e20))
* **MySQL Node:** Query to statements splitting fix ([#9207](https://github.com/n8n-io/n8n/issues/9207)) ([dc84452](https://github.com/n8n-io/n8n/commit/dc844528f4554ae41037e2c25542237a74d86f3f))
### Features
* Add Ask AI to HTTP Request Node ([#8917](https://github.com/n8n-io/n8n/issues/8917)) ([cd9bc44](https://github.com/n8n-io/n8n/commit/cd9bc44bddf7fc78acec9ee7c96a40077a07615f))
* **Gmail Node:** Add support for creating drafts using an alias ([#8728](https://github.com/n8n-io/n8n/issues/8728)) ([3986356](https://github.com/n8n-io/n8n/commit/3986356c8995998cb6ab392ae07f41efcb46d4bd))
* **Gmail Node:** Add thread option for draft emails ([#8729](https://github.com/n8n-io/n8n/issues/8729)) ([2dd0b32](https://github.com/n8n-io/n8n/commit/2dd0b329ca243de87eb1b59bf831593f70c42784))
* **Groq Chat Model Node:** Add support for Groq chat models ([#9250](https://github.com/n8n-io/n8n/issues/9250)) ([96f02bd](https://github.com/n8n-io/n8n/commit/96f02bd6552cf9ea75fcb8ba29c3afac9553aa25))
* **HTTP Request Node:** Option to provide SSL Certificates in Http Request Node ([#9125](https://github.com/n8n-io/n8n/issues/9125)) ([306b68d](https://github.com/n8n-io/n8n/commit/306b68da6bb37dbce67dcf5c4791c2986750579c))
* **Jira Software Node:** Add Wiki Markup support for Jira Cloud comments ([#8857](https://github.com/n8n-io/n8n/issues/8857)) ([756012b](https://github.com/n8n-io/n8n/commit/756012b0524e09601fada80213dd4da3057d329a))
* **Microsoft To Do Node:** Add an option to set a reminder when updating a task ([#6918](https://github.com/n8n-io/n8n/issues/6918)) ([22b2afd](https://github.com/n8n-io/n8n/commit/22b2afdd23bef2a301cd9d3743400e0d69463b1b))
* **MISP Node:** Rest search operations ([#9196](https://github.com/n8n-io/n8n/issues/9196)) ([b694e77](https://github.com/n8n-io/n8n/commit/b694e7743e17507b901706c5023a9aac83b903dd))
* **Ollama Chat Model Node:** Add aditional Ollama config parameters & fix vision ([#9215](https://github.com/n8n-io/n8n/issues/9215)) ([e17e767](https://github.com/n8n-io/n8n/commit/e17e767e700a74b187706552fc879c00fd551611))
* **Pipedrive Node:** Add busy and description options to activities ([#9208](https://github.com/n8n-io/n8n/issues/9208)) ([9b3ac16](https://github.com/n8n-io/n8n/commit/9b3ac1648f1888d79079fd50998140fd27efae97))
* **Postgres Node:** Add option IS NOT NULL and hide value input fields ([#9241](https://github.com/n8n-io/n8n/issues/9241)) ([e896889](https://github.com/n8n-io/n8n/commit/e89688939438b2d5414155f053530bd9eb34b300))
* **S3 Node:** Add support for self signed SSL certificates ([#9269](https://github.com/n8n-io/n8n/issues/9269)) ([ddff804](https://github.com/n8n-io/n8n/commit/ddff80416df87166627fdefc755e3f79102c5664))
* **Telegram Node:** Disable page preview by default ([#9267](https://github.com/n8n-io/n8n/issues/9267)) ([41ce178](https://github.com/n8n-io/n8n/commit/41ce178491135b5f972974ebecec0f5f223a71ce))
* Upgrade typeorm for separate sqlite read & write connections ([#9230](https://github.com/n8n-io/n8n/issues/9230)) ([0b52320](https://github.com/n8n-io/n8n/commit/0b523206358886d5b81d7009ce95cb9d3ba9fa40))
* **Wise Node:** Add XML as supported format in getStatement operation ([#9193](https://github.com/n8n-io/n8n/issues/9193)) ([a424b59](https://github.com/n8n-io/n8n/commit/a424b59e4949e96c0e56319cea91fcf084a5208e))
* **Wise Trigger Node:** Add support for balance updates ([#9189](https://github.com/n8n-io/n8n/issues/9189)) ([42a9891](https://github.com/n8n-io/n8n/commit/42a9891081e7f1a19364c406b056eee036180c24))
# [1.39.0](https://github.com/n8n-io/n8n/compare/n8n@1.38.0...n8n@1.39.0) (2024-04-24)

View file

@ -9,6 +9,7 @@ Great that you are here and you want to contribute to n8n
- [Code of conduct](#code-of-conduct)
- [Directory structure](#directory-structure)
- [Development setup](#development-setup)
- [Dev Container](#dev-container)
- [Requirements](#requirements)
- [Node.js](#nodejs)
- [pnpm](#pnpm)
@ -41,7 +42,6 @@ n8n is split up in different modules which are all in a single mono repository.
The most important directories:
- [/docker/image](/docker/images) - Dockerfiles to create n8n containers
- [/docker/compose](/docker/compose) - Examples Docker Setups
- [/packages](/packages) - The different n8n modules
- [/packages/cli](/packages/cli) - CLI code to run front- & backend
- [/packages/core](/packages/core) - Core code which handles workflow
@ -60,15 +60,19 @@ The most important directories:
If you want to change or extend n8n you have to make sure that all the needed
dependencies are installed and the packages get linked correctly. Here's a short guide on how that can be done:
### Dev Container
If you already have VS Code and Docker installed, you can click [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/n8n-io/n8n) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
### Requirements
#### Node.js
[Node.js](https://nodejs.org/en/) version 16.9 or newer is required for development purposes.
[Node.js](https://nodejs.org/en/) version 18.10 or newer is required for development purposes.
#### pnpm
[pnpm](https://pnpm.io/) version 8.9 or newer is required for development purposes. We recommend installing it with [corepack](#corepack).
[pnpm](https://pnpm.io/) version 9.1 or newer is required for development purposes. We recommend installing it with [corepack](#corepack).
##### pnpm workspaces

34
cypress/.eslintrc.js Normal file
View file

@ -0,0 +1,34 @@
const sharedOptions = require('@n8n_io/eslint-config/shared');
/**
* @type {import('@types/eslint').ESLint.ConfigData}
*/
module.exports = {
extends: ['@n8n_io/eslint-config/base', 'plugin:cypress/recommended'],
...sharedOptions(__dirname),
plugins: ['cypress'],
env: {
'cypress/globals': true,
},
rules: {
// TODO: remove these rules
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/promise-function-async': 'off',
'n8n-local-rules/no-uncaught-json-parse': 'off',
'cypress/no-assigning-return-values': 'warn',
'cypress/no-unnecessary-waiting': 'warn',
'cypress/unsafe-to-chain-command': 'warn',
},
};

3
cypress/.gitignore vendored Normal file
View file

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

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

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

View file

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

View file

@ -7,15 +7,15 @@ export function getManualChatModal() {
}
export function getManualChatInput() {
return cy.getByTestId('workflow-chat-input');
return getManualChatModal().get('.chat-inputs textarea');
}
export function getManualChatSendButton() {
return getManualChatModal().getByTestId('workflow-chat-send-button');
return getManualChatModal().get('.chat-input-send-button');
}
export function getManualChatMessages() {
return getManualChatModal().get('.messages .message');
return getManualChatModal().get('.chat-messages-list .chat-message');
}
export function getManualChatModalCloseButton() {

View file

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

View file

@ -0,0 +1,3 @@
export function getSaveChangesModal() {
return cy.get('.el-overlay').contains('Save changes before leaving?');
}

View file

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

View file

@ -0,0 +1,66 @@
import { CredentialsModal, WorkflowPage } from '../pages';
const workflowPage = new WorkflowPage();
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');
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 getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]');
export const getProjectSettingsNameInput = () => cy.getByTestId('project-settings-name-input');
export const getProjectSettingsSaveButton = () => cy.getByTestId('project-settings-save-button');
export const getProjectSettingsCancelButton = () =>
cy.getByTestId('project-settings-cancel-button');
export const getProjectSettingsDeleteButton = () =>
cy.getByTestId('project-settings-delete-button');
export const getProjectMembersSelect = () => cy.getByTestId('project-members-select');
export const addProjectMember = (email: string) => {
getProjectMembersSelect().click();
getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).click();
};
export const getProjectNameInput = () => cy.get('#projectName').find('input');
export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal');
export const getResourceMoveConfirmModal = () =>
cy.getByTestId('project-move-resource-confirm-modal');
export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select');
export function createProject(name: string) {
getAddProjectButton().should('be.visible').click();
getProjectNameInput()
.should('be.visible')
.should('be.focused')
.should('have.value', 'My project')
.clear()
.type(name);
getProjectSettingsSaveButton().click();
}
export function createWorkflow(fixtureKey: string, name: string) {
workflowPage.getters.workflowImportInput().selectFile(`fixtures/${fixtureKey}`, { force: true });
workflowPage.actions.setWorkflowName(name);
workflowPage.getters.saveButton().should('contain', 'Saved');
workflowPage.actions.zoomToFit();
}
export function createCredential(name: string) {
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(name);
credentialsModal.actions.save();
credentialsModal.actions.close();
}
export const actions = {
createProject: (name: string) => {
getAddProjectButton().click();
getProjectSettingsNameInput().type(name);
getProjectSettingsSaveButton().click();
},
};

View file

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

View file

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

View file

@ -35,7 +35,7 @@ export const INSTANCE_MEMBERS = [
];
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Test workflow"';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking Test workflow';
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';
@ -53,12 +53,14 @@ export const AGENT_NODE_NAME = 'AI Agent';
export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain';
export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory';
export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator';
export const AI_TOOL_CODE_NODE_NAME = 'Custom Code Tool';
export const AI_TOOL_CODE_NODE_NAME = 'Code Tool';
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
export const AI_TOOL_HTTP_NODE_NAME = 'HTTP Request Tool';
export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model';
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
export const WEBHOOK_NODE_NAME = 'Webhook';
export const META_KEY = Cypress.platform === 'darwin' ? '{meta}' : '{ctrl}';
export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl';
export const NEW_GOOGLE_ACCOUNT_NAME = 'Gmail account';
export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account';

View file

@ -18,10 +18,11 @@ module.exports = defineConfig({
screenshotOnRunFailure: true,
experimentalInteractiveRunEvents: true,
experimentalSessionAndOrigin: true,
},
env: {
MAX_PINNED_DATA_SIZE: process.env.VUE_APP_MAX_PINNED_DATA_SIZE
? parseInt(process.env.VUE_APP_MAX_PINNED_DATA_SIZE, 10)
: 16 * 1024,
specPattern: 'e2e/**/*.ts',
supportFile: 'support/e2e.ts',
fixturesFolder: 'fixtures',
downloadsFolder: 'downloads',
screenshotsFolder: 'screenshots',
videosFolder: 'videos',
},
});

View file

@ -1,6 +1,6 @@
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { v4 as uuid } from 'uuid';
import { getUniqueWorkflowName } from '../utils/workflowUtils';
const WorkflowsPage = new WorkflowsPageClass();
const WorkflowPage = new WorkflowPageClass();
@ -16,7 +16,7 @@ describe('Workflows', () => {
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_1.json', `Empty State Card Workflow ${uuid()}`);
cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
WorkflowPage.getters.workflowTags().should('contain.text', 'some-tag-1');
WorkflowPage.getters.workflowTags().should('contain.text', 'some-tag-2');
@ -27,7 +27,7 @@ describe('Workflows', () => {
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_2.json', `My New Workflow ${uuid()}`);
cy.createFixtureWorkflow('Test_workflow_2.json', getUniqueWorkflowName('My New Workflow'));
WorkflowPage.getters.workflowTags().should('contain.text', 'other-tag-1');
WorkflowPage.getters.workflowTags().should('contain.text', 'other-tag-2');

View file

@ -1,5 +1,9 @@
import { CODE_NODE_NAME, SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from './../constants';
import { SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import {
SCHEDULE_TRIGGER_NODE_NAME,
CODE_NODE_NAME,
SET_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
} from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
import { NDV } from '../pages/ndv';
@ -16,24 +20,6 @@ describe('Undo/Redo', () => {
WorkflowPage.actions.visit();
});
it('should undo/redo adding nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
});
it('should undo/redo adding connected nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
});
it('should undo/redo adding node in the middle', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -43,28 +29,28 @@ describe('Undo/Redo', () => {
SET_NODE_NAME,
);
WorkflowPage.actions.zoomToFit();
WorkflowPage.getters
.canvasNodeByName('Code')
.should('have.css', 'left', '860px')
.should('have.css', 'top', '220px');
WorkflowPage.getters.canvasNodeByName('Code').then(($codeNode) => {
const cssLeft = parseInt($codeNode.css('left'));
const cssTop = parseInt($codeNode.css('top'));
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 2);
// Last node should be added back to original position
WorkflowPage.getters
.canvasNodeByName('Code')
.should('have.css', 'left', '860px')
.should('have.css', 'top', '220px');
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 2);
// Last node should be added back to original position
WorkflowPage.getters
.canvasNodeByName('Code')
.should('have.css', 'left', cssLeft + 'px')
.should('have.css', 'top', cssTop + 'px');
});
});
it('should undo/redo deleting node using context menu', () => {
@ -118,8 +104,7 @@ describe('Undo/Redo', () => {
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.get('body').type('{esc}');
cy.get('body').type('{esc}');
WorkflowPage.actions.selectAll();
cy.get('body').type('{backspace}');
WorkflowPage.actions.hitDeleteAllNodes();
WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
@ -132,22 +117,30 @@ describe('Undo/Redo', () => {
it('should undo/redo moving nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
WorkflowPage.getters
.canvasNodeByName('Code')
.should('have.css', 'left', '740px')
.should('have.css', 'top', '320px');
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => {
const initialPosition = $node.position();
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
WorkflowPage.actions.hitUndo();
WorkflowPage.getters
.canvasNodeByName('Code')
.should('have.css', 'left', '640px')
.should('have.css', 'top', '220px');
WorkflowPage.actions.hitRedo();
WorkflowPage.getters
.canvasNodeByName('Code')
.should('have.css', 'left', '740px')
.should('have.css', 'top', '320px');
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => {
const cssLeft = parseInt($node.css('left'));
const cssTop = parseInt($node.css('top'));
expect(cssLeft).to.be.greaterThan(initialPosition.left);
expect(cssTop).to.be.greaterThan(initialPosition.top);
});
WorkflowPage.actions.hitUndo();
WorkflowPage.getters
.canvasNodeByName(CODE_NODE_NAME)
.should('have.css', 'left', `${initialPosition.left}px`)
.should('have.css', 'top', `${initialPosition.top}px`);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => {
const cssLeft = parseInt($node.css('left'));
const cssTop = parseInt($node.css('top'));
expect(cssLeft).to.be.greaterThan(initialPosition.left);
expect(cssTop).to.be.greaterThan(initialPosition.top);
});
});
});
it('should undo/redo deleting a connection using context menu', () => {
@ -204,7 +197,7 @@ describe('Undo/Redo', () => {
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.get('body').type('{esc}');
cy.get('body').type('{esc}');
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 2);
WorkflowPage.actions.hitUndo();
@ -213,21 +206,6 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.disabledNodes().should('have.length', 2);
});
it('should undo/redo renaming node using NDV', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
ndv.actions.rename(CODE_NODE_NEW_NAME);
cy.get('body').type('{esc}');
WorkflowPage.actions.hitUndo();
cy.get('body').type('{esc}');
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).should('exist');
WorkflowPage.actions.hitRedo();
cy.get('body').type('{esc}');
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NEW_NAME).should('exist');
});
it('should undo/redo renaming node using keyboard shortcut', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -289,8 +267,12 @@ describe('Undo/Redo', () => {
WorkflowPage.getters
.canvasNodes()
.first()
.should('have.css', 'left', `${initialPosition.left + 120}px`)
.should('have.css', 'top', `${initialPosition.top + 140}px`);
.then(($node) => {
const cssLeft = parseInt($node.css('left'));
const cssTop = parseInt($node.css('top'));
expect(cssLeft).to.be.greaterThan(initialPosition.left);
expect(cssTop).to.be.greaterThan(initialPosition.top);
});
// Delete the set node
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click();
@ -319,8 +301,12 @@ describe('Undo/Redo', () => {
WorkflowPage.getters
.canvasNodes()
.first()
.should('have.css', 'left', `${initialPosition.left + 120}px`)
.should('have.css', 'top', `${initialPosition.top + 140}px`);
.then(($node) => {
const cssLeft = parseInt($node.css('left'));
const cssTop = parseInt($node.css('top'));
expect(cssLeft).to.be.greaterThan(initialPosition.left);
expect(cssTop).to.be.greaterThan(initialPosition.top);
});
// Third redo: Should delete the Set node
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.length', 3);
@ -337,9 +323,6 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch'))
.should('have.css', 'left', `637px`)
.should('have.css', 'top', `501px`);
cy.fixture('Test_workflow_form_switch.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
@ -352,9 +335,6 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch'))
.should('have.css', 'left', `637px`)
.should('have.css', 'top', `501px`);
});
it('should not undo/redo when NDV or a modal is open', () => {

View file

@ -8,45 +8,46 @@ describe('Inline expression editor', () => {
beforeEach(() => {
WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Schedule');
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError');
cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
});
describe('Static data', () => {
beforeEach(() => {
WorkflowPage.actions.addNodeToCanvas('Hacker News');
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.openNode('Hacker News');
WorkflowPage.actions.openInlineExpressionEditor();
});
it('should resolve primitive resolvables', () => {
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('1 + 2');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^3$/);
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('"ab"');
WorkflowPage.getters.inlineExpressionEditorInput().type('{rightArrow}+');
WorkflowPage.getters.inlineExpressionEditorInput().type('"cd"');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^abcd$/);
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('true && false');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^false$/);
});
it('should resolve object resolvables', () => {
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters
.inlineExpressionEditorInput()
.type('{ a: 1 }', { parseSpecialCharSequences: false });
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Object: \{"a": 1\}\]$/);
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters
.inlineExpressionEditorInput()
.type('{ a: 1 }.a', { parseSpecialCharSequences: false });
@ -55,13 +56,13 @@ describe('Inline expression editor', () => {
it('should resolve array resolvables', () => {
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Array: \[1,2,3\]\]$/);
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
WorkflowPage.getters.inlineExpressionEditorInput().type('[0]');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/);
@ -75,13 +76,14 @@ describe('Inline expression editor', () => {
ndv.actions.close();
WorkflowPage.actions.addNodeToCanvas('No Operation');
WorkflowPage.actions.addNodeToCanvas('Hacker News');
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.openNode('Hacker News');
WorkflowPage.actions.openInlineExpressionEditor();
});
it('should resolve $parameter[]', () => {
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
// Resolving $parameter is slow, especially on CI runner
WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]');
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'getAll');
@ -90,19 +92,19 @@ describe('Inline expression editor', () => {
it('should resolve input: $json,$input,$(nodeName)', () => {
// Previous nodes have not run, input is empty
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr');
WorkflowPage.getters
.inlineExpressionEditorOutput()
.should('have.text', '[Execute previous nodes for preview]');
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr');
WorkflowPage.getters
.inlineExpressionEditorOutput()
.should('have.text', '[Execute previous nodes for preview]');
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters
.inlineExpressionEditorInput()
.type("$('Schedule Trigger').item.json.myStr");
@ -118,15 +120,15 @@ describe('Inline expression editor', () => {
// Previous nodes have run, input can be resolved
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr');
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday');
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr');
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday');
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters
.inlineExpressionEditorInput()
.type("$('Schedule Trigger').item.json.myStr");

View file

@ -1,3 +1,5 @@
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { successToast } from '../pages/notifications';
import {
MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
@ -7,7 +9,6 @@ import {
IF_NODE_NAME,
HTTP_REQUEST_NODE_NAME,
} from './../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass();
describe('Canvas Actions', () => {
@ -124,6 +125,8 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
WorkflowPage.actions.zoomToFit();
WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 2);
WorkflowPage.actions.addNodeBetweenNodes(
CODE_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
@ -131,12 +134,15 @@ describe('Canvas Actions', () => {
);
WorkflowPage.getters.canvasNodes().should('have.length', 4);
WorkflowPage.getters.nodeConnections().should('have.length', 3);
// And last node should be pushed to the right
WorkflowPage.getters
.canvasNodes()
.last()
.should('have.css', 'left', '860px')
.should('have.css', 'top', '220px');
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left'));
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
const httpNodeLeft = parseFloat($httpNode.css('left'));
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
});
});
});
it('should delete connections by pressing the delete button', () => {
@ -166,8 +172,8 @@ describe('Canvas Actions', () => {
.findChildByTestId('execute-node-button')
.click({ force: true });
WorkflowPage.actions.executeNode(CODE_NODE_NAME);
WorkflowPage.getters.successToast().should('have.length', 2);
WorkflowPage.getters.successToast().should('contain.text', 'Node executed successfully');
successToast().should('have.length', 2);
successToast().should('contain.text', 'Node executed successfully');
});
it('should disable and enable node', () => {
@ -198,19 +204,19 @@ describe('Canvas Actions', () => {
it('should copy selected nodes', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitCopy();
WorkflowPage.getters.successToast().should('contain', 'Copied!');
successToast().should('contain', 'Copied!');
WorkflowPage.actions.copyNode(CODE_NODE_NAME);
WorkflowPage.getters.successToast().should('contain', 'Copied!');
successToast().should('contain', 'Copied!');
});
it('should select/deselect all nodes', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.getters.selectedNodes().should('have.length', 2);
WorkflowPage.actions.deselectAll();
WorkflowPage.getters.selectedNodes().should('have.length', 0);

View file

@ -1,3 +1,5 @@
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV, WorkflowExecutionsTab } from '../pages';
import {
MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
@ -7,8 +9,6 @@ import {
SWITCH_NODE_NAME,
MERGE_NODE_NAME,
} from './../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV, WorkflowExecutionsTab } from '../pages';
const WorkflowPage = new WorkflowPageClass();
const ExecutionsTab = new WorkflowExecutionsTab();
@ -69,7 +69,9 @@ 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(600 + i * 100, 200, { force: true });
WorkflowPage.getters
.nodeViewBackground()
.click((i + 1) * 200, (i + 1) * 200, { force: true });
}
WorkflowPage.actions.zoomToFit();
@ -164,8 +166,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500);
WorkflowPage.actions.selectAll();
cy.get('body').type('{backspace}');
WorkflowPage.actions.hitDeleteAllNodes();
WorkflowPage.getters.canvasNodes().should('have.length', 0);
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
@ -181,8 +182,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500);
WorkflowPage.actions.selectAll();
cy.get('body').type('{backspace}');
WorkflowPage.actions.hitDeleteAllNodes();
WorkflowPage.getters.canvasNodes().should('have.length', 0);
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
@ -199,13 +199,23 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.zoomToFit();
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
WorkflowPage.getters
.canvasNodes()
.last()
.should('have.css', 'left', '740px')
.should('have.css', 'top', '320px');
.then(($node) => {
const { left, top } = $node.position();
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
clickToFinish: true,
});
WorkflowPage.getters
.canvasNodes()
.last()
.then(($node) => {
const { left: newLeft, top: newTop } = $node.position();
expect(newLeft).to.be.greaterThan(left);
expect(newTop).to.be.greaterThan(top);
});
});
});
it('should zoom in', () => {
@ -258,7 +268,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
// Zoom in 1x + Zoom out 1x should reset to default (=1)
WorkflowPage.getters.nodeView().should('have.css', 'transform', `matrix(1, 0, 0, 1, 0, 0)`);
WorkflowPage.getters.nodeView().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)');
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
WorkflowPage.getters
@ -315,7 +325,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.get('body').type('{esc}');
// Keyboard shortcut
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 2);
WorkflowPage.actions.hitDisableNodeShortcut();
@ -324,12 +334,12 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 1);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 2);
// Context menu
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 0);
@ -341,7 +351,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 1);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 2);
@ -364,6 +374,17 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodeByName(RENAME_NODE_NAME2).should('exist');
});
it('should not allow empty strings for node names', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodes().last().click();
cy.get('body').trigger('keydown', { key: 'F2' });
cy.get('.rename-prompt').should('be.visible');
cy.get('body').type('{backspace}');
cy.get('body').type('{enter}');
cy.get('.rename-prompt').should('contain', 'Invalid Name');
});
it('should duplicate nodes (context menu or shortcut)', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
@ -372,8 +393,8 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitDuplicateNodeShortcut();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDuplicateNode();
WorkflowPage.getters.canvasNodes().should('have.length', 5);
});

View file

@ -6,13 +6,19 @@ import {
BACKEND_BASE_URL,
} from '../constants';
import { WorkflowPage, NDV } from '../pages';
import { errorToast } from '../pages/notifications';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('Data pinning', () => {
const maxPinnedDataSize = 16384;
beforeEach(() => {
workflowPage.actions.visit();
cy.window().then((win) => {
win.maxPinnedDataSize = maxPinnedDataSize;
});
});
it('Should be able to pin node output', () => {
@ -108,6 +114,8 @@ describe('Data pinning', () => {
.parent()
.should('have.class', 'is-disabled');
cy.get('body').type('{esc}');
// Unpin using context menu
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
ndv.actions.setPinnedData([{ test: 1 }]);
@ -136,12 +144,21 @@ describe('Data pinning', () => {
ndv.actions.pastePinnedData([
{
test: '1'.repeat(Cypress.env('MAX_PINNED_DATA_SIZE')),
test: '1'.repeat(maxPinnedDataSize),
},
]);
workflowPage.getters
.errorToast()
.should('contain', 'Workflow has reached the maximum allowed pinned data size');
errorToast().should('contain', 'Workflow has reached the maximum allowed pinned data size');
});
it('Should show an error when pin data JSON in invalid', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
ndv.getters.container().should('be.visible');
ndv.getters.pinDataButton().should('not.exist');
ndv.getters.editPinnedDataButton().should('be.visible');
ndv.actions.setPinnedData('[ { "name": "First item", "code": 2dsa }]');
errorToast().should('contain', 'Unable to save due to invalid JSON');
});
it('Should be able to reference paired items in a node located before pinned data', () => {
@ -155,6 +172,7 @@ describe('Data pinning', () => {
ndv.actions.close();
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
setExpressionOnStringValueInSet(`{{ $('${HTTP_REQUEST_NODE_NAME}').item`);
const output = '[Object: {"json": {"http": 123}, "pairedItem": {"item": 0}}]';

View file

@ -1,5 +1,5 @@
import { v4 as uuid } from 'uuid';
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
import { successToast } from '../pages/notifications';
const workflowPage = new WorkflowPageClass();
const ndv = new NDV();
@ -10,13 +10,13 @@ describe('ADO-1338-ndv-missing-input-panel', () => {
});
it('should show the input and output panels when node is missing input and output data', () => {
cy.createFixtureWorkflow('Test_ado_1338.json', uuid());
cy.createFixtureWorkflow('Test_ado_1338.json');
// Execute the workflow
workflowPage.getters.zoomToFitButton().click();
workflowPage.getters.executeWorkflowButton().click();
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
workflowPage.getters.successToast().should('be.visible');
successToast().should('be.visible');
workflowPage.actions.openNode('Discourse1');
ndv.getters.inputPanel().should('be.visible');

View file

@ -1,5 +1,4 @@
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const wf = new WorkflowPage();
const ndv = new NDV();

View file

@ -1,10 +1,10 @@
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
import {
MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
SCHEDULE_TRIGGER_NODE_NAME,
} from './../constants';
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
@ -18,6 +18,8 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow-actions_paste-data.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
ndv.actions.executePrevious();
ndv.actions.switchInputMode('Table');
@ -49,6 +51,7 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
ndv.actions.switchInputMode('Table');
@ -73,6 +76,7 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.getters.parameterExpressionPreview('value').should('include.text', '0');
ndv.getters.inputTbodyCell(1, 0).realHover();
@ -110,6 +114,7 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
ndv.actions.switchInputMode('JSON');
@ -148,6 +153,7 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
ndv.actions.clearParameterInput('value');
@ -169,20 +175,26 @@ describe('Data mapping', () => {
});
it('maps expressions from previous nodes', () => {
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`);
cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow');
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set1');
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
ndv.getters.inputDataContainer().find('span').contains('count').realMouseDown();
ndv.actions.executePrevious();
ndv.actions.expandSchemaViewNode(SCHEDULE_TRIGGER_NODE_NAME);
const dataPill = ndv.getters
.inputDataContainer()
.findChildByTestId('run-data-schema-item')
.contains('count')
.should('be.visible');
dataPill.realMouseDown();
ndv.actions.mapToParameter('value');
ndv.getters
.inlineExpressionEditorInput()
.should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`);
ndv.actions.switchInputMode('Table');
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
ndv.actions.mapDataFromHeader(1, 'value');
ndv.getters
.inlineExpressionEditorInput()
@ -193,7 +205,6 @@ describe('Data mapping', () => {
ndv.actions.selectInputNode('Set');
ndv.actions.executePrevious();
ndv.getters.executingLoader().should('not.exist');
ndv.getters.inputDataContainer().should('exist');
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
@ -249,6 +260,7 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
@ -280,6 +292,7 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
ndv.actions.typeIntoParameterInput('value', 'test_value');
@ -290,14 +303,8 @@ describe('Data mapping', () => {
ndv.actions.executePrevious();
ndv.getters.executingLoader().should('not.exist');
ndv.getters.inputDataContainer().should('exist');
ndv.getters
.inputDataContainer()
.should('exist')
.find('span')
.contains('test_name')
.realMouseDown();
ndv.actions.mapToParameter('value');
ndv.actions.switchInputMode('Table');
ndv.actions.mapDataFromHeader(1, 'value');
ndv.actions.validateExpressionPreview('value', 'test_value');
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
ndv.actions.validateExpressionPreview('value', 'test_value');
@ -307,21 +314,15 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
ndv.actions.clearParameterInput('value');
cy.get('body').type('{esc}');
ndv.getters.parameterInput('includeOtherFields').find('input[type="checkbox"]').should('exist');
ndv.getters.parameterInput('includeOtherFields').find('input[type="text"]').should('not.exist');
ndv.getters
.inputDataContainer()
.should('exist')
.find('span')
.contains('count')
.realMouseDown()
.realMouseMove(100, 100);
cy.wait(50);
const pill = ndv.getters.inputDataContainer().find('span').contains('count');
pill.should('be.visible');
pill.realMouseDown();
pill.realMouseMove(100, 100);
ndv.getters
.parameterInput('includeOtherFields')
@ -332,13 +333,13 @@ describe('Data mapping', () => {
.find('input[type="text"]')
.should('exist')
.invoke('css', 'border')
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)'));
.should('include', 'dashed rgb(90, 76, 194)');
ndv.getters
.parameterInput('value')
.find('input[type="text"]')
.should('exist')
.invoke('css', 'border')
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)'));
.should('include', 'dashed rgb(90, 76, 194)');
});
});

View file

@ -1,12 +1,9 @@
import { WorkflowPage, WorkflowsPage, NDV } from '../pages';
import { BACKEND_BASE_URL } from '../constants';
import { getVisibleSelect } from '../utils';
import { WorkflowPage, NDV } from '../pages';
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('Schedule Trigger node', async () => {
describe('Schedule Trigger node', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
@ -18,49 +15,4 @@ describe('Schedule Trigger node', async () => {
ndv.getters.outputPanel().contains('timestamp');
ndv.getters.backToCanvas().click();
});
it('should execute once per second when activated', () => {
workflowPage.actions.renameWorkflow('Schedule Trigger Workflow');
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.openNode('Schedule Trigger');
cy.getByTestId('parameter-input-field').click();
getVisibleSelect().find('.option-headline').contains('Seconds').click();
cy.getByTestId('parameter-input-secondsInterval').clear().type('1');
ndv.getters.backToCanvas().click();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.activateWorkflow();
workflowPage.getters.activatorSwitch().should('have.class', 'is-checked');
cy.url().then((url) => {
const workflowId = url.split('/').pop();
cy.wait(1200);
cy.request('GET', `${BACKEND_BASE_URL}/rest/executions`).then((response) => {
expect(response.status).to.eq(200);
expect(workflowId).to.not.be.undefined;
expect(response.body.data.results.length).to.be.greaterThan(0);
const matchingExecutions = response.body.data.results.filter(
(execution: any) => execution.workflowId === workflowId,
);
expect(matchingExecutions).to.have.length(1);
cy.wait(1200);
cy.request('GET', `${BACKEND_BASE_URL}/rest/executions`).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.data.results.length).to.be.greaterThan(0);
const matchingExecutions = response.body.data.results.filter(
(execution: any) => execution.workflowId === workflowId,
);
expect(matchingExecutions).to.have.length(2);
workflowPage.actions.activateWorkflow();
workflowPage.getters.activatorSwitch().should('not.have.class', 'is-checked');
cy.visit(workflowsPage.url);
workflowsPage.actions.deleteWorkFlow('Schedule Trigger Workflow');
});
});
});
});
});

View file

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

View file

@ -1,4 +1,4 @@
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../constants';
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN, NOTION_NODE_NAME } from '../constants';
import {
CredentialsModal,
CredentialsPage,
@ -8,6 +8,7 @@ import {
WorkflowsPage,
} from '../pages';
import { getVisibleSelect } from '../utils';
import * as projects from '../composables/projects';
/**
* User U1 - Instance owner
@ -30,11 +31,11 @@ const workflowSharingModal = new WorkflowSharingModal();
const ndv = new NDV();
describe('Sharing', { disableAutoLogin: true }, () => {
before(() => cy.enableFeature('sharing', true));
before(() => cy.enableFeature('sharing'));
let workflowW2Url = '';
it('should create C1, W1, W2, share W1 with U3, as U2', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
@ -67,7 +68,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should create C2, share C2 with U1 and U2, as U3', () => {
cy.signin(INSTANCE_MEMBERS[1]);
cy.signinAsMember(1);
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
@ -83,7 +84,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should open W1, add node using C2 as U3', () => {
cy.signin(INSTANCE_MEMBERS[1]);
cy.signinAsMember(1);
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 1);
@ -99,7 +100,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should open W1, add node using C2 as U2', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
@ -119,7 +120,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should not have access to W2, as U3', () => {
cy.signin(INSTANCE_MEMBERS[1]);
cy.signinAsMember(1);
cy.visit(workflowW2Url);
cy.waitForLoad();
@ -128,13 +129,17 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should have access to W1, W2, as U1', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCard('Workflow W1').click();
workflowPage.actions.openNode('Notion');
ndv.getters.credentialInput().should('have.value', 'Credential C1').should('be.disabled');
ndv.getters
.credentialInput()
.find('input')
.should('have.value', 'Credential C1')
.should('be.enabled');
ndv.actions.close();
cy.waitForLoad();
@ -144,7 +149,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should automatically test C2 when opened by U2 sharee', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(credentialsPage.url);
credentialsPage.getters.credentialCard('Credential C2').click();
@ -152,7 +157,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should work for admin role on credentials created by others (also can share it with themselves)', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(credentialsPage.url);
credentialsPage.getters.createCredentialButton().click();
@ -164,18 +169,18 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsModal.actions.close();
cy.signout();
cy.signin(INSTANCE_ADMIN);
cy.signinAsAdmin();
cy.visit(credentialsPage.url);
credentialsPage.getters.credentialCard('Credential C3').click();
credentialsModal.getters.testSuccessTag().should('be.visible');
cy.get('input').should('not.have.length');
credentialsModal.actions.changeTab('Sharing');
cy.contains(
'You can view this credential because you have permission to read and share',
'Sharing a credential allows people to use it in their workflows. They cannot access credential details.',
).should('be.visible');
credentialsModal.getters.usersSelect().click();
cy.getByTestId('user-email')
cy.getByTestId('project-sharing-info')
.filter(':visible')
.should('have.length', 3)
.contains(INSTANCE_ADMIN.email)
@ -188,3 +193,146 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsModal.actions.close();
});
});
describe('Credential Usage in Cross Shared Workflows', () => {
beforeEach(() => {
cy.resetDatabase();
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
cy.reload();
cy.signinAsOwner();
cy.visit(credentialsPage.url);
});
it('should only show credentials from the same team project', () => {
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
// Create a notion credential in the home project
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
// Create a notion credential in one project
projects.actions.createProject('Development');
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
// Create a notion credential in another project
projects.actions.createProject('Test');
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
// Create a workflow with a notion node in the same project
projects.getProjectTabWorkflows().click();
workflowsPage.actions.createWorkflowFromCard();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Only the credential in this project (+ the 'Create new' option) should
// be in the dropdown
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 2);
});
it('should only show credentials in their personal project for members if the workflow was shared with them', () => {
const workflowName = 'Test workflow';
cy.enableFeature('sharing');
cy.reload();
// Create a notion credential as the owner and a workflow that is shared
// with member 0
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
//cy.visit(workflowsPage.url);
projects.getProjectTabWorkflows().click();
workflowsPage.actions.createWorkflowFromCard();
workflowPage.actions.setWorkflowName(workflowName);
workflowPage.actions.openShareModal();
workflowSharingModal.actions.addUser(INSTANCE_MEMBERS[0].email);
workflowSharingModal.actions.save();
// As the member, create a new notion credential
cy.signinAsMember();
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard(workflowName).click();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Only the own credential the shared one (+ the 'Create new' option)
// should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 2);
});
it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
const workflowName = 'Test workflow';
cy.enableFeature('sharing');
// As member 1, create a new notion credential. This should not show up.
cy.signinAsMember(1);
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
// As admin, create a new notion credential. This should show up.
cy.signinAsAdmin();
cy.visit(credentialsPage.url);
credentialsPage.getters.createCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
// As member 0, create a new notion credential and a workflow and share it
// with the global owner and the admin.
cy.signinAsMember();
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.actions.createWorkflowFromCard();
workflowPage.actions.setWorkflowName(workflowName);
workflowPage.actions.openShareModal();
workflowSharingModal.actions.addUser(INSTANCE_OWNER.email);
workflowSharingModal.actions.addUser(INSTANCE_ADMIN.email);
workflowSharingModal.actions.save();
// As the global owner, create a new notion credential and open the shared
// workflow
cy.signinAsOwner();
cy.visit(credentialsPage.url);
credentialsPage.getters.createCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard(workflowName).click();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Only the personal credentials of the workflow owner and the global owner
// should show up.
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 4);
});
it('should show all personal credentials if the global owner owns the workflow', () => {
cy.enableFeature('sharing');
// As member 0, create a new notion credential.
cy.signinAsMember();
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
// As the global owner, create a workflow and add a notion node
cy.signinAsOwner();
cy.visit(workflowsPage.url);
workflowsPage.actions.createWorkflowFromCard();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Show all personal credentials
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.have.length', 2);
});
});

View file

@ -51,34 +51,13 @@ describe('Workflow tags', () => {
wf.getters.tagPills().should('have.length', 0); // none attached
});
it('should update a tag via modal', () => {
wf.actions.openTagManagerModal();
const [first] = TEST_TAGS;
cy.contains('Create a tag').click();
cy.getByTestId('tags-table').find('input').type(first).type('{enter}');
cy.getByTestId('tags-table').should('contain.text', first);
cy.getByTestId('edit-tag-button').eq(-1).click({ force: true });
cy.wait(300);
cy.getByTestId('tags-table')
.find('.el-input--large')
.should('be.visible')
.type(' Updated')
.type('{enter}');
cy.contains('Done').click();
wf.getters.createTagButton().click();
wf.getters.tagsInDropdown().should('have.length', 1); // one stored
wf.getters.tagsInDropdown().contains('Updated').should('exist');
wf.getters.tagPills().should('have.length', 0); // none attached
});
it('should detach a tag inline by clicking on X on tag pill', () => {
wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS);
wf.getters.nthTagPill(1).click();
wf.getters.tagsDropdown().find('.el-tag__close').first().click();
cy.get('body').click(0, 0);
wf.getters.workflowTags().click();
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
});
@ -88,6 +67,7 @@ describe('Workflow tags', () => {
wf.getters.nthTagPill(1).click();
wf.getters.tagsInDropdown().filter('.selected').first().click();
cy.get('body').click(0, 0);
wf.getters.workflowTags().click();
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
});
});

View file

@ -1,7 +1,8 @@
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../constants';
import { MainSidebar, SettingsSidebar, SettingsUsersPage, WorkflowPage } from '../pages';
import { MainSidebar, SettingsSidebar, SettingsUsersPage } from '../pages';
import { PersonalSettingsPage } from '../pages/settings-personal';
import { getVisibleSelect } from '../utils';
import { errorToast, successToast } from '../pages/notifications';
/**
* User A - Instance owner
@ -24,7 +25,6 @@ const updatedPersonalData = {
};
const usersSettingsPage = new SettingsUsersPage();
const workflowPage = new WorkflowPage();
const personalSettingsPage = new PersonalSettingsPage();
const settingsSidebar = new SettingsSidebar();
const mainSidebar = new MainSidebar();
@ -34,6 +34,27 @@ describe('User Management', { disableAutoLogin: true }, () => {
cy.enableFeature('sharing');
});
it('should login and logout', () => {
cy.visit('/');
cy.get('input[name="email"]').type(INSTANCE_OWNER.email);
cy.get('input[name="password"]').type(INSTANCE_OWNER.password);
cy.getByTestId('form-submit-button').click();
mainSidebar.getters.logo().should('be.visible');
mainSidebar.actions.goToSettings();
settingsSidebar.getters.users().should('be.visible');
mainSidebar.actions.closeSettings();
mainSidebar.actions.openUserMenu();
cy.getByTestId('user-menu-item-logout').click();
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
cy.getByTestId('form-submit-button').click();
mainSidebar.getters.logo().should('be.visible');
mainSidebar.actions.goToSettings();
cy.getByTestId('menu-item').filter('#settings-users').should('not.exist');
});
it('should prevent non-owners to access UM settings', () => {
usersSettingsPage.actions.loginAndVisit(
INSTANCE_MEMBERS[0].email,
@ -153,7 +174,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
usersSettingsPage.getters.deleteDataRadioButton().click();
usersSettingsPage.getters.deleteDataInput().type('delete all data');
usersSettingsPage.getters.deleteUserButton().click();
workflowPage.getters.successToast().should('contain', 'User deleted');
successToast().should('contain', 'User deleted');
});
it('should delete user and transfer their data', () => {
@ -163,10 +184,10 @@ describe('User Management', { disableAutoLogin: true }, () => {
usersSettingsPage.getters.userSelectDropDown().click();
usersSettingsPage.getters.userSelectOptions().first().click();
usersSettingsPage.getters.deleteUserButton().click();
workflowPage.getters.successToast().should('contain', 'User deleted');
successToast().should('contain', 'User deleted');
});
it(`should allow user to change their personal data`, () => {
it('should allow user to change their personal data', () => {
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.actions.updateFirstAndLastName(
updatedPersonalData.newFirstName,
@ -175,42 +196,39 @@ describe('User Management', { disableAutoLogin: true }, () => {
personalSettingsPage.getters
.currentUserName()
.should('contain', `${updatedPersonalData.newFirstName} ${updatedPersonalData.newLastName}`);
workflowPage.getters.successToast().should('contain', 'Personal details updated');
successToast().should('contain', 'Personal details updated');
});
it(`shouldn't allow user to set weak password`, () => {
it("shouldn't allow user to set weak password", () => {
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.getters.changePasswordLink().click();
for (let weakPass of updatedPersonalData.invalidPasswords) {
for (const weakPass of updatedPersonalData.invalidPasswords) {
personalSettingsPage.actions.tryToSetWeakPassword(INSTANCE_OWNER.password, weakPass);
}
});
it(`shouldn't allow user to change password if old password is wrong`, () => {
it("shouldn't allow user to change password if old password is wrong", () => {
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.getters.changePasswordLink().click();
personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword);
workflowPage.getters
.errorToast()
.closest('div')
.should('contain', 'Provided current password is incorrect.');
errorToast().closest('div').should('contain', 'Provided current password is incorrect.');
});
it(`should change current user password`, () => {
it('should change current user password', () => {
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.getters.changePasswordLink().click();
personalSettingsPage.actions.updatePassword(
INSTANCE_OWNER.password,
updatedPersonalData.newPassword,
);
workflowPage.getters.successToast().should('contain', 'Password updated');
successToast().should('contain', 'Password updated');
personalSettingsPage.actions.loginWithNewData(
INSTANCE_OWNER.email,
updatedPersonalData.newPassword,
);
});
it(`shouldn't allow users to set invalid email`, () => {
it("shouldn't allow users to set invalid email", () => {
personalSettingsPage.actions.loginAndVisit(
INSTANCE_OWNER.email,
updatedPersonalData.newPassword,
@ -221,13 +239,13 @@ describe('User Management', { disableAutoLogin: true }, () => {
personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('.')[0]);
});
it(`should change user email`, () => {
it('should change user email', () => {
personalSettingsPage.actions.loginAndVisit(
INSTANCE_OWNER.email,
updatedPersonalData.newPassword,
);
personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail);
workflowPage.getters.successToast().should('contain', 'Personal details updated');
successToast().should('contain', 'Personal details updated');
personalSettingsPage.actions.loginWithNewData(
updatedPersonalData.newEmail,
updatedPersonalData.newPassword,

View file

@ -1,6 +1,6 @@
import { v4 as uuid } from 'uuid';
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { clearNotifications, errorToast, successToast } from '../pages/notifications';
const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();
@ -12,7 +12,7 @@ describe('Execution', () => {
});
it('should test manual workflow', () => {
cy.createFixtureWorkflow('Manual_wait_set.json', `Manual wait set ${uuid()}`);
cy.createFixtureWorkflow('Manual_wait_set.json');
// Check workflow buttons
workflowPage.getters.executeWorkflowButton().should('be.visible');
@ -62,17 +62,17 @@ describe('Execution', () => {
.within(() => cy.get('.fa-check'))
.should('exist');
successToast().should('be.visible');
clearNotifications();
// Clear execution data
workflowPage.getters.clearExecutionDataButton().should('be.visible');
workflowPage.getters.clearExecutionDataButton().click();
workflowPage.getters.clearExecutionDataButton().should('not.exist');
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
workflowPage.getters.successToast().should('be.visible');
});
it('should test manual workflow stop', () => {
cy.createFixtureWorkflow('Manual_wait_set.json', `Manual wait set ${uuid()}`);
cy.createFixtureWorkflow('Manual_wait_set.json');
// Check workflow buttons
workflowPage.getters.executeWorkflowButton().should('be.visible');
@ -106,6 +106,9 @@ describe('Execution', () => {
.canvasNodeByName('Set')
.within(() => cy.get('.fa-check').should('not.exist'));
successToast().should('be.visible');
clearNotifications();
workflowPage.getters.stopExecutionButton().should('exist');
workflowPage.getters.stopExecutionButton().click();
@ -121,17 +124,17 @@ describe('Execution', () => {
.canvasNodeByName('Set')
.within(() => cy.get('.fa-check').should('not.exist'));
successToast().should('be.visible');
clearNotifications();
// Clear execution data
workflowPage.getters.clearExecutionDataButton().should('be.visible');
workflowPage.getters.clearExecutionDataButton().click();
workflowPage.getters.clearExecutionDataButton().should('not.exist');
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
workflowPage.getters.successToast().should('be.visible');
});
it('should test webhook workflow', () => {
cy.createFixtureWorkflow('Webhook_wait_set.json', `Webhook wait set ${uuid()}`);
cy.createFixtureWorkflow('Webhook_wait_set.json');
// Check workflow buttons
workflowPage.getters.executeWorkflowButton().should('be.visible');
@ -194,17 +197,17 @@ describe('Execution', () => {
.within(() => cy.get('.fa-check'))
.should('exist');
successToast().should('be.visible');
clearNotifications();
// Clear execution data
workflowPage.getters.clearExecutionDataButton().should('be.visible');
workflowPage.getters.clearExecutionDataButton().click();
workflowPage.getters.clearExecutionDataButton().should('not.exist');
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
workflowPage.getters.successToast().should('be.visible');
});
it('should test webhook workflow stop', () => {
cy.createFixtureWorkflow('Webhook_wait_set.json', `Webhook wait set ${uuid()}`);
cy.createFixtureWorkflow('Webhook_wait_set.json');
// Check workflow buttons
workflowPage.getters.executeWorkflowButton().should('be.visible');
@ -239,6 +242,9 @@ describe('Execution', () => {
});
});
successToast().should('be.visible');
clearNotifications();
workflowPage.getters.stopExecutionButton().click();
// Check canvas nodes after 1st step (workflow passed the manual trigger node
workflowPage.getters
@ -268,13 +274,13 @@ describe('Execution', () => {
.canvasNodeByName('Set')
.within(() => cy.get('.fa-check').should('not.exist'));
successToast().should('be.visible');
clearNotifications();
// Clear execution data
workflowPage.getters.clearExecutionDataButton().should('be.visible');
workflowPage.getters.clearExecutionDataButton().click();
workflowPage.getters.clearExecutionDataButton().should('not.exist');
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
workflowPage.getters.successToast().should('be.visible');
});
describe('execution preview', () => {
@ -286,13 +292,13 @@ describe('Execution', () => {
executionsTab.actions.deleteExecutionInPreview();
executionsTab.getters.successfulExecutionListItems().should('have.length', 0);
workflowPage.getters.successToast().contains('Execution deleted');
successToast().contains('Execution deleted');
});
});
describe('connections should be colored differently for pinned data', () => {
beforeEach(() => {
cy.createFixtureWorkflow('Schedule_pinned.json', `Schedule pinned ${uuid()}`);
cy.createFixtureWorkflow('Schedule_pinned.json');
workflowPage.actions.deselectAll();
workflowPage.getters.zoomToFitButton().click();
@ -491,17 +497,14 @@ describe('Execution', () => {
});
it('should send proper payload for node rerun', () => {
cy.createFixtureWorkflow(
'Multiple_trigger_node_rerun.json',
`Multiple trigger node rerun ${uuid()}`,
);
cy.createFixtureWorkflow('Multiple_trigger_node_rerun.json', 'Multiple trigger node rerun');
workflowPage.getters.zoomToFitButton().click();
workflowPage.getters.executeWorkflowButton().click();
workflowPage.getters.clearExecutionDataButton().should('be.visible');
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
workflowPage.getters
.canvasNodeByName('do something with them')
@ -510,22 +513,20 @@ describe('Execution', () => {
cy.wait('@workflowRun').then((interception) => {
expect(interception.request.body).to.have.property('runData').that.is.an('object');
const expectedKeys = ['When clicking "Test workflow"', 'fetch 5 random users'];
const expectedKeys = ['When clicking Test workflow', 'fetch 5 random users'];
expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(expectedKeys.length);
expect(interception.request.body.runData).to.include.all.keys(expectedKeys);
const { runData } = interception.request.body as Record<string, object>;
expect(Object.keys(runData)).to.have.lengthOf(expectedKeys.length);
expect(runData).to.include.all.keys(expectedKeys);
});
});
it('should send proper payload for manual node run', () => {
cy.createFixtureWorkflow(
'Check_manual_node_run_for_pinned_and_rundata.json',
`Check manual node run for pinned and rundata ${uuid()}`,
);
cy.createFixtureWorkflow('Check_manual_node_run_for_pinned_and_rundata.json');
workflowPage.getters.zoomToFitButton().click();
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
workflowPage.getters
.canvasNodeByName('If')
@ -534,18 +535,20 @@ describe('Execution', () => {
cy.wait('@workflowRun').then((interception) => {
expect(interception.request.body).not.to.have.property('runData').that.is.an('object');
expect(interception.request.body).to.have.property('pinData').that.is.an('object');
expect(interception.request.body).to.have.property('workflowData').that.is.an('object');
expect(interception.request.body.workflowData)
.to.have.property('pinData')
.that.is.an('object');
const expectedPinnedDataKeys = ['Webhook'];
expect(Object.keys(interception.request.body.pinData)).to.have.lengthOf(
expectedPinnedDataKeys.length,
);
expect(interception.request.body.pinData).to.include.all.keys(expectedPinnedDataKeys);
const { pinData } = interception.request.body.workflowData as Record<string, object>;
expect(Object.keys(pinData)).to.have.lengthOf(expectedPinnedDataKeys.length);
expect(pinData).to.include.all.keys(expectedPinnedDataKeys);
});
workflowPage.getters.clearExecutionDataButton().should('be.visible');
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
workflowPage.getters
.canvasNodeByName('NoOp2')
@ -554,29 +557,27 @@ describe('Execution', () => {
cy.wait('@workflowRun').then((interception) => {
expect(interception.request.body).to.have.property('runData').that.is.an('object');
expect(interception.request.body).to.have.property('pinData').that.is.an('object');
expect(interception.request.body).to.have.property('workflowData').that.is.an('object');
expect(interception.request.body.workflowData)
.to.have.property('pinData')
.that.is.an('object');
const expectedPinnedDataKeys = ['Webhook'];
const expectedRunDataKeys = ['If', 'Webhook'];
expect(Object.keys(interception.request.body.pinData)).to.have.lengthOf(
expectedPinnedDataKeys.length,
);
expect(interception.request.body.pinData).to.include.all.keys(expectedPinnedDataKeys);
const { pinData } = interception.request.body.workflowData as Record<string, object>;
expect(Object.keys(pinData)).to.have.lengthOf(expectedPinnedDataKeys.length);
expect(pinData).to.include.all.keys(expectedPinnedDataKeys);
expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(
expectedRunDataKeys.length,
);
expect(interception.request.body.runData).to.include.all.keys(expectedRunDataKeys);
const { runData } = interception.request.body as Record<string, object>;
expect(Object.keys(runData)).to.have.lengthOf(expectedRunDataKeys.length);
expect(runData).to.include.all.keys(expectedRunDataKeys);
});
});
it('should successfully execute partial executions with nodes attached to the second output', () => {
cy.createFixtureWorkflow(
'Test_Workflow_pairedItem_incomplete_manual_bug.json',
'My test workflow',
);
cy.createFixtureWorkflow('Test_Workflow_pairedItem_incomplete_manual_bug.json');
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
workflowPage.getters.zoomToFitButton().click();
workflowPage.getters.executeWorkflowButton().click();
@ -590,16 +591,13 @@ describe('Execution', () => {
cy.wait('@workflowRun');
// Wait again for the websocket message to arrive and the UI to update.
cy.wait(100);
workflowPage.getters.errorToast({ timeout: 1 }).should('not.exist');
errorToast({ timeout: 1 }).should('not.exist');
});
it('should execute workflow partially up to the node that has issues', () => {
cy.createFixtureWorkflow(
'Test_workflow_partial_execution_with_missing_credentials.json',
'My test workflow',
);
cy.createFixtureWorkflow('Test_workflow_partial_execution_with_missing_credentials.json');
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
workflowPage.getters.zoomToFitButton().click();
workflowPage.getters.executeWorkflowButton().click();
@ -617,6 +615,6 @@ describe('Execution', () => {
.within(() => cy.get('.fa-check'))
.should('exist');
workflowPage.getters.errorToast().should('contain', `Problem in node Telegram`);
errorToast().should('contain', 'Problem in node Telegram');
});
});

View file

@ -1,4 +1,7 @@
import { type ICredentialType } from 'n8n-workflow';
import {
AGENT_NODE_NAME,
AI_TOOL_HTTP_NODE_NAME,
GMAIL_NODE_NAME,
HTTP_REQUEST_NODE_NAME,
NEW_GOOGLE_ACCOUNT_NAME,
@ -11,6 +14,7 @@ import {
TRELLO_NODE_NAME,
} from '../constants';
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
import { successToast } from '../pages/notifications';
import { getVisibleSelect } from '../utils';
const credentialsPage = new CredentialsPage();
@ -19,6 +23,7 @@ const workflowPage = new WorkflowPage();
const nodeDetailsView = new NDV();
const NEW_CREDENTIAL_NAME = 'Something else';
const NEW_CREDENTIAL_NAME2 = 'Something else entirely';
describe('Credentials', () => {
beforeEach(() => {
@ -42,39 +47,6 @@ describe('Credentials', () => {
credentialsPage.getters.credentialCards().should('have.length', 1);
});
it.skip('should create a new credential using Add Credential button', () => {
credentialsPage.getters.createCredentialButton().click();
credentialsModal.getters.newCredentialModal().should('be.visible');
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Airtable API').click();
credentialsModal.getters.newCredentialTypeButton().click();
credentialsModal.getters.editCredentialModal().should('be.visible');
credentialsModal.getters.connectionParameter('API Key').type('1234567890');
credentialsModal.actions.setName('Airtable Account');
credentialsModal.actions.save();
credentialsModal.actions.close();
credentialsPage.getters.credentialCards().should('have.length', 2);
});
it.skip('should search credentials', () => {
// Search by name
credentialsPage.actions.search('Notion');
credentialsPage.getters.credentialCards().should('have.length', 1);
// Search by Credential type
credentialsPage.actions.search('Airtable API');
credentialsPage.getters.credentialCards().should('have.length', 1);
// No results
credentialsPage.actions.search('Google');
credentialsPage.getters.credentialCards().should('have.length', 0);
credentialsPage.getters.emptyList().should('be.visible');
});
it('should sort credentials', () => {
credentialsPage.actions.search('');
credentialsPage.actions.sortBy('nameDesc');
@ -185,7 +157,7 @@ describe('Credentials', () => {
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.deleteButton().click();
cy.get('.el-message-box').find('button').contains('Yes').click();
workflowPage.getters.successToast().contains('Credential deleted');
successToast().contains('Credential deleted');
workflowPage.getters
.nodeCredentialsSelect()
.find('input')
@ -211,6 +183,49 @@ describe('Credentials', () => {
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_CREDENTIAL_NAME);
// Reload page to make sure this also works when the credential hasn't been
// just created.
nodeDetailsView.actions.close();
workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload();
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsEditButton().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.name().click();
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME2);
credentialsModal.getters.saveButton().click();
credentialsModal.getters.closeButton().click();
workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_CREDENTIAL_NAME2);
});
it('should edit credential for non-standard credential type', () => {
workflowPage.actions.visit();
workflowPage.actions.addNodeToCanvas(AGENT_NODE_NAME);
workflowPage.actions.addNodeToCanvas(AI_TOOL_HTTP_NODE_NAME);
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
cy.getByTestId('parameter-input-authentication').click();
cy.contains('Predefined Credential Type').click();
cy.getByTestId('credential-select').click();
cy.contains('Adalo API').click();
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
credentialsModal.actions.fillCredentialsForm();
workflowPage.getters.nodeCredentialsEditButton().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.name().click();
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME);
credentialsModal.getters.saveButton().click();
credentialsModal.getters.closeButton().click();
workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_CREDENTIAL_NAME);
});
it('should setup generic authentication for HTTP node', () => {
@ -242,7 +257,7 @@ describe('Credentials', () => {
req.headers['cache-control'] = 'no-cache, no-store';
req.on('response', (res) => {
const credentials = res.body || [];
const credentials: ICredentialType[] = res.body || [];
const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api');
@ -254,8 +269,9 @@ describe('Credentials', () => {
});
workflowPage.actions.visit(true);
workflowPage.actions.addNodeToCanvas('Slack');
workflowPage.actions.openNode('Slack');
workflowPage.actions.addNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Slack', true, true, 'Get a channel');
workflowPage.getters.nodeCredentialsSelect().should('exist');
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();

View file

@ -1,6 +1,8 @@
import type { RouteHandler } from 'cypress/types/net-stubbing';
import { WorkflowPage } from '../pages';
import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
import type { RouteHandler } from 'cypress/types/net-stubbing';
import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage();
const executionsTab = new WorkflowExecutionsTab();
@ -10,7 +12,7 @@ const executionsRefreshInterval = 4000;
describe('Current Workflow Executions', () => {
beforeEach(() => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`);
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
});
it('should render executions tab correctly', () => {
@ -57,8 +59,8 @@ describe('Current Workflow Executions', () => {
});
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
const throttleResponse: RouteHandler = (req) => {
return new Promise((resolve) => {
const throttleResponse: RouteHandler = async (req) => {
return await new Promise((resolve) => {
setTimeout(() => resolve(req.continue()), 2000);
});
};
@ -71,6 +73,160 @@ describe('Current Workflow Executions', () => {
cy.wait(executionsRefreshInterval);
cy.url().should('not.include', '/executions');
});
it('should error toast when server error message returned without stack trace', () => {
executionsTab.actions.createManualExecutions(1);
const message = 'Workflow did not finish, possible out-of-memory issue';
cy.intercept('GET', '/rest/executions/*', {
statusCode: 200,
body: executionOutOfMemoryServerResponse,
}).as('getExecution');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecution']);
executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body') // Access the body of the iframe document
.should('not.be.empty') // Ensure the body is not empty
.then(cy.wrap)
.find('.el-notification:has(.el-notification--error)')
.should('be.visible')
.filter(`:contains("${message}")`)
.should('be.visible');
});
it('should show workflow data in executions tab after hard reload and modify name and tags', () => {
executionsTab.actions.switchToExecutionsTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
workflowPage.getters.workflowTags().click();
getVisibleSelect().find('li:contains("Manage tags")').click();
cy.get('button:contains("Add new")').click();
cy.getByTestId('tags-table').find('input').type('nutag').type('{enter}');
cy.get('button:contains("Done")').click();
cy.reload();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.workflowTags().click();
workflowPage.getters.tagsInDropdown().first().should('have.text', 'nutag').click();
workflowPage.getters.tagPills().should('have.length', 3);
let newWorkflowName = 'Renamed workflow';
workflowPage.actions.renameWorkflow(newWorkflowName);
workflowPage.getters.isWorkflowSaved();
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToEditorTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 3);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToExecutionsTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 3);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToEditorTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 3);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
newWorkflowName = 'New workflow';
workflowPage.actions.renameWorkflow(newWorkflowName);
workflowPage.getters.isWorkflowSaved();
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
workflowPage.getters.workflowTags().click();
workflowPage.getters.tagsDropdown().find('.el-tag__close').first().click();
cy.get('body').click(0, 0);
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
executionsTab.actions.switchToExecutionsTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToEditorTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
});
it('should load items and auto scroll after filter change', () => {
createMockExecutions();
createMockExecutions();
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions']);
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
executionsTab.getters.executionListItems().eq(10).click();
cy.getByTestId('executions-filter-button').click();
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
getVisibleSelect().find('li:contains("Error")').click();
executionsTab.getters.executionListItems().should('have.length', 5);
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
executionsTab.getters.failedExecutionListItems().should('have.length', 4);
cy.getByTestId('executions-filter-button').click();
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
getVisibleSelect().find('li:contains("Success")').click();
// check if the list is scrolled
executionsTab.getters.executionListItems().eq(10).should('be.visible');
executionsTab.getters.executionsList().then(($el) => {
const { scrollTop, scrollHeight, clientHeight } = $el[0];
expect(scrollTop).to.be.greaterThan(0);
expect(scrollTop + clientHeight).to.be.lessThan(scrollHeight);
// scroll to the bottom
$el[0].scrollTo(0, scrollHeight);
executionsTab.getters.executionListItems().should('have.length', 18);
executionsTab.getters.successfulExecutionListItems().should('have.length', 18);
executionsTab.getters.failedExecutionListItems().should('have.length', 0);
});
cy.getByTestId('executions-filter-button').click();
cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
executionsTab.getters.executionListItems().eq(11).should('be.visible');
});
});
const createMockExecutions = () => {
@ -82,3 +238,10 @@ const createMockExecutions = () => {
executionsTab.actions.toggleNodeEnabled('Error');
executionsTab.actions.createManualExecutions(4);
};
const checkMainHeaderELements = () => {
workflowPage.getters.workflowNameInputContainer().should('be.visible');
workflowPage.getters.workflowTagsContainer().should('be.visible');
workflowPage.getters.workflowMenu().should('be.visible');
workflowPage.getters.saveButton().should('be.visible');
};

View file

@ -1,3 +1,4 @@
import type { ICredentialType } from 'n8n-workflow';
import { NodeCreator } from '../pages/features/node-creator';
import CustomNodeFixture from '../fixtures/Custom_node.json';
import { CredentialsModal, WorkflowPage } from '../pages';
@ -5,6 +6,13 @@ import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_cred
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
import CustomCredential from '../fixtures/Custom_credential.json';
import { getVisibleSelect } from '../utils';
import {
confirmCommunityNodeUninstall,
confirmCommunityNodeUpdate,
getCommunityCards,
installFirstCommunityNode,
visitCommunityNodesSettings,
} from '../pages/settings-community-nodes';
const credentialsModal = new CredentialsModal();
const nodeCreatorFeature = new NodeCreator();
@ -13,7 +21,7 @@ const workflowPage = new WorkflowPage();
// We separate-out the custom nodes because they require injecting nodes and credentials
// so the /nodes and /credentials endpoints are intercepted and non-cached.
// We want to keep the other tests as fast as possible so we don't want to break the cache in those.
describe('Community Nodes', () => {
describe('Community and custom nodes in canvas', () => {
beforeEach(() => {
cy.intercept('/types/nodes.json', { middleware: true }, (req) => {
req.headers['cache-control'] = 'no-cache, no-store';
@ -33,9 +41,9 @@ describe('Community Nodes', () => {
req.headers['cache-control'] = 'no-cache, no-store';
req.on('response', (res) => {
const credentials = res.body || [];
const credentials: ICredentialType[] = res.body || [];
credentials.push(CustomCredential);
credentials.push(CustomCredential as ICredentialType);
});
});
@ -94,3 +102,89 @@ describe('Community Nodes', () => {
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
});
});
describe('Community nodes', () => {
const mockPackage = {
createdAt: '2024-07-22T19:08:06.505Z',
updatedAt: '2024-07-22T19:08:06.505Z',
packageName: 'n8n-nodes-chatwork',
installedVersion: '1.0.0',
authorName: null,
authorEmail: null,
installedNodes: [
{
name: 'Chatwork',
type: 'n8n-nodes-chatwork.chatwork',
latestVersion: 1,
},
],
updateAvailable: '1.1.2',
};
it('can install, update and uninstall community nodes', () => {
cy.intercept(
{
hostname: 'api.npms.io',
pathname: '/v2/search',
query: { q: 'keywords:n8n-community-node-package' },
},
{ body: {} },
);
cy.intercept(
{ method: 'GET', pathname: '/rest/community-packages', times: 1 },
{
body: { data: [] },
},
).as('getEmptyPackages');
visitCommunityNodesSettings();
cy.wait('@getEmptyPackages');
// install a package
cy.intercept(
{ method: 'POST', pathname: '/rest/community-packages', times: 1 },
{
body: { data: mockPackage },
},
).as('installPackage');
cy.intercept(
{ method: 'GET', pathname: '/rest/community-packages', times: 1 },
{
body: { data: [mockPackage] },
},
).as('getPackages');
installFirstCommunityNode('n8n-nodes-chatwork@1.0.0');
cy.wait('@installPackage');
cy.wait('@getPackages');
getCommunityCards().should('have.length', 1);
getCommunityCards().eq(0).should('include.text', 'v1.0.0');
// update the package
cy.intercept(
{ method: 'PATCH', pathname: '/rest/community-packages' },
{
body: { data: { ...mockPackage, installedVersion: '1.2.0', updateAvailable: undefined } },
},
).as('updatePackage');
getCommunityCards().eq(0).find('button').click();
confirmCommunityNodeUpdate();
cy.wait('@updatePackage');
getCommunityCards().should('have.length', 1);
getCommunityCards().eq(0).should('not.include.text', 'v1.0.0');
// uninstall the package
cy.intercept(
{
method: 'DELETE',
pathname: '/rest/community-packages',
query: { name: 'n8n-nodes-chatwork' },
},
{ statusCode: 204 },
).as('uninstallPackage');
getCommunityCards().getByTestId('action-toggle').click();
cy.getByTestId('action-uninstall').click();
confirmCommunityNodeUninstall();
cy.wait('@uninstallPackage');
cy.getByTestId('action-box').should('exist');
});
});

View file

@ -0,0 +1,38 @@
import { WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();
describe('ADO-2106 connections should be colored correctly for pinned data in executions preview', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
beforeEach(() => {
cy.createFixtureWorkflow('Webhook_set_pinned.json');
workflowPage.actions.deselectAll();
workflowPage.getters.zoomToFitButton().click();
workflowPage.getters.getConnectionBetweenNodes('Webhook', 'Set').should('have.class', 'pinned');
});
it('should color connections for pinned data nodes for manual executions', () => {
workflowPage.actions.executeWorkflow();
executionsTab.actions.switchToExecutionsTab();
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find('.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]')
.should('have.class', 'success')
.should('have.class', 'has-run')
.should('have.class', 'pinned');
});
});

View file

@ -0,0 +1,135 @@
import { WorkflowPage, NDV } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('ADO-2111 expressions should support pinned data', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
it('supports pinned data in expressions unexecuted and executed parent nodes', () => {
cy.createFixtureWorkflow('Test_workflow_pinned_data_in_expressions.json', 'Expressions');
// test previous node unexecuted
workflowPage.actions.openNode('NotPinnedWithExpressions');
ndv.getters
.parameterExpressionPreview('value')
.eq(0)
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
ndv.getters
.parameterExpressionPreview('value')
.eq(1)
.should('contain.text', '0,0\nJoe\n\nJoe\n\nJoe\n\nJoe\nJoe');
// test can resolve correctly based on item
ndv.actions.switchInputMode('Table');
ndv.getters.inputTableRow(2).realHover();
cy.wait(50);
ndv.getters
.parameterExpressionPreview('value')
.eq(0)
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
ndv.getters
.parameterExpressionPreview('value')
.eq(1)
.should('contain.text', '0,1\nJoan\n\nJoan\n\nJoan\n\nJoan\nJoan');
// test previous node executed
ndv.actions.execute();
ndv.getters.inputTableRow(1).realHover();
cy.wait(50);
ndv.getters
.parameterExpressionPreview('value')
.eq(0)
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
ndv.getters
.parameterExpressionPreview('value')
.eq(1)
.should('contain.text', '0,0\nJoe\n\nJoe\n\nJoe\n\nJoe\nJoe');
ndv.getters.inputTableRow(2).realHover();
cy.wait(50);
ndv.getters
.parameterExpressionPreview('value')
.eq(0)
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
ndv.getters
.parameterExpressionPreview('value')
.eq(1)
.should('contain.text', '0,1\nJoan\n\nJoan\n\nJoan\n\nJoan\nJoan');
// check it resolved correctly on the backend
ndv.getters
.outputTbodyCell(1, 0)
.should('contain.text', 'Joe\\nJoe\\nJoan\\nJoan\\nJoe\\nJoan\\n\\nJoe\\nJoan\\n\\nJoe');
ndv.getters
.outputTbodyCell(2, 0)
.should('contain.text', 'Joe\\nJoe\\nJoan\\nJoan\\nJoe\\nJoan\\n\\nJoe\\nJoan\\n\\nJoe');
ndv.getters
.outputTbodyCell(1, 1)
.should('contain.text', '0,0\\nJoe\\n\\nJoe\\n\\nJoe\\n\\nJoe\\nJoe');
ndv.getters
.outputTbodyCell(2, 1)
.should('contain.text', '0,1\\nJoan\\n\\nJoan\\n\\nJoan\\n\\nJoan\\nJoan');
});
it('resets expressions after node is unpinned', () => {
cy.createFixtureWorkflow('Test_workflow_pinned_data_in_expressions.json', 'Expressions');
// test previous node unexecuted
workflowPage.actions.openNode('NotPinnedWithExpressions');
ndv.getters
.parameterExpressionPreview('value')
.eq(0)
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
ndv.getters
.parameterExpressionPreview('value')
.eq(1)
.should('contain.text', '0,0\nJoe\n\nJoe\n\nJoe\n\nJoe\nJoe');
ndv.actions.close();
// unpin pinned node
workflowPage.getters
.canvasNodeByName('PinnedSet')
.eq(0)
.find('.node-pin-data-icon')
.should('exist');
workflowPage.getters.canvasNodeByName('PinnedSet').eq(0).click();
workflowPage.actions.hitPinNodeShortcut();
workflowPage.getters
.canvasNodeByName('PinnedSet')
.eq(0)
.find('.node-pin-data-icon')
.should('not.exist');
workflowPage.actions.openNode('NotPinnedWithExpressions');
ndv.getters.nodeParameters().find('parameter-expression-preview-value').should('not.exist');
ndv.getters.parameterInput('value').eq(0).click();
ndv.getters
.inlineExpressionEditorOutput()
.should(
'have.text',
'[Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute previous nodes for preview][Execute previous nodes for preview][undefined]',
);
// close open expression
ndv.getters.inputLabel().eq(0).click();
ndv.getters.parameterInput('value').eq(1).click();
ndv.getters
.inlineExpressionEditorOutput()
.should(
'have.text',
'0,0[Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute previous nodes for preview][Execute previous nodes for preview][Execute previous nodes for preview]',
);
});
});

View file

@ -0,0 +1,34 @@
import { NDV, WorkflowPage } from '../pages';
import { clearNotifications } from '../pages/notifications';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('ADO-2230 NDV Pagination Reset', () => {
it('should reset pagaintion if data size changes to less than current page', () => {
// setup, load workflow with debughelper node with random seed
workflowPage.actions.visit();
cy.createFixtureWorkflow('NDV-debug-generate-data.json', 'Debug workflow');
workflowPage.actions.openNode('DebugHelper');
// execute node outputting 10 pages, check output of first page
ndv.actions.execute();
clearNotifications();
ndv.getters.outputTbodyCell(1, 1).invoke('text').should('eq', 'Terry.Dach@hotmail.com');
// open 4th page, check output
ndv.getters.pagination().should('be.visible');
ndv.getters.pagination().find('li.number').should('have.length', 5);
ndv.getters.pagination().find('li.number').eq(3).click();
ndv.getters.outputTbodyCell(1, 1).invoke('text').should('eq', 'Shane.Cormier68@yahoo.com');
// output a lot less data
ndv.getters.parameterInput('randomDataCount').find('input').clear().type('20');
ndv.actions.execute();
clearNotifications();
// check we are back to second page now
ndv.getters.pagination().find('li.number').should('have.length', 2);
ndv.getters.outputTbodyCell(1, 1).invoke('text').should('eq', 'Sylvia.Weber@hotmail.com');
});
});

View file

@ -4,7 +4,7 @@ const variablesPage = new VariablesPage();
describe('Variables', () => {
it('should show the unlicensed action box when the feature is disabled', () => {
cy.disableFeature('variables', false);
cy.disableFeature('variables');
cy.visit(variablesPage.url);
variablesPage.getters.unavailableResourcesList().should('be.visible');
@ -18,14 +18,15 @@ describe('Variables', () => {
beforeEach(() => {
cy.intercept('GET', '/rest/variables').as('loadVariables');
cy.intercept('GET', '/rest/login').as('login');
cy.visit(variablesPage.url);
cy.wait(['@loadVariables', '@loadSettings']);
cy.wait(['@loadVariables', '@loadSettings', '@login']);
});
it('should show the licensed action box when the feature is enabled', () => {
variablesPage.getters.emptyResourcesList().should('be.visible');
variablesPage.getters.createVariableButton().should('be.visible');
variablesPage.getters.emptyResourcesListNewVariableButton().should('be.visible');
});
it('should create a new variable using empty state row', () => {

View file

@ -1,5 +1,4 @@
import { WorkflowPage, NDV } from '../pages';
import { v4 as uuid } from 'uuid';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
@ -7,7 +6,7 @@ const ndv = new NDV();
describe('NDV', () => {
beforeEach(() => {
workflowPage.actions.visit();
workflowPage.actions.renameWorkflow(uuid());
workflowPage.actions.renameWithUniqueName();
workflowPage.actions.saveWorkflowOnButtonClick();
});
@ -113,6 +112,9 @@ describe('NDV', () => {
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Set3');
ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table');
ndv.getters
.inputRunSelector()
.should('exist')
@ -124,9 +126,6 @@ describe('NDV', () => {
.find('input')
.should('include.value', '2 of 2 (6 items)');
ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table');
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
@ -163,64 +162,6 @@ describe('NDV', () => {
ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
});
it('resolves expression with default item when input node is not parent, while still pairing items', () => {
cy.fixture('Test_workflow_5.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Set2');
ndv.getters.inputPanel().contains('6 items').should('exist');
ndv.getters
.outputRunSelector()
.find('input')
.should('exist')
.should('have.value', '2 of 2 (6 items)');
ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table');
ndv.getters.backToCanvas().realHover(); // 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(100);
ndv.getters.outputHoveringItem().should('not.exist');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
ndv.actions.selectInputNode('Code1');
ndv.getters.inputTableRow(1).realHover();
ndv.getters.inputTableRow(1).should('have.text', '1000');
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputTableRow(1).should('have.text', '1000');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
ndv.actions.selectInputNode('Code');
ndv.getters.inputTableRow(1).realHover();
cy.wait(100);
ndv.getters.inputTableRow(1).should('have.text', '6666');
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputHoveringItem().should('not.exist');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
ndv.actions.selectInputNode('When clicking');
ndv.getters.inputTableRow(1).realHover();
ndv.getters
.inputTableRow(1)
.should('have.text', "This is an item, but it's empty.")
.realHover();
ndv.getters.outputHoveringItem().should('have.length', 6);
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
});
it('can pair items between input and output across branches and runs', () => {
cy.fixture('Test_workflow_5.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));

View file

@ -1,7 +1,5 @@
import { META_KEY } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { getPopper } from '../utils';
import { Interception } from 'cypress/types/net-stubbing';
const workflowPage = new WorkflowPageClass();
@ -26,6 +24,9 @@ function checkStickiesStyle(
describe('Canvas Actions', () => {
beforeEach(() => {
workflowPage.actions.visit();
cy.get('#collapse-change-button').should('be.visible').click();
cy.get('#side-menu[class*=collapsed i]').should('be.visible');
workflowPage.actions.zoomToFit();
});
it('adds sticky to canvas with default text and position', () => {
@ -34,15 +35,12 @@ describe('Canvas Actions', () => {
addDefaultSticky();
workflowPage.actions.deselectAll();
workflowPage.actions.addStickyFromContextMenu();
workflowPage.actions.hitAddStickyShortcut();
workflowPage.actions.hitAddSticky();
workflowPage.getters.stickies().should('have.length', 3);
// Should not add a sticky for ctrl+shift+s
cy.get('body')
.type(META_KEY, { delay: 500, release: false })
.type('{shift}', { release: false })
.type('s');
cy.get('body').type(`{${META_KEY}+shift+s}`);
workflowPage.getters.stickies().should('have.length', 3);
workflowPage.getters
@ -82,32 +80,6 @@ describe('Canvas Actions', () => {
workflowPage.getters.stickies().should('have.length', 0);
});
it('change sticky color', () => {
workflowPage.actions.addSticky();
workflowPage.getters.stickies().should('have.length', 1);
workflowPage.actions.toggleColorPalette();
getPopper().should('be.visible');
workflowPage.actions.pickColor(2);
workflowPage.actions.toggleColorPalette();
getPopper().should('not.be.visible');
workflowPage.actions.saveWorkflowOnButtonClick();
cy.wait('@createWorkflow').then((interception: Interception) => {
const { request } = interception;
const color = request.body?.nodes[0]?.parameters?.color;
expect(color).to.equal(2);
});
workflowPage.getters.stickies().should('have.length', 1);
});
it('edits sticky and updates content as markdown', () => {
workflowPage.actions.addSticky();
@ -301,15 +273,6 @@ function stickyShouldBePositionedCorrectly(position: Position) {
});
}
function stickyShouldHaveCorrectSize(size: [number, number]) {
const yOffset = 0;
const xOffset = 0;
workflowPage.getters.stickies().should(($el) => {
expect($el).to.have.css('height', `${yOffset + size[0]}px`);
expect($el).to.have.css('width', `${xOffset + size[1]}px`);
});
}
function moveSticky(target: Position) {
cy.drag('[data-test-id="sticky"]', [target.left, target.top], { abs: true });
stickyShouldBePositionedCorrectly(target);

View file

@ -1,5 +1,5 @@
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
import { getVisiblePopper, getVisibleSelect } from '../utils';
import { getVisiblePopper } from '../utils';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
@ -37,21 +37,49 @@ describe('Resource Locator', () => {
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);
});
it('should show create credentials modal when clicking "add your credential"', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
ndv.getters.resourceLocator('documentId').should('be.visible');
ndv.getters.resourceLocatorInput('documentId').click();
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);
ndv.getters.resourceLocatorAddCredentials().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
});
it('should show appropriate error when credentials are not valid', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
workflowPage.getters.nodeCredentialsSelect().click();
// Add oAuth credentials
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
credentialsModal.actions.fillCredentialsForm();
cy.get('.el-message-box').find('button').contains('Close').click();
ndv.getters.resourceLocatorInput('documentId').click();
ndv.getters.resourceLocatorErrorMessage().should('contain', INVALID_CREDENTIALS_MESSAGE);
});
it('should show appropriate errors when search filter is required', () => {
workflowPage.actions.addNodeToCanvas('Github', true, true, 'On Pull Request');
ndv.getters.resourceLocator('owner').should('be.visible');
ndv.getters.resourceLocatorInput('owner').click();
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);
workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.actions.fillCredentialsForm();
ndv.getters.resourceLocatorInput('owner').click();
ndv.getters.resourceLocatorSearch('owner').type('owner');
ndv.getters.resourceLocatorErrorMessage().should('contain', INVALID_CREDENTIALS_MESSAGE);
});
it('should reset resource locator when dependent field is changed', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
@ -75,7 +103,7 @@ describe('Resource Locator', () => {
ndv.actions.setInvalidExpression({ fieldName: 'fieldId' });
ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview
ndv.getters.inputPanel().click(); // remove focus from input, hide expression preview
ndv.getters.resourceLocatorInput('rlc').click();

View file

@ -6,86 +6,51 @@ import {
getPublicApiUpgradeCTA,
} from '../pages';
import planData from '../fixtures/Plan_data_opt_in_trial.json';
import { INSTANCE_OWNER } from '../constants';
const mainSidebar = new MainSidebar();
const bannerStack = new BannerStack();
const workflowPage = new WorkflowPage();
describe('Cloud', { disableAutoLogin: true }, () => {
describe('Cloud', () => {
before(() => {
const now = new Date();
const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
planData.expirationDate = fiveDaysFromNow.toJSON();
});
beforeEach(() => {
cy.overrideSettings({
deployment: { type: 'cloud' },
n8nMetadata: { userId: '1' },
});
cy.intercept('GET', '/rest/admin/cloud-plan', planData).as('getPlanData');
cy.intercept('GET', '/rest/cloud/proxy/user/me', {}).as('getCloudUserInfo');
cy.intercept('GET', new RegExp('/rest/projects*')).as('projects');
cy.intercept('GET', new RegExp('/rest/roles')).as('roles');
});
function visitWorkflowPage() {
cy.visit(workflowPage.url);
cy.wait('@getPlanData');
cy.wait('@projects');
cy.wait('@roles');
}
describe('BannerStack', () => {
it('should render trial banner for opt-in cloud user', () => {
cy.intercept('GET', '/rest/admin/cloud-plan', {
body: planData,
}).as('getPlanData');
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } },
});
});
}).as('loadSettings');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
cy.wait('@getPlanData');
visitWorkflowPage();
bannerStack.getters.banner().should('be.visible');
mainSidebar.actions.signout();
bannerStack.getters.banner().should('not.be.visible');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
bannerStack.getters.banner().should('be.visible');
mainSidebar.actions.signout();
});
it('should not render opt-in-trial banner for non cloud deployment', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'default' } },
});
});
}).as('loadSettings');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
bannerStack.getters.banner().should('not.be.visible');
mainSidebar.actions.signout();
});
});
describe('Admin Home', () => {
it('Should show admin button', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } },
});
});
}).as('loadSettings');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
visitWorkflowPage();
mainSidebar.getters.adminPanel().should('be.visible');
});
@ -93,25 +58,8 @@ describe('Cloud', { disableAutoLogin: true }, () => {
describe('Public API', () => {
it('Should show upgrade CTA for Public API if user is trialing', () => {
cy.intercept('GET', '/rest/admin/cloud-plan', {
body: planData,
}).as('getPlanData');
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: {
...res.body.data,
deployment: { type: 'cloud' },
n8nMetadata: { userId: 1 },
},
});
});
}).as('loadSettings');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
visitPublicApiPage();
cy.wait(['@loadSettings', '@projects', '@roles', '@getPlanData']);
getPublicApiUpgradeCTA().should('be.visible');
});

View file

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

View file

@ -1,7 +1,6 @@
import {
HTTP_REQUEST_NODE_NAME,
IF_NODE_NAME,
INSTANCE_OWNER,
MANUAL_TRIGGER_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
} from '../constants';
@ -19,9 +18,9 @@ describe('Debug', () => {
it('should be able to debug executions', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.signinAsOwner();
workflowPage.actions.visit();

View file

@ -1,71 +0,0 @@
import { WorkflowPage, NDV } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('SQL editors', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
it('should preserve changes when opening-closing Postgres node', () => {
workflowPage.actions.addInitialNodeToCanvas('Postgres', {
action: 'Execute a SQL query',
keepNdvOpen: true,
});
ndv.getters
.sqlEditorContainer()
.click()
.find('.cm-content')
.type('SELECT * FROM `testTable`')
.type('{esc}');
ndv.actions.close();
workflowPage.actions.openNode('Postgres');
ndv.getters.sqlEditorContainer().find('.cm-content').type('{end} LIMIT 10').type('{esc}');
ndv.actions.close();
workflowPage.actions.openNode('Postgres');
ndv.getters.sqlEditorContainer().should('contain', 'SELECT * FROM `testTable` LIMIT 10');
});
it('should update expression output dropdown as the query is edited', () => {
workflowPage.actions.addInitialNodeToCanvas('MySQL', {
action: 'Execute a SQL query',
});
ndv.actions.close();
workflowPage.actions.openNode('When clicking "Test workflow"');
ndv.actions.setPinnedData([{ table: 'test_table' }]);
ndv.actions.close();
workflowPage.actions.openNode('MySQL');
ndv.getters
.sqlEditorContainer()
.find('.cm-content')
.type('SELECT * FROM {{ $json.table }}', { parseSpecialCharSequences: false });
workflowPage.getters
.inlineExpressionEditorOutput()
.should('have.text', 'SELECT * FROM test_table');
});
it('should not push NDV header out with a lot of code in Postgres editor', () => {
workflowPage.actions.addInitialNodeToCanvas('Postgres', {
action: 'Execute a SQL query',
keepNdvOpen: true,
});
cy.fixture('Dummy_javascript.txt').then((code) => {
ndv.getters.sqlEditorContainer().find('.cm-content').paste(code);
});
ndv.getters.nodeExecuteButton().should('be.visible');
});
it('should not push NDV header out with a lot of code in MySQL editor', () => {
workflowPage.actions.addInitialNodeToCanvas('MySQL', {
action: 'Execute a SQL query',
keepNdvOpen: true,
});
cy.fixture('Dummy_javascript.txt').then((code) => {
ndv.getters.sqlEditorContainer().find('.cm-content').paste(code);
});
ndv.getters.nodeExecuteButton().should('be.visible');
});
});

View file

@ -1,46 +1,246 @@
import { TemplatesPage } from '../pages/templates';
import { WorkflowPage } from '../pages/workflow';
import { WorkflowsPage } from '../pages/workflows';
import { MainSidebar } from '../pages/sidebar/main-sidebar';
import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json';
import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json';
const templatesPage = new TemplatesPage();
const workflowPage = new WorkflowPage();
const workflowsPage = new WorkflowsPage();
const mainSidebar = new MainSidebar();
describe('Workflow templates', () => {
beforeEach(() => {
cy.intercept('GET', '**/rest/settings', (req) => {
// Disable cache
delete req.headers['if-none-match']
req.reply((res) => {
if (res.body.data) {
// Disable custom templates host if it has been overridden by another intercept
res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' };
}
});
}).as('settingsRequest');
});
it('Opens website when clicking templates sidebar link', () => {
cy.visit(workflowsPage.url);
mainSidebar.getters.menuItem('Templates').should('be.visible');
// Templates should be a link to the website
mainSidebar.getters.templates().parent('a').should('have.attr', 'href').and('include', 'https://n8n.io/workflows');
// Link should contain instance address and n8n version
mainSidebar.getters.templates().parent('a').then(($a) => {
const href = $a.attr('href');
const params = new URLSearchParams(href);
// Link should have all mandatory parameters expected on the website
expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include(window.location.origin);
expect(params.get('utm_n8n_version')).to.match(/[0-9]+\.[0-9]+\.[0-9]+/);
expect(params.get('utm_awc')).to.match(/[0-9]+/);
const mockTemplateHost = (host: string) => {
cy.overrideSettings({
templates: { enabled: true, host },
});
};
describe('For api.n8n.io', () => {
beforeEach(() => {
mockTemplateHost('https://api.n8n.io/api/');
});
it('Opens website when clicking templates sidebar link', () => {
cy.visit(workflowsPage.url);
mainSidebar.getters.templates().should('be.visible');
// Templates should be a link to the website
mainSidebar.getters
.templates()
.parent('a')
.should('have.attr', 'href')
.and('include', 'https://n8n.io/workflows');
// Link should contain instance address and n8n version
mainSidebar.getters
.templates()
.parent('a')
.then(($a) => {
const href = $a.attr('href');
const params = new URLSearchParams(href);
// Link should have all mandatory parameters expected on the website
expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include(
window.location.origin,
);
expect(params.get('utm_n8n_version')).to.match(/[0-9]+\.[0-9]+\.[0-9]+/);
expect(params.get('utm_awc')).to.match(/[0-9]+/);
});
mainSidebar.getters.templates().parent('a').should('have.attr', 'target', '_blank');
});
it('Redirects to website when visiting templates page directly', () => {
cy.intercept(
{
hostname: 'n8n.io',
pathname: '/workflows',
},
'Mock Template Page',
).as('templatesPage');
cy.visit(templatesPage.url);
cy.wait('@templatesPage');
});
mainSidebar.getters.templates().parent('a').should('have.attr', 'target', '_blank');
});
it('Redirects to website when visiting templates page directly', () => {
cy.visit(templatesPage.url);
cy.origin('https://n8n.io', () => {
cy.url().should('include', 'https://n8n.io/workflows');
})
describe('For a custom template host', () => {
const hostname = 'random.domain';
const categories = [
{ id: 1, name: 'Engineering' },
{ id: 2, name: 'Finance' },
{ id: 3, name: 'Sales' },
];
const collections = [
{
id: 1,
name: 'Test Collection',
workflows: [{ id: 1 }],
nodes: [],
},
];
beforeEach(() => {
cy.intercept({ hostname, pathname: '/api/health' }, { status: 'OK' });
cy.intercept({ hostname, pathname: '/api/templates/categories' }, { categories });
cy.intercept(
{ hostname, pathname: '/api/templates/collections', query: { category: '**' } },
(req) => {
req.reply({ collections: req.query['category[]'] === '3' ? [] : collections });
},
);
cy.intercept(
{ hostname, pathname: '/api/templates/search', query: { category: '**' } },
(req) => {
const fixture =
req.query.category === 'Sales'
? 'templates_search/sales_templates_search_response.json'
: 'templates_search/all_templates_search_response.json';
req.reply({ statusCode: 200, fixture });
},
);
cy.intercept(
{ hostname, pathname: '/api/workflows/templates/1' },
{
statusCode: 200,
body: {
id: 1,
name: OnboardingWorkflow.name,
workflow: OnboardingWorkflow,
},
},
).as('getTemplate');
cy.intercept(
{ hostname, pathname: '/api/templates/workflows/1' },
{
statusCode: 200,
body: WorkflowTemplate,
},
).as('getTemplatePreview');
mockTemplateHost(`https://${hostname}/api`);
});
it('can open onboarding flow', () => {
templatesPage.actions.openOnboardingFlow();
cy.url().should('match', /.*\/workflow\/.*?onboardingId=1$/);
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', () => {
templatesPage.actions.importTemplate();
cy.url().should('include', '/workflow/new?templateId=1');
workflowPage.getters.canvasNodes().should('have.length', 4);
workflowPage.getters.stickies().should('have.length', 1);
workflowPage.actions.shouldHaveWorkflowName(OnboardingWorkflow.name);
});
it('should save template id with the workflow', () => {
templatesPage.actions.importTemplate();
cy.visit(templatesPage.url);
cy.get('.el-skeleton.n8n-loading').should('not.exist');
templatesPage.getters.firstTemplateCard().should('exist');
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.firstTemplateCard().click();
cy.url().should('include', '/templates/1');
cy.wait('@getTemplatePreview');
templatesPage.getters.useTemplateButton().click();
cy.url().should('include', '/workflow/new');
workflowPage.actions.saveWorkflowOnButtonClick();
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"');
});
});
it('can open template with images and hides workflow screenshots', () => {
cy.visit(`${templatesPage.url}/1`);
cy.wait('@getTemplatePreview');
templatesPage.getters.description().find('img').should('have.length', 1);
});
it('renders search elements correctly', () => {
cy.visit(templatesPage.url);
templatesPage.getters.searchInput().should('exist');
templatesPage.getters.allCategoriesFilter().should('exist');
templatesPage.getters.categoryFilters().should('have.length.greaterThan', 1);
templatesPage.getters.templateCards().should('have.length.greaterThan', 0);
});
it('can filter templates by category', () => {
cy.visit(templatesPage.url);
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.categoryFilter('sales').should('exist');
let initialTemplateCount = 0;
let initialCollectionCount = 0;
templatesPage.getters.templateCountLabel().then(($el) => {
initialTemplateCount = parseInt($el.text().replace(/\D/g, ''), 10);
templatesPage.getters.collectionCountLabel().then(($el1) => {
initialCollectionCount = parseInt($el1.text().replace(/\D/g, ''), 10);
templatesPage.getters.categoryFilter('sales').click();
templatesPage.getters.templatesLoadingContainer().should('not.exist');
// Should have less templates and collections after selecting a category
templatesPage.getters.templateCountLabel().should(($el2) => {
expect(parseInt($el2.text().replace(/\D/g, ''), 10)).to.be.lessThan(
initialTemplateCount,
);
});
templatesPage.getters.collectionCountLabel().should(($el2) => {
expect(parseInt($el2.text().replace(/\D/g, ''), 10)).to.be.lessThan(
initialCollectionCount,
);
});
});
});
});
it('should preserve search query in URL', () => {
cy.visit(templatesPage.url);
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.categoryFilter('sales').should('exist');
templatesPage.getters.categoryFilter('sales').click();
templatesPage.getters.searchInput().type('auto');
cy.url().should('include', '?categories=');
cy.url().should('include', '&search=');
cy.reload();
// Should preserve search query in URL
cy.url().should('include', '?categories=');
cy.url().should('include', '&search=');
// Sales category should still be selected
templatesPage.getters
.categoryFilter('sales')
.find('label')
.should('have.class', 'is-checked');
// Search input should still have the search query
templatesPage.getters.searchInput().should('have.value', 'auto');
// Sales checkbox should be pushed to the top
templatesPage.getters
.categoryFilters()
.eq(1)
.then(($el) => {
expect($el.text()).to.equal('Sales');
});
});
});
});

View file

@ -2,7 +2,6 @@ import {
CODE_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
IF_NODE_NAME,
INSTANCE_OWNER,
SCHEDULE_TRIGGER_NODE_NAME,
} from '../constants';
import {
@ -103,7 +102,7 @@ const switchBetweenEditorAndHistory = () => {
const switchBetweenEditorAndWorkflowlist = () => {
cy.getByTestId('menu-item').first().click();
cy.wait(['@getUsers', '@getWorkflows', '@getActiveWorkflows', '@getCredentials']);
cy.wait(['@getUsers', '@getWorkflows', '@getActiveWorkflows', '@getProjects']);
cy.getByTestId('resources-list-item').first().click();
@ -125,15 +124,10 @@ describe('Editor actions should work', () => {
beforeEach(() => {
cy.enableFeature('debugInEditor');
cy.enableFeature('workflowHistory');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.signinAsOwner();
createNewWorkflowAndActivate();
});
it('after saving a new workflow', () => {
editWorkflowAndDeactivate();
editWorkflowMoreAndActivate();
});
it('after switching between Editor and Executions', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
@ -148,7 +142,7 @@ describe('Editor actions should work', () => {
it('after switching between Editor and Debug', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun');
editWorkflowAndDeactivate();
workflowPage.actions.executeWorkflow();
@ -186,9 +180,9 @@ describe('Editor zoom should work after route changes', () => {
beforeEach(() => {
cy.enableFeature('debugInEditor');
cy.enableFeature('workflowHistory');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.signinAsOwner();
workflowPage.actions.visit();
cy.createFixtureWorkflow('Lots_of_nodes.json', `Lots of nodes`);
cy.createFixtureWorkflow('Lots_of_nodes.json', 'Lots of nodes');
workflowPage.actions.saveWorkflowOnButtonClick();
});
@ -196,9 +190,9 @@ describe('Editor zoom should work after route changes', () => {
cy.intercept('GET', '/rest/workflow-history/workflow/*/version/*').as('getVersion');
cy.intercept('GET', '/rest/workflow-history/workflow/*').as('getHistory');
cy.intercept('GET', '/rest/users').as('getUsers');
cy.intercept('GET', '/rest/workflows').as('getWorkflows');
cy.intercept('GET', '/rest/workflows?*').as('getWorkflows');
cy.intercept('GET', '/rest/active-workflows').as('getActiveWorkflows');
cy.intercept('GET', '/rest/credentials').as('getCredentials');
cy.intercept('GET', '/rest/projects').as('getProjects');
switchBetweenEditorAndHistory();
zoomInAndCheckNodes();

View file

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

View file

@ -1,118 +0,0 @@
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { MainSidebar } from '../pages';
import { INSTANCE_OWNER } from '../constants';
const WorkflowsPage = new WorkflowsPageClass();
const WorkflowPages = new WorkflowPageClass();
const mainSidebar = new MainSidebar();
describe.skip('Workflow filters', () => {
before(() => {
cy.enableFeature('sharing', true);
});
beforeEach(() => {
cy.visit(WorkflowsPage.url);
});
it('Should filter by tags', () => {
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_1.json', `Workflow 1`);
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_2.json', `Workflow 2`);
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.workflowFilterButton().click();
WorkflowsPage.getters.workflowTagsDropdown().click();
WorkflowsPage.getters.workflowTagItem('other-tag-1').click();
cy.get('body').click(0, 0);
WorkflowsPage.getters.workflowCards().should('have.length', 1);
WorkflowsPage.getters.workflowCard('Workflow 2').should('contain.text', 'Workflow 2');
mainSidebar.actions.goToSettings();
cy.go('back');
WorkflowsPage.getters.workflowCards().should('have.length', 1);
WorkflowsPage.getters.workflowCard('Workflow 2').should('contain.text', 'Workflow 2');
WorkflowsPage.getters.workflowResetFilters().click();
WorkflowsPage.getters.workflowCards().each(($el) => {
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text();
WorkflowsPage.getters.workflowCardActions(workflowName).click();
WorkflowsPage.getters.workflowDeleteButton().click();
cy.get('button').contains('delete').click();
});
});
it('Should filter by status', () => {
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_1.json', `Workflow 1`);
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_3.json', `Workflow 3`);
WorkflowPages.getters.activatorSwitch().click();
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.workflowFilterButton().click();
WorkflowsPage.getters.workflowStatusDropdown().click();
WorkflowsPage.getters.workflowStatusItem('Active').click();
cy.get('body').click(0, 0);
WorkflowsPage.getters.workflowCards().should('have.length', 1);
WorkflowsPage.getters.workflowCard('Workflow 3').should('contain.text', 'Workflow 3');
mainSidebar.actions.goToSettings();
cy.go('back');
WorkflowsPage.getters.workflowCards().should('have.length', 1);
WorkflowsPage.getters.workflowCard('Workflow 3').should('contain.text', 'Workflow 3');
WorkflowsPage.getters.workflowResetFilters().click();
WorkflowsPage.getters.workflowCards().each(($el) => {
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text();
WorkflowsPage.getters.workflowCardActions(workflowName).click();
WorkflowsPage.getters.workflowDeleteButton().click();
cy.get('button').contains('delete').click();
});
});
it('Should filter by owned by', () => {
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_1.json', `Workflow 1`);
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_3.json', `Workflow 3`);
WorkflowPages.getters.activatorSwitch().click();
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.workflowFilterButton().click();
WorkflowsPage.getters.workflowOwnershipDropdown().realClick();
WorkflowsPage.getters.workflowOwner(INSTANCE_OWNER.email).click();
cy.get('body').click(0, 0);
WorkflowsPage.getters.workflowCards().should('have.length', 2);
mainSidebar.actions.goToSettings();
cy.go('back');
WorkflowsPage.getters.workflowResetFilters().click();
WorkflowsPage.getters.workflowCards().each(($el) => {
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text();
WorkflowsPage.getters.workflowCardActions(workflowName).click();
WorkflowsPage.getters.workflowDeleteButton().click();
cy.get('button').contains('delete').click();
});
});
});

View file

@ -1,23 +1,32 @@
import workflow from '../fixtures/Manual_wait_set.json';
import { importWorkflow, vistDemoPage } from '../pages/demo';
import { importWorkflow, visitDemoPage } from '../pages/demo';
import { WorkflowPage } from '../pages/workflow';
import { errorToast } from '../pages/notifications';
const workflowPage = new WorkflowPage();
describe('Demo', () => {
beforeEach(() => {
cy.overrideSettings({ previewMode: true });
cy.signout();
});
it('can import template', () => {
vistDemoPage();
visitDemoPage();
errorToast().should('not.exist');
importWorkflow(workflow);
workflowPage.getters.canvasNodes().should('have.length', 3);
});
it('can override theme to dark', () => {
vistDemoPage('dark');
visitDemoPage('dark');
cy.get('body').should('have.attr', 'data-theme', 'dark');
errorToast().should('not.exist');
});
it('can override theme to light', () => {
vistDemoPage('light');
visitDemoPage('light');
cy.get('body').should('have.attr', 'data-theme', 'light');
errorToast().should('not.exist');
});
});

View file

@ -6,7 +6,7 @@ const ndv = new NDV();
describe('Node IO Filter', () => {
beforeEach(() => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Node_IO_filter.json', `Node IO filter`);
cy.createFixtureWorkflow('Node_IO_filter.json', 'Node IO filter');
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.executeWorkflow();
});
@ -15,13 +15,13 @@ describe('Node IO Filter', () => {
workflowPage.getters.canvasNodes().first().dblclick();
ndv.actions.close();
workflowPage.getters.canvasNodes().first().dblclick();
cy.wait(500);
ndv.getters.outputDataContainer().should('be.visible');
ndv.getters.outputPanel().findChildByTestId('ndv-search').should('exist');
cy.document().trigger('keyup', { key: '/' });
const searchInput = ndv.getters.searchInput();
searchInput.filter(':focus').should('exist');
searchInput.should('have.focus');
ndv.getters.pagination().find('li').should('have.length', 3);
ndv.getters.outputDataContainer().find('mark').should('not.exist');
@ -36,19 +36,18 @@ describe('Node IO Filter', () => {
it('should filter input/output data separately', () => {
workflowPage.getters.canvasNodes().eq(1).dblclick();
cy.wait(500);
ndv.getters.outputDataContainer().should('be.visible');
ndv.getters.inputDataContainer().should('be.visible');
ndv.actions.switchInputMode('Table');
ndv.getters.outputPanel().findChildByTestId('ndv-search').should('exist');
cy.document().trigger('keyup', { key: '/' });
ndv.getters.outputPanel().findChildByTestId('ndv-search').filter(':focus').should('not.exist');
ndv.getters.outputPanel().findChildByTestId('ndv-search').should('not.have.focus');
let focusedInput = ndv.getters
.inputPanel()
.findChildByTestId('ndv-search')
.filter(':focus')
.should('exist');
.should('have.focus');
const getInputPagination = () =>
ndv.getters.inputPanel().findChildByTestId('ndv-data-pagination');
@ -82,13 +81,9 @@ describe('Node IO Filter', () => {
ndv.getters.outputDataContainer().trigger('mouseover');
cy.document().trigger('keyup', { key: '/' });
ndv.getters.inputPanel().findChildByTestId('ndv-search').filter(':focus').should('not.exist');
ndv.getters.inputPanel().findChildByTestId('ndv-search').should('not.have.focus');
focusedInput = ndv.getters
.outputPanel()
.findChildByTestId('ndv-search')
.filter(':focus')
.should('exist');
focusedInput = ndv.getters.outputPanel().findChildByTestId('ndv-search').should('have.focus');
getInputPagination().find('li').should('have.length', 3);
getInputCounter().contains('21 items').should('exist');

View file

@ -1,4 +1,3 @@
import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
import { WorkerViewPage } from '../pages';
const workerViewPage = new WorkerViewPage();
@ -10,13 +9,13 @@ describe('Worker View (unlicensed)', () => {
});
it('should not show up in the menu sidebar', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(workerViewPage.url);
workerViewPage.getters.menuItem().should('not.exist');
});
it('should show action box', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(workerViewPage.url);
workerViewPage.getters.workerViewUnlicensed().should('exist');
});
@ -29,14 +28,14 @@ describe('Worker View (licensed)', () => {
});
it('should show up in the menu sidebar', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.enableQueueMode();
cy.visit(workerViewPage.url);
workerViewPage.getters.menuItem().should('exist');
});
it('should show worker list view', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(workerViewPage.url);
workerViewPage.getters.workerViewLicensed().should('exist');
});

View file

@ -1,6 +1,4 @@
import { WorkflowPage } from "../pages";
const workflowPage = new WorkflowPage();
import { errorToast, successToast } from '../pages/notifications';
const INVALID_NAMES = [
'https://n8n.io',
@ -27,14 +25,14 @@ const VALID_NAMES = [
];
describe('Personal Settings', () => {
it ('should allow to change first and last name', () => {
it('should allow to change first and last name', () => {
cy.visit('/settings/personal');
VALID_NAMES.forEach((name) => {
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name[0]);
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name[1]);
cy.getByTestId('save-settings-button').click();
workflowPage.getters.successToast().should('contain', 'Personal details updated');
workflowPage.getters.successToast().find('.el-notification__closeBtn').click();
successToast().should('contain', 'Personal details updated');
successToast().find('.el-notification__closeBtn').click();
});
});
it('not allow malicious values for personal data', () => {
@ -43,10 +41,8 @@ describe('Personal Settings', () => {
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name);
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name);
cy.getByTestId('save-settings-button').click();
workflowPage.getters
.errorToast()
.should('contain', 'Malicious firstName | Malicious lastName');
workflowPage.getters.errorToast().find('.el-notification__closeBtn').click();
errorToast().should('contain', 'Malicious firstName | Malicious lastName');
errorToast().find('.el-notification__closeBtn').click();
});
});
});

View file

@ -8,10 +8,19 @@ import { WorkflowPage } from '../pages/workflow';
import * as formStep from '../composables/setup-template-form-step';
import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button';
import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal';
import TestTemplate1 from '../fixtures/Test_Template_1.json';
import TestTemplate2 from '../fixtures/Test_Template_2.json';
const workflowPage = new WorkflowPage();
const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate;
const testTemplate = {
id: 1205,
data: TestTemplate1,
};
const templateWithoutCredentials = {
id: 1344,
data: TestTemplate2,
};
// NodeView uses beforeunload listener that will show a browser
// native popup, which will block cypress from continuing / exiting.
@ -29,19 +38,19 @@ Cypress.on('window:before:load', (win) => {
describe('Template credentials setup', () => {
beforeEach(() => {
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${testTemplate.id}`, {
fixture: testTemplate.fixture,
cy.intercept(
'GET',
`https://api.n8n.io/api/templates/workflows/${testTemplate.id}`,
testTemplate.data,
).as('getTemplatePreview');
cy.intercept(
'GET',
`https://api.n8n.io/api/workflows/templates/${testTemplate.id}`,
testTemplate.data.workflow,
).as('getTemplate');
cy.overrideSettings({
templates: { enabled: true, host: 'https://api.n8n.io/api/' },
});
cy.intercept('GET', '**/rest/settings', (req) => {
// Disable cache
delete req.headers['if-none-match']
req.reply((res) => {
if (res.body.data) {
// Disable custom templates host if it has been overridden by another intercept
res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' };
}
});
}).as('settingsRequest');
});
it('can be opened from template collection page', () => {
@ -50,7 +59,7 @@ describe('Template credentials setup', () => {
clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');
templateCredentialsSetupPage.getters
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
.title("Set up 'Promote new Shopify products on Twitter and Telegram' template")
.should('be.visible');
});
@ -58,7 +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 on Twitter and Telegram' template")
.should('be.visible');
templateCredentialsSetupPage.getters
@ -108,7 +117,7 @@ describe('Template credentials setup', () => {
// Focus the canvas so the copy to clipboard works
workflowPage.getters.canvasNodes().eq(0).realClick();
workflowPage.actions.selectAll();
workflowPage.actions.hitSelectAll();
workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
@ -117,6 +126,7 @@ describe('Template credentials setup', () => {
const workflow = JSON.parse(workflowJSON);
expect(workflow.meta).to.haveOwnProperty('templateId', testTemplate.id.toString());
expect(workflow.meta).not.to.haveOwnProperty('templateCredsSetupCompleted');
workflow.nodes.forEach((node: any) => {
expect(Object.keys(node.credentials ?? {})).to.have.lengthOf(1);
});
@ -124,11 +134,9 @@ describe('Template credentials setup', () => {
});
it('should work with a template that has no credentials (ADO-1603)', () => {
const templateWithoutCreds = templateCredentialsSetupPage.testData.templateWithoutCredentials;
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${templateWithoutCreds.id}`, {
fixture: templateWithoutCreds.fixture,
});
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(templateWithoutCreds.id);
const { id, data } = templateWithoutCredentials;
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${id}`, data);
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(id);
const expectedAppNames = ['1. Email (IMAP)', '2. Nextcloud'];
const expectedAppDescriptions = [
@ -151,7 +159,7 @@ describe('Template credentials setup', () => {
workflowPage.getters.canvasNodes().should('have.length', 3);
});
describe('Credential setup from workflow editor', () => {
describe('Credential setup from workflow editor', { disableAutoLogin: true }, () => {
beforeEach(() => {
cy.resetDatabase();
cy.signinAsOwner();
@ -189,7 +197,7 @@ describe('Template credentials setup', () => {
// Focus the canvas so the copy to clipboard works
workflowPage.getters.canvasNodes().eq(0).realClick();
workflowPage.actions.selectAll();
workflowPage.actions.hitSelectAll();
workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');

View file

@ -1,11 +1,10 @@
import { INSTANCE_ADMIN, INSTANCE_OWNER } from '../constants';
import { SettingsPage } from '../pages/settings';
const settingsPage = new SettingsPage();
describe('Admin user', { disableAutoLogin: true }, () => {
it('should see same Settings sub menu items as instance owner', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.visit(settingsPage.url);
let ownerMenuItems = 0;
@ -15,7 +14,7 @@ describe('Admin user', { disableAutoLogin: true }, () => {
});
cy.signout();
cy.signin(INSTANCE_ADMIN);
cy.signinAsAdmin();
cy.visit(settingsPage.url);
settingsPage.getters.menuItems().should('have.length', ownerMenuItems);

View file

@ -1,143 +0,0 @@
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
type SuggestedTemplatesStub = {
sections: SuggestedTemplatesSectionStub[];
}
type SuggestedTemplatesSectionStub = {
name: string;
title: string;
description: string;
workflows: Array<Object>;
};
const WorkflowsListPage = new WorkflowsPageClass();
const WorkflowPage = new WorkflowPageClass();
let fixtureSections: SuggestedTemplatesStub = { sections: [] };;
describe('Suggested templates - Should render', () => {
before(() => {
cy.fixture('Suggested_Templates.json').then((data) => {
fixtureSections = data;
});
});
beforeEach(() => {
localStorage.removeItem('SHOW_N8N_SUGGESTED_TEMPLATES');
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' } },
});
});
}).as('loadSettings');
cy.intercept('GET', '/rest/cloud/proxy/templates', {
fixture: 'Suggested_Templates.json',
});
cy.visit(WorkflowsListPage.url);
cy.wait('@loadSettings');
});
it('should render suggested templates page in empty workflow list', () => {
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('exist');
WorkflowsListPage.getters.suggestedTemplatesCards().should('have.length', fixtureSections.sections[0].workflows.length);
WorkflowsListPage.getters.suggestedTemplatesSectionDescription().should('contain', fixtureSections.sections[0].description);
});
it('should render suggested templates when there are workflows in the list', () => {
WorkflowsListPage.getters.suggestedTemplatesNewWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_1.json', 'Test workflow');
cy.visit(WorkflowsListPage.url);
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('exist');
cy.contains(`Explore ${fixtureSections.sections[0].name.toLocaleLowerCase()} workflow templates`).should('exist');
WorkflowsListPage.getters.suggestedTemplatesCards().should('have.length', fixtureSections.sections[0].workflows.length);
});
it('should enable users to signup for suggested templates templates', () => {
// Test the whole flow
WorkflowsListPage.getters.suggestedTemplatesCards().first().click();
WorkflowsListPage.getters.suggestedTemplatesPreviewModal().should('exist');
WorkflowsListPage.getters.suggestedTemplatesUseTemplateButton().click();
cy.url().should('include', '/workflow/new');
WorkflowPage.getters.infoToast().should('contain', 'Template coming soon!');
WorkflowPage.getters.infoToast().contains('Notify me when it\'s available').click();
WorkflowPage.getters.successToast().should('contain', 'We will contact you via email once this template is released.');
cy.visit(WorkflowsListPage.url);
// Once users have signed up for a template, suggestions should not be shown again
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
});
});
describe('Suggested templates - Should not render', () => {
beforeEach(() => {
localStorage.removeItem('SHOW_N8N_SUGGESTED_TEMPLATES');
cy.visit(WorkflowsListPage.url);
});
it('should not render suggested templates templates if not in cloud deployment', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'notCloud' } },
});
});
});
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
});
it('should not render suggested templates templates if endpoint throws error', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' } },
});
});
});
cy.intercept('GET', '/rest/cloud/proxy/templates', { statusCode: 500 }).as('loadTemplates');
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
});
it('should not render suggested templates templates if endpoint returns empty list', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' } },
});
});
});
cy.intercept('GET', '/rest/cloud/proxy/templates', (req) => {
req.on('response', (res) => {
res.send({
data: { collections: [] },
});
});
});
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
});
it('should not render suggested templates templates if endpoint returns invalid response', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' } },
});
});
});
cy.intercept('GET', '/rest/cloud/proxy/templates', (req) => {
req.on('response', (res) => {
res.send({
data: { somethingElse: [] },
});
});
});
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
});
});

View file

@ -1,4 +1,3 @@
import { INSTANCE_OWNER } from '../constants';
import { WorkflowsPage } from '../pages/workflows';
import {
closeVersionUpdatesPanel,
@ -11,52 +10,18 @@ const workflowsPage = new WorkflowsPage();
describe('Versions', () => {
it('should open updates panel', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.continue((res) => {
if (res.body.hasOwnProperty('data')) {
res.body.data = {
...res.body.data,
releaseChannel: 'stable',
versionCli: '1.0.0',
versionNotifications: {
enabled: true,
endpoint: 'https://api.n8n.io/api/versions/',
infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html',
},
};
}
});
}).as('settings');
cy.intercept('GET', 'https://api.n8n.io/api/versions/1.0.0', [
{
name: '1.3.1',
createdAt: '2023-08-18T11:53:12.857Z',
hasSecurityIssue: null,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: null,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131',
nodes: [],
description: 'Includes <strong>bug fixes</strong>',
cy.overrideSettings({
releaseChannel: 'stable',
versionCli: '1.0.0',
versionNotifications: {
enabled: true,
endpoint: 'https://api.n8n.io/api/versions/',
infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html',
},
{
name: '1.0.5',
createdAt: '2023-07-24T10:54:56.097Z',
hasSecurityIssue: false,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: true,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104',
nodes: [],
description: 'Includes <strong>core functionality</strong> and <strong>bug fixes</strong>',
},
]);
cy.signin(INSTANCE_OWNER);
});
cy.visit(workflowsPage.url);
cy.wait('@settings');
cy.wait('@loadSettings');
getVersionUpdatesPanelOpenButton().should('contain', '2 updates');
openVersionUpdatesPanel();

View file

@ -1,147 +0,0 @@
import { TemplatesPage } from '../pages/templates';
import { WorkflowPage } from '../pages/workflow';
import { TemplateWorkflowPage } from '../pages/template-workflow';
import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json';
import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json';
const templatesPage = new TemplatesPage();
const workflowPage = new WorkflowPage();
const templateWorkflowPage = new TemplateWorkflowPage();
describe.skip('In-app templates repository', () => {
beforeEach(() => {
cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=&search=', { fixture: 'templates_search/all_templates_search_response.json' }).as('searchRequest');
cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=Sales*', { fixture: 'templates_search/sales_templates_search_response.json' }).as('categorySearchRequest');
cy.intercept('GET', '**/api/templates/workflows/*', { fixture: 'templates_search/test_template_preview.json' }).as('singleTemplateRequest');
cy.intercept('GET', '**/api/workflows/templates/*', { fixture: 'templates_search/test_template_import.json' }).as('singleTemplateRequest');
cy.intercept('GET', '**/rest/settings', (req) => {
// Disable cache
delete req.headers['if-none-match']
req.reply((res) => {
if (res.body.data) {
// Enable in-app templates by setting a custom host
res.body.data.templates = { enabled: true, host: 'https://api-staging.n8n.io/api/' };
}
});
}).as('settingsRequest');
});
it('can open onboarding flow', () => {
templatesPage.actions.openOnboardingFlow(1, OnboardingWorkflow.name, OnboardingWorkflow, 'https://api-staging.n8n.io');
cy.url().then(($url) => {
expect($url).to.match(/.*\/workflow\/.*?onboardingId=1$/);
})
workflowPage.actions.shouldHaveWorkflowName(`Demo: ${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', () => {
templatesPage.actions.importTemplate(1, OnboardingWorkflow.name, OnboardingWorkflow, 'https://api-staging.n8n.io');
cy.url().then(($url) => {
expect($url).to.include('/workflow/new?templateId=1');
});
workflowPage.getters.canvasNodes().should('have.length', 4);
workflowPage.getters.stickies().should('have.length', 1);
workflowPage.actions.shouldHaveWorkflowName(OnboardingWorkflow.name);
});
it('should save template id with the workflow', () => {
cy.visit(templatesPage.url);
cy.get('.el-skeleton.n8n-loading').should('not.exist');
templatesPage.getters.firstTemplateCard().should('exist');
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.firstTemplateCard().click();
cy.url().should('include', '/templates/');
cy.url().then(($url) => {
const templateId = $url.split('/').pop();
templatesPage.getters.useTemplateButton().click();
cy.url().should('include', '/workflow/new');
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.selectAll();
workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
// Check workflow JSON by copying it to clipboard
cy.readClipboard().then((workflowJSON) => {
expect(workflowJSON).to.contain(`"templateId": "${templateId}"`);
});
});
});
it('can open template with images and hides workflow screenshots', () => {
templateWorkflowPage.actions.openTemplate(WorkflowTemplate, 'https://api-staging.n8n.io');
templateWorkflowPage.getters.description().find('img').should('have.length', 1);
});
it('renders search elements correctly', () => {
cy.visit(templatesPage.url);
templatesPage.getters.searchInput().should('exist');
templatesPage.getters.allCategoriesFilter().should('exist');
templatesPage.getters.categoryFilters().should('have.length.greaterThan', 1);
templatesPage.getters.templateCards().should('have.length.greaterThan', 0);
});
it('can filter templates by category', () => {
cy.visit(templatesPage.url);
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.categoryFilter('sales').should('exist');
let initialTemplateCount = 0;
let initialCollectionCount = 0;
templatesPage.getters.templateCountLabel().then(($el) => {
initialTemplateCount = parseInt($el.text().replace(/\D/g, ''), 10);
templatesPage.getters.collectionCountLabel().then(($el) => {
initialCollectionCount = parseInt($el.text().replace(/\D/g, ''), 10);
templatesPage.getters.categoryFilter('sales').click();
templatesPage.getters.templatesLoadingContainer().should('not.exist');
// Should have less templates and collections after selecting a category
templatesPage.getters.templateCountLabel().should(($el) => {
expect(parseInt($el.text().replace(/\D/g, ''), 10)).to.be.lessThan(initialTemplateCount);
});
templatesPage.getters.collectionCountLabel().should(($el) => {
expect(parseInt($el.text().replace(/\D/g, ''), 10)).to.be.lessThan(initialCollectionCount);
});
});
});
});
it('should preserve search query in URL', () => {
cy.visit(templatesPage.url);
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.categoryFilter('sales').should('exist');
templatesPage.getters.categoryFilter('sales').click();
templatesPage.getters.searchInput().type('auto');
cy.url().should('include', '?categories=');
cy.url().should('include', '&search=');
cy.reload();
// Should preserve search query in URL
cy.url().should('include', '?categories=');
cy.url().should('include', '&search=');
// Sales category should still be selected
templatesPage.getters.categoryFilter('sales').find('label').should('have.class', 'is-checked');
// Search input should still have the search query
templatesPage.getters.searchInput().should('have.value', 'auto');
// Sales checkbox should be pushed to the top
templatesPage.getters.categoryFilters().eq(1).then(($el) => {
expect($el.text()).to.equal('Sales');
});
});
});

View file

@ -1,5 +1,6 @@
import { WorkflowPage } from '../pages';
import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
import { errorToast, successToast } from '../pages/notifications';
const workflowPage = new WorkflowPage();
const messageBox = new MessageBoxClass();
@ -29,9 +30,9 @@ describe('Import workflow', () => {
workflowPage.getters.canvasNodes().should('have.length', 4);
workflowPage.getters.errorToast().should('not.exist');
errorToast().should('not.exist');
workflowPage.getters.successToast().should('not.exist');
successToast().should('not.exist');
});
it('clicking outside modal should not show error toast', () => {
@ -42,7 +43,7 @@ describe('Import workflow', () => {
cy.get('body').click(0, 0);
workflowPage.getters.errorToast().should('not.exist');
errorToast().should('not.exist');
});
it('canceling modal should not show error toast', () => {
@ -52,7 +53,7 @@ describe('Import workflow', () => {
workflowPage.getters.workflowMenuItemImportFromURLItem().click();
messageBox.getters.cancel().click();
workflowPage.getters.errorToast().should('not.exist');
errorToast().should('not.exist');
});
});
@ -64,7 +65,7 @@ describe('Import workflow', () => {
workflowPage.getters.workflowMenuItemImportFromFile().click();
workflowPage.getters
.workflowImportInput()
.selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true });
.selectFile('fixtures/Test_workflow-actions_paste-data.json', { force: true });
cy.waitForLoad(false);
workflowPage.actions.zoomToFit();
workflowPage.getters.canvasNodes().should('have.length', 5);

View file

@ -0,0 +1,567 @@
import {
INSTANCE_MEMBERS,
INSTANCE_OWNER,
MANUAL_TRIGGER_NODE_NAME,
NOTION_NODE_NAME,
} from '../constants';
import {
WorkflowsPage,
WorkflowPage,
CredentialsModal,
CredentialsPage,
WorkflowExecutionsTab,
NDV,
} from '../pages';
import * as projects from '../composables/projects';
import { getVisibleSelect } from '../utils';
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
const executionsTab = new WorkflowExecutionsTab();
const ndv = new NDV();
describe('Projects', { disableAutoLogin: true }, () => {
before(() => {
cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
});
it('should handle workflows and credentials and menu items', () => {
cy.signinAsAdmin();
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('not.have.length');
workflowsPage.getters.newWorkflowButtonCard().click();
cy.intercept('POST', '/rest/workflows').as('workflowSave');
workflowPage.actions.saveWorkflowOnButtonClick();
cy.wait('@workflowSave').then((interception) => {
expect(interception.request.body).not.to.have.property('projectId');
});
projects.getHomeButton().click();
projects.getProjectTabs().should('have.length', 2);
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('not.have.length');
credentialsPage.getters.emptyListCreateCredentialButton().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');
cy.intercept('POST', '/rest/credentials').as('credentialSave');
credentialsModal.actions.save();
cy.wait('@credentialSave').then((interception) => {
expect(interception.request.body).not.to.have.property('projectId');
});
credentialsModal.actions.close();
credentialsPage.getters.credentialCards().should('have.length', 1);
credentialsPage.getters
.credentialCards()
.first()
.find('.n8n-node-icon img')
.should('be.visible');
projects.getProjectTabWorkflows().click();
workflowsPage.getters.workflowCards().should('have.length', 1);
projects.getMenuItems().should('not.have.length');
cy.intercept('POST', '/rest/projects').as('projectCreate');
projects.getAddProjectButton().click();
cy.wait('@projectCreate');
projects.getMenuItems().should('have.length', 1);
projects.getProjectTabs().should('have.length', 3);
cy.get('input[name="name"]').type('Development');
projects.addProjectMember(INSTANCE_MEMBERS[0].email);
cy.intercept('PATCH', '/rest/projects/*').as('projectSettingsSave');
projects.getProjectSettingsSaveButton().click();
cy.wait('@projectSettingsSave').then((interception) => {
expect(interception.request.body).to.have.property('name').and.to.equal('Development');
expect(interception.request.body).to.have.property('relations').to.have.lengthOf(2);
});
projects.getMenuItems().first().click();
workflowsPage.getters.workflowCards().should('not.have.length');
projects.getProjectTabs().should('have.length', 3);
workflowsPage.getters.newWorkflowButtonCard().click();
cy.intercept('POST', '/rest/workflows').as('workflowSave');
workflowPage.actions.saveWorkflowOnButtonClick();
cy.wait('@workflowSave').then((interception) => {
expect(interception.request.body).to.have.property('projectId');
});
projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('not.have.length');
credentialsPage.getters.emptyListCreateCredentialButton().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');
cy.intercept('POST', '/rest/credentials').as('credentialSave');
credentialsModal.actions.save();
cy.wait('@credentialSave').then((interception) => {
expect(interception.request.body).to.have.property('projectId');
});
credentialsModal.actions.close();
projects.getAddProjectButton().click();
projects.getMenuItems().should('have.length', 2);
let projectId: string;
projects.getMenuItems().first().click();
cy.intercept('GET', '/rest/credentials*').as('credentialsList');
projects.getProjectTabCredentials().click();
cy.wait('@credentialsList').then((interception) => {
const url = new URL(interception.request.url);
const queryParams = new URLSearchParams(url.search);
const filter = queryParams.get('filter');
expect(filter).to.be.a('string').and.to.contain('projectId');
if (filter) {
projectId = JSON.parse(filter).projectId;
}
});
projects.getMenuItems().last().click();
cy.intercept('GET', '/rest/credentials*').as('credentialsListProjectId');
projects.getProjectTabCredentials().click();
cy.wait('@credentialsListProjectId').then((interception) => {
const url = new URL(interception.request.url);
const queryParams = new URLSearchParams(url.search);
const filter = queryParams.get('filter');
expect(filter).to.be.a('string').and.to.contain('projectId');
if (filter) {
expect(JSON.parse(filter).projectId).not.to.equal(projectId);
}
});
projects.getHomeButton().click();
workflowsPage.getters.workflowCards().should('have.length', 2);
cy.intercept('GET', '/rest/credentials*').as('credentialsListUnfiltered');
projects.getProjectTabCredentials().click();
cy.wait('@credentialsListUnfiltered').then((interception) => {
expect(interception.request.url).not.to.contain('filter');
});
let menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Home")[class*=active_]').should('exist');
projects.getMenuItems().first().click();
menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Development")[class*=active_]').should('exist');
cy.intercept('GET', '/rest/workflows/*').as('loadWorkflow');
workflowsPage.getters.workflowCards().first().click();
cy.wait('@loadWorkflow');
menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Development")[class*=active_]').should('exist');
cy.intercept('GET', '/rest/executions*').as('loadExecutions');
executionsTab.actions.switchToExecutionsTab();
cy.wait('@loadExecutions');
menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Development")[class*=active_]').should('exist');
executionsTab.actions.switchToEditorTab();
menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Development")[class*=active_]').should('exist');
cy.getByTestId('menu-item').filter(':contains("Variables")').click();
cy.getByTestId('unavailable-resources-list').should('be.visible');
menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Variables")[class*=active_]').should('exist');
projects.getHomeButton().click();
menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Home")[class*=active_]').should('exist');
workflowsPage.getters.workflowCards().should('have.length', 2).first().click();
cy.wait('@loadWorkflow');
cy.getByTestId('execute-workflow-button').should('be.visible');
menuItems = cy.getByTestId('menu-item');
menuItems.filter(':contains("Home")[class*=active_]').should('not.exist');
menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Development")[class*=active_]').should('exist');
});
it('should not show project add button and projects to a member if not invited to any project', () => {
cy.signinAsMember(1);
cy.visit(workflowsPage.url);
projects.getAddProjectButton().should('not.exist');
projects.getMenuItems().should('not.exist');
});
describe('when starting from scratch', () => {
beforeEach(() => {
cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
});
it('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', () => {
cy.signinAsOwner();
cy.visit(workflowsPage.url);
// Create a project and add a credential to it
cy.intercept('POST', '/rest/projects').as('projectCreate');
projects.getAddProjectButton().should('contain', 'Add project').should('be.visible').click();
cy.wait('@projectCreate');
projects.getMenuItems().should('have.length', 1);
projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('not.have.length');
credentialsPage.getters.emptyListCreateCredentialButton().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('Notion account project 1');
cy.intercept('POST', '/rest/credentials').as('credentialSave');
credentialsModal.actions.save();
cy.wait('@credentialSave').then((interception) => {
expect(interception.request.body).to.have.property('projectId');
});
credentialsModal.actions.close();
// Create another project and add a credential to it
projects.getAddProjectButton().click();
cy.wait('@projectCreate');
projects.getMenuItems().should('have.length', 2);
projects.getMenuItems().last().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('not.have.length');
credentialsPage.getters.emptyListCreateCredentialButton().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('Notion account project 2');
credentialsModal.actions.save();
cy.wait('@credentialSave').then((interception) => {
expect(interception.request.body).to.have.property('projectId');
});
credentialsModal.actions.close();
// Create a credential in Home project
projects.getHomeButton().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 2);
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('Notion account personal project');
cy.intercept('POST', '/rest/credentials').as('credentialSave');
credentialsModal.actions.save();
cy.wait('@credentialSave');
credentialsModal.actions.close();
// Go to the first project and create a workflow
projects.getMenuItems().first().click();
workflowsPage.getters.workflowCards().should('not.have.length');
cy.intercept('GET', '/rest/credentials/for-workflow*').as('getCredentialsForWorkflow');
workflowsPage.getters.newWorkflowButtonCard().click();
cy.wait('@getCredentialsForWorkflow').then((interception) => {
expect(interception.request.query).to.have.property('projectId');
expect(interception.request.query).not.to.have.property('workflowId');
});
workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account project 1');
ndv.getters.backToCanvas().click();
workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload();
cy.wait('@getCredentialsForWorkflow').then((interception) => {
expect(interception.request.query).not.to.have.property('projectId');
expect(interception.request.query).to.have.property('workflowId');
});
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account project 1');
ndv.getters.backToCanvas().click();
// Go to the second project and create a workflow
projects.getMenuItems().last().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);
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account project 2');
ndv.getters.backToCanvas().click();
workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload();
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account project 2');
ndv.getters.backToCanvas().click();
// Go to the Home project and create a workflow
projects.getHomeButton().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 3);
projects.getProjectTabWorkflows().click();
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.createWorkflowButton().click();
workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account personal project');
ndv.getters.backToCanvas().click();
workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload();
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Notion account personal project');
});
it('should move resources between projects', () => {
cy.signin(INSTANCE_OWNER);
cy.visit(workflowsPage.url);
// Create a workflow and a credential in the Home project
workflowsPage.getters.workflowCards().should('not.have.length');
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project');
projects.getHomeButton().click();
projects.getProjectTabCredentials().should('be.visible').click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Home project');
// Create a project and add a credential and a workflow to it
projects.createProject('Project 1');
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Project 1');
projects.getProjectTabWorkflows().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1');
// Create another project and add a credential and a workflow to it
projects.createProject('Project 2');
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Project 2');
projects.getProjectTabWorkflows().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2');
// Move the workflow owned by me from Home to Project 1
projects.getHomeButton().click();
workflowsPage.getters
.workflowCards()
.should('have.length', 3)
.filter(':contains("Owned by me")')
.should('exist');
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
workflowsPage.getters.workflowMoveButton().click();
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Next")')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 2)
.first()
.should('contain.text', 'Project 1')
.click();
projects.getResourceMoveModal().find('button:contains("Next")').click();
projects
.getResourceMoveConfirmModal()
.should('be.visible')
.find('button:contains("Confirm")')
.should('be.disabled');
projects
.getResourceMoveConfirmModal()
.find('input[type="checkbox"]')
.first()
.parents('label')
.click();
projects
.getResourceMoveConfirmModal()
.find('button:contains("Confirm")')
.should('be.disabled');
projects
.getResourceMoveConfirmModal()
.find('input[type="checkbox"]')
.last()
.parents('label')
.click();
projects
.getResourceMoveConfirmModal()
.find('button:contains("Confirm")')
.should('not.be.disabled')
.click();
workflowsPage.getters
.workflowCards()
.should('have.length', 3)
.filter(':contains("Owned by me")')
.should('not.exist');
// Move the credential from Project 1 to Project 2
projects.getMenuItems().first().click();
workflowsPage.getters.workflowCards().should('have.length', 2);
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 1);
credentialsPage.getters.credentialCardActions('Credential in Project 1').click();
credentialsPage.getters.credentialMoveButton().click();
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Next")')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 1)
.first()
.should('contain.text', 'Project 2')
.click();
projects.getResourceMoveModal().find('button:contains("Next")').click();
projects
.getResourceMoveConfirmModal()
.should('be.visible')
.find('button:contains("Confirm")')
.should('be.disabled');
projects
.getResourceMoveConfirmModal()
.find('input[type="checkbox"]')
.first()
.parents('label')
.click();
projects
.getResourceMoveConfirmModal()
.find('button:contains("Confirm")')
.should('be.disabled');
projects
.getResourceMoveConfirmModal()
.find('input[type="checkbox"]')
.last()
.parents('label')
.click();
projects
.getResourceMoveConfirmModal()
.find('button:contains("Confirm")')
.should('not.be.disabled')
.click();
credentialsPage.getters.credentialCards().should('not.have.length');
projects.getMenuItems().last().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 2);
});
});
});

View file

@ -35,7 +35,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').type('manual');
nodeCreatorFeature.getters.creatorItem().should('have.length', 2);
nodeCreatorFeature.getters.creatorItem().should('have.length', 1);
nodeCreatorFeature.getters.searchBar().find('input').clear().type('manual123');
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
nodeCreatorFeature.getters
@ -159,7 +159,7 @@ describe('Node Creator', () => {
it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.getCreatorItem('Manually').click();
nodeCreatorFeature.getters.getCreatorItem('Trigger manually').click();
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
@ -308,7 +308,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
NDVModal.actions.close();
WorkflowPage.actions.deleteNode('When clicking "Test workflow"');
WorkflowPage.actions.deleteNode('When clicking Test workflow');
WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click();

View file

@ -0,0 +1,28 @@
import { NDV, WorkflowPage } from '../pages';
const canvas = new WorkflowPage();
const ndv = new NDV();
describe('Manual partial execution', () => {
it('should execute parent nodes with no run data only once', () => {
canvas.actions.visit();
cy.fixture('manual-partial-execution.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
canvas.actions.zoomToFit();
canvas.actions.openNode('Edit Fields');
cy.get('button').contains('Test step').click(); // create run data
cy.get('button').contains('Test step').click(); // use run data
ndv.actions.close();
canvas.actions.openNode('Webhook1');
ndv.getters.nodeRunSuccessIndicator().should('exist');
ndv.getters.outputRunSelector().should('not.exist'); // single run
});
});

View file

@ -0,0 +1,176 @@
import { WorkflowPage, NDV } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
// Update is debounced in editors, so adding typing delay to catch up
const TYPING_DELAY = 100;
describe('Editors', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
describe('SQL Editor', () => {
it('should preserve changes when opening-closing Postgres node', () => {
workflowPage.actions.addInitialNodeToCanvas('Postgres', {
action: 'Execute a SQL query',
keepNdvOpen: true,
});
ndv.getters
.sqlEditorContainer()
.click()
.find('.cm-content')
.type('SELECT * FROM `testTable`', { delay: TYPING_DELAY })
.type('{esc}');
ndv.actions.close();
workflowPage.actions.openNode('Postgres');
ndv.getters
.sqlEditorContainer()
.find('.cm-content')
.type('{end} LIMIT 10', { delay: TYPING_DELAY })
.type('{esc}');
ndv.actions.close();
workflowPage.actions.openNode('Postgres');
ndv.getters.sqlEditorContainer().should('contain', 'SELECT * FROM `testTable` LIMIT 10');
});
it('should update expression output dropdown as the query is edited', () => {
workflowPage.actions.addInitialNodeToCanvas('MySQL', {
action: 'Execute a SQL query',
});
ndv.actions.close();
workflowPage.actions.openNode('When clicking Test workflow');
ndv.actions.setPinnedData([{ table: 'test_table' }]);
ndv.actions.close();
workflowPage.actions.openNode('MySQL');
ndv.getters
.sqlEditorContainer()
.find('.cm-content')
.type('SELECT * FROM {{ $json.table }}', { parseSpecialCharSequences: false });
workflowPage.getters
.inlineExpressionEditorOutput()
.should('have.text', 'SELECT * FROM test_table');
});
it('should not push NDV header out with a lot of code in Postgres editor', () => {
workflowPage.actions.addInitialNodeToCanvas('Postgres', {
action: 'Execute a SQL query',
keepNdvOpen: true,
});
cy.fixture('Dummy_javascript.txt').then((code) => {
ndv.getters.sqlEditorContainer().find('.cm-content').paste(code);
});
ndv.getters.nodeExecuteButton().should('be.visible');
});
it('should not push NDV header out with a lot of code in MySQL editor', () => {
workflowPage.actions.addInitialNodeToCanvas('MySQL', {
action: 'Execute a SQL query',
keepNdvOpen: true,
});
cy.fixture('Dummy_javascript.txt').then((code) => {
ndv.getters.sqlEditorContainer().find('.cm-content').paste(code);
});
ndv.getters.nodeExecuteButton().should('be.visible');
});
it('should not trigger dirty flag if nothing is changed', () => {
workflowPage.actions.addInitialNodeToCanvas('Postgres', {
action: 'Execute a SQL query',
keepNdvOpen: true,
});
ndv.actions.close();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.getters.isWorkflowSaved();
workflowPage.actions.openNode('Postgres');
ndv.actions.close();
// Workflow should still be saved
workflowPage.getters.isWorkflowSaved();
});
it('should trigger dirty flag if query is updated', () => {
workflowPage.actions.addInitialNodeToCanvas('Postgres', {
action: 'Execute a SQL query',
keepNdvOpen: true,
});
ndv.actions.close();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.getters.isWorkflowSaved();
workflowPage.actions.openNode('Postgres');
ndv.getters
.sqlEditorContainer()
.click()
.find('.cm-content')
.type('SELECT * FROM `testTable`', { delay: TYPING_DELAY })
.type('{esc}');
ndv.actions.close();
workflowPage.getters.isWorkflowSaved().should('not.be.true');
});
});
describe('HTML Editor', () => {
// Closing tags will be added by the editor
const TEST_ELEMENT_H1 = '<h1>Test';
const TEST_ELEMENT_P = '<p>Test';
it('should preserve changes when opening-closing HTML node', () => {
workflowPage.actions.addInitialNodeToCanvas('HTML', {
action: 'Generate HTML template',
keepNdvOpen: true,
});
ndv.getters
.htmlEditorContainer()
.click()
.find('.cm-content')
.type(`{selectall}${TEST_ELEMENT_H1}`, { delay: TYPING_DELAY, force: true })
.type('{esc}');
ndv.actions.close();
workflowPage.actions.openNode('HTML');
ndv.getters
.htmlEditorContainer()
.find('.cm-content')
.type(`{end}${TEST_ELEMENT_P}`, { delay: TYPING_DELAY, force: true })
.type('{esc}');
ndv.actions.close();
workflowPage.actions.openNode('HTML');
ndv.getters.htmlEditorContainer().should('contain', TEST_ELEMENT_H1);
ndv.getters.htmlEditorContainer().should('contain', TEST_ELEMENT_P);
});
it('should not trigger dirty flag if nothing is changed', () => {
workflowPage.actions.addInitialNodeToCanvas('HTML', {
action: 'Generate HTML template',
keepNdvOpen: true,
});
ndv.actions.close();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.getters.isWorkflowSaved();
workflowPage.actions.openNode('HTML');
ndv.actions.close();
// Workflow should still be saved
workflowPage.getters.isWorkflowSaved();
});
it('should trigger dirty flag if query is updated', () => {
workflowPage.actions.addInitialNodeToCanvas('HTML', {
action: 'Generate HTML template',
keepNdvOpen: true,
});
ndv.actions.close();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.getters.isWorkflowSaved();
workflowPage.actions.openNode('HTML');
ndv.getters
.htmlEditorContainer()
.click()
.find('.cm-content')
.type(`{selectall}${TEST_ELEMENT_H1}`, { delay: TYPING_DELAY, force: true })
.type('{esc}');
ndv.actions.close();
workflowPage.getters.isWorkflowSaved().should('not.be.true');
});
});
});

View file

@ -0,0 +1,143 @@
import { INSTANCE_ADMIN } from '../constants';
import { clearNotifications } from '../pages/notifications';
import {
getNpsSurvey,
getNpsSurveyClose,
getNpsSurveyEmail,
getNpsSurveyRatings,
} from '../pages/npsSurvey';
import { WorkflowPage } from '../pages/workflow';
const workflowPage = new WorkflowPage();
const NOW = 1717771477012;
const ONE_DAY = 24 * 60 * 60 * 1000;
const THREE_DAYS = ONE_DAY * 3;
const SEVEN_DAYS = ONE_DAY * 7;
const ABOUT_SIX_MONTHS = ONE_DAY * 30 * 6 + ONE_DAY;
describe('NpsSurvey', () => {
beforeEach(() => {
cy.resetDatabase();
cy.signin(INSTANCE_ADMIN);
});
it('shows nps survey to recently activated user and can submit email ', () => {
cy.intercept('/rest/settings', { middleware: true }, (req) => {
req.on('response', (res) => {
if (res.body.data) {
res.body.data.telemetry = {
enabled: true,
config: {
key: 'test',
url: 'https://telemetry-test.n8n.io',
},
};
}
});
});
cy.intercept('/rest/login', { middleware: true }, (req) => {
req.on('response', (res) => {
if (res.body.data) {
res.body.data.settings = res.body.data.settings || {};
res.body.data.settings.userActivated = true;
res.body.data.settings.userActivatedAt = NOW - THREE_DAYS - 1000;
}
});
});
workflowPage.actions.visit(true, NOW);
workflowPage.actions.saveWorkflowOnButtonClick();
getNpsSurvey().should('be.visible');
getNpsSurveyRatings().find('button').should('have.length', 11);
getNpsSurveyRatings().find('button').first().click();
getNpsSurveyEmail().find('input').type('test@n8n.io');
getNpsSurveyEmail().find('button').click();
// test that modal does not show up again until 6 months later
workflowPage.actions.visit(true, NOW + ONE_DAY);
workflowPage.actions.saveWorkflowOnButtonClick();
getNpsSurvey().should('not.be.visible');
// 6 months later
workflowPage.actions.visit(true, NOW + ABOUT_SIX_MONTHS);
workflowPage.actions.saveWorkflowOnButtonClick();
getNpsSurvey().should('be.visible');
});
it('allows user to ignore survey 3 times before stopping to show until 6 months later', () => {
cy.intercept('/rest/settings', { middleware: true }, (req) => {
req.on('response', (res) => {
if (res.body.data) {
res.body.data.telemetry = {
enabled: true,
config: {
key: 'test',
url: 'https://telemetry-test.n8n.io',
},
};
}
});
});
cy.intercept('/rest/login', { middleware: true }, (req) => {
req.on('response', (res) => {
if (res.body.data) {
res.body.data.settings = res.body.data.settings || {};
res.body.data.settings.userActivated = true;
res.body.data.settings.userActivatedAt = NOW - THREE_DAYS - 1000;
}
});
});
// can ignore survey and it won't show up again
workflowPage.actions.visit(true, NOW);
workflowPage.actions.saveWorkflowOnButtonClick();
clearNotifications();
getNpsSurvey().should('be.visible');
getNpsSurveyClose().click();
getNpsSurvey().should('not.be.visible');
workflowPage.actions.visit(true, NOW + ONE_DAY);
workflowPage.actions.saveWorkflowOnButtonClick();
getNpsSurvey().should('not.be.visible');
// shows up seven days later to ignore again
workflowPage.actions.visit(true, NOW + SEVEN_DAYS + 10000);
workflowPage.actions.saveWorkflowOnButtonClick();
clearNotifications();
getNpsSurvey().should('be.visible');
getNpsSurveyClose().click();
getNpsSurvey().should('not.be.visible');
workflowPage.actions.visit(true, NOW + SEVEN_DAYS + 10000);
workflowPage.actions.saveWorkflowOnButtonClick();
getNpsSurvey().should('not.be.visible');
// shows up after at least seven days later to ignore again
workflowPage.actions.visit(true, NOW + (SEVEN_DAYS + 10000) * 2 + ONE_DAY);
workflowPage.actions.saveWorkflowOnButtonClick();
clearNotifications();
getNpsSurvey().should('be.visible');
getNpsSurveyClose().click();
getNpsSurvey().should('not.be.visible');
workflowPage.actions.visit(true, NOW + (SEVEN_DAYS + 10000) * 2 + ONE_DAY * 2);
workflowPage.actions.saveWorkflowOnButtonClick();
getNpsSurvey().should('not.be.visible');
// does not show up again after at least 7 days
workflowPage.actions.visit(true, NOW + (SEVEN_DAYS + 10000) * 3 + ONE_DAY * 3);
workflowPage.actions.saveWorkflowOnButtonClick();
getNpsSurvey().should('not.be.visible');
// shows up 6 months later
workflowPage.actions.visit(true, NOW + (SEVEN_DAYS + 10000) * 3 + ABOUT_SIX_MONTHS);
workflowPage.actions.saveWorkflowOnButtonClick();
getNpsSurvey().should('be.visible');
});
});

View file

@ -0,0 +1,46 @@
import { CredentialsPage, CredentialsModal } from '../pages';
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
describe('Credentials', () => {
it('create and connect with Google OAuth2', () => {
// Open credentials page
cy.visit(credentialsPage.url, {
onBeforeLoad(win) {
cy.stub(win, 'open').as('windowOpen');
},
});
// Add a new Google OAuth2 credential
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.getters.newCredentialTypeOption('Google OAuth2 API').click();
credentialsModal.getters.newCredentialTypeButton().click();
// Fill in the key/secret and save
credentialsModal.actions.fillField('clientId', 'test-key');
credentialsModal.actions.fillField('clientSecret', 'test-secret');
credentialsModal.actions.save();
// Connect to Google
credentialsModal.getters.oauthConnectButton().click();
cy.get('@windowOpen').should(
'have.been.calledOnceWith',
Cypress.sinon.match(
'https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent&client_id=test-key&redirect_uri=http%3A%2F%2Flocalhost%3A5678%2Frest%2Foauth2-credential%2Fcallback&response_type=code',
),
'OAuth Authorization',
'scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700',
);
// Emulate successful save using BroadcastChannel
cy.window().then(() => {
const channel = new BroadcastChannel('oauth-callback');
channel.postMessage('success');
});
// Check that the credential was saved and connected successfully
credentialsModal.getters.saveButton().should('contain.text', 'Saved');
credentialsModal.getters.oauthConnectSuccessBanner().should('be.visible');
});
});

View file

@ -0,0 +1,26 @@
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { getSaveChangesModal } from '../composables/modals/save-changes-modal';
const WorkflowsPage = new WorkflowsPageClass();
const WorkflowPage = new WorkflowPageClass();
describe('Workflows', () => {
beforeEach(() => {
cy.visit(WorkflowsPage.url);
});
it('should ask to save unsaved changes before leaving route', () => {
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
cy.getByTestId('project-home-menu-item').click();
getSaveChangesModal().should('be.visible');
});
});

View file

@ -1,6 +1,4 @@
import { v4 as uuid } from 'uuid';
import { getVisibleSelect } from '../utils';
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../constants';
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants';
import { NDV, WorkflowPage } from '../pages';
import { NodeCreator } from '../pages/features/node-creator';
import { clickCreateNewCredential } from '../composables/ndv';
@ -12,7 +10,7 @@ const ndv = new NDV();
describe('NDV', () => {
beforeEach(() => {
workflowPage.actions.visit();
workflowPage.actions.renameWorkflow(uuid());
workflowPage.actions.renameWithUniqueName();
workflowPage.actions.saveWorkflowOnButtonClick();
});
@ -24,6 +22,14 @@ describe('NDV', () => {
ndv.getters.container().should('not.be.visible');
});
it('should show input panel when node is not connected', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.deselectAll();
workflowPage.actions.addNodeToCanvas('Set');
workflowPage.getters.canvasNodes().last().dblclick();
ndv.getters.container().should('be.visible').should('contain', 'Wire me up');
});
it('should test webhook node', () => {
workflowPage.actions.addInitialNodeToCanvas('Webhook');
workflowPage.getters.canvasNodes().first().dblclick();
@ -46,12 +52,13 @@ describe('NDV', () => {
});
it('should change input and go back to canvas', () => {
cy.createFixtureWorkflow('NDV-test-select-input.json', `NDV test select input ${uuid()}`);
cy.createFixtureWorkflow('NDV-test-select-input.json', 'NDV test select input');
workflowPage.actions.zoomToFit();
workflowPage.getters.canvasNodes().last().dblclick();
ndv.actions.switchInputMode('Table');
ndv.getters.inputSelect().click();
ndv.getters.inputOption().last().click();
ndv.getters.inputDataContainer().find('[class*=schema_]').should('exist');
ndv.getters.inputDataContainer().should('be.visible');
ndv.getters.inputDataContainer().should('contain', 'start');
ndv.getters.backToCanvas().click();
ndv.getters.container().should('not.be.visible');
@ -59,7 +66,7 @@ describe('NDV', () => {
});
it('should disconect Switch outputs if rules order was changed', () => {
cy.createFixtureWorkflow('NDV-test-switch_reorder.json', `NDV test switch reorder`);
cy.createFixtureWorkflow('NDV-test-switch_reorder.json', 'NDV test switch reorder');
workflowPage.actions.zoomToFit();
workflowPage.actions.executeWorkflow();
@ -105,13 +112,26 @@ describe('NDV', () => {
});
it('should show all validation errors when opening pasted node', () => {
cy.fixture('Test_workflow_ndv_errors.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
workflowPage.getters.canvasNodes().should('have.have.length', 1);
workflowPage.actions.openNode('Airtable');
cy.get('.has-issues').should('have.length', 3);
cy.get('[class*=hasIssues]').should('have.length', 1);
});
cy.createFixtureWorkflow('Test_workflow_ndv_errors.json', 'Validation errors');
workflowPage.getters.canvasNodes().should('have.have.length', 1);
workflowPage.actions.openNode('Airtable');
cy.get('.has-issues').should('have.length', 3);
cy.get('[class*=hasIssues]').should('have.length', 1);
});
it('should render run errors correctly', () => {
cy.createFixtureWorkflow('Test_workflow_ndv_run_error.json', 'Run error');
workflowPage.actions.openNode('Error');
ndv.actions.execute();
ndv.getters
.nodeRunErrorMessage()
.should('have.text', 'Info for expression missing from previous node');
ndv.getters
.nodeRunErrorDescription()
.should(
'contains.text',
"An expression here won't work because it uses .item and n8n can't figure out the matching item.",
);
});
it('should save workflow using keyboard shortcut from NDV', () => {
@ -135,7 +155,7 @@ describe('NDV', () => {
'prop2',
];
function setupSchemaWorkflow() {
cy.createFixtureWorkflow('Test_workflow_schema_test.json', `NDV test schema view ${uuid()}`);
cy.createFixtureWorkflow('Test_workflow_schema_test.json');
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
ndv.actions.execute();
@ -209,7 +229,7 @@ describe('NDV', () => {
it('should display large schema', () => {
cy.createFixtureWorkflow(
'Test_workflow_schema_test_pinned_data.json',
`NDV test schema view ${uuid()}`,
'NDV test schema view 2',
);
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
@ -231,6 +251,9 @@ describe('NDV', () => {
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Set3');
ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table');
ndv.getters
.inputRunSelector()
.should('exist')
@ -242,9 +265,6 @@ describe('NDV', () => {
.find('input')
.should('include.value', '2 of 2 (6 items)');
ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table');
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
ndv.getters.inputTbodyCell(1, 0).should('have.text', '1111');
@ -284,7 +304,7 @@ describe('NDV', () => {
it('should display parameter hints correctly', () => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`);
cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow 1');
workflowPage.actions.openNode('Set1');
ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions
@ -312,43 +332,11 @@ describe('NDV', () => {
}
ndv.getters.parameterInput('name').click(); // remove focus from input, hide expression preview
ndv.actions.validateExpressionPreview('value', output || input);
ndv.actions.validateExpressionPreview('value', output ?? input);
ndv.getters.parameterInput('value').clear();
});
});
it('should not retrieve remote options when required params throw errors', () => {
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
ndv.actions.setInvalidExpression({ fieldName: 'fieldId', delay: 200 });
ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview
ndv.getters.parameterInput('remoteOptions').click();
ndv.getters.parameterInputIssues('remoteOptions').realHover({ scrollBehavior: false });
// Remote options dropdown should not be visible
ndv.getters.parameterInput('remoteOptions').find('.el-select').should('not.exist');
});
it('should retrieve remote options when non-required params throw errors', () => {
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
ndv.getters.parameterInput('remoteOptions').click();
ndv.actions.setInvalidExpression({ fieldName: 'otherField', delay: 50 });
ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview
ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
});
it('should flag issues as soon as params are set', () => {
workflowPage.actions.addInitialNodeToCanvas('Webhook');
workflowPage.getters.canvasNodes().first().dblclick();
@ -395,7 +383,11 @@ describe('NDV', () => {
});
it('should not retrieve remote options when a parameter value changes', () => {
cy.intercept('/rest/dynamic-node-parameters/options?**', cy.spy().as('fetchParameterOptions'));
cy.intercept(
'POST',
'/rest/dynamic-node-parameters/options',
cy.spy().as('fetchParameterOptions'),
);
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
// Type something into the field
ndv.actions.typeIntoParameterInput('otherField', 'test');
@ -411,7 +403,7 @@ describe('NDV', () => {
}
it('should traverse floating nodes with mouse', () => {
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`);
cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes');
workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist');
@ -457,7 +449,7 @@ describe('NDV', () => {
});
it('should traverse floating nodes with keyboard', () => {
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`);
cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes');
workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist');
@ -548,21 +540,21 @@ describe('NDV', () => {
});
it('should show node name and version in settings', () => {
cy.createFixtureWorkflow('Test_workflow_ndv_version.json', `NDV test version ${uuid()}`);
cy.createFixtureWorkflow('Test_workflow_ndv_version.json', 'NDV test version');
workflowPage.actions.openNode('Edit Fields (old)');
ndv.actions.openSettings();
ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.3)');
ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.4)');
ndv.actions.close();
workflowPage.actions.openNode('Edit Fields (latest)');
ndv.actions.openSettings();
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.3 (Latest)');
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.4 (Latest)');
ndv.actions.close();
workflowPage.actions.openNode('Edit Fields (no typeVersion)');
ndv.actions.openSettings();
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.3 (Latest)');
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.4 (Latest)');
ndv.actions.close();
workflowPage.actions.openNode('Function');
@ -572,7 +564,7 @@ describe('NDV', () => {
});
it('Should render xml and html tags as strings and can search', () => {
cy.createFixtureWorkflow('Test_workflow_xml_output.json', `test`);
cy.createFixtureWorkflow('Test_workflow_xml_output.json', 'test');
workflowPage.actions.executeWorkflow();
@ -605,8 +597,7 @@ describe('NDV', () => {
ndv.getters.outputDisplayMode().find('label').eq(2).click({ force: true });
ndv.getters
.outputDataContainer()
.findChildByTestId('run-data-schema-item')
.find('> span')
.findChildByTestId('run-data-schema-item-value')
.should('include.text', '<?xml version="1.0" encoding="UTF-8"?>');
});
@ -627,7 +618,7 @@ describe('NDV', () => {
ndv.getters.backToCanvas().click();
workflowPage.actions.executeWorkflow();
// Manual tigger node should show success indicator
workflowPage.actions.openNode('When clicking "Test workflow"');
workflowPage.actions.openNode('When clicking Test workflow');
ndv.getters.nodeRunSuccessIndicator().should('exist');
// Code node should show error
ndv.getters.backToCanvas().click();
@ -673,6 +664,23 @@ describe('NDV', () => {
ndv.getters.parameterInput('operation').find('input').should('have.value', 'Delete');
});
it('Should show error state when remote options cannot be fetched', () => {
cy.intercept('POST', '/rest/dynamic-node-parameters/options', { statusCode: 500 }).as(
'parameterOptions',
);
workflowPage.actions.addInitialNodeToCanvas(NOTION_NODE_NAME, {
keepNdvOpen: true,
action: 'Update a database page',
});
ndv.actions.addItemToFixedCollection('propertiesUi');
ndv.getters
.parameterInput('key')
.find('input')
.should('have.value', 'Error fetching options from Notion');
});
it('Should open appropriate node creator after clicking on connection hint link', () => {
const nodeCreator = new NodeCreator();
const hintMapper = {
@ -685,7 +693,7 @@ describe('NDV', () => {
};
cy.createFixtureWorkflow(
'open_node_creator_for_connection.json',
`open_node_creator_for_connection ${uuid()}`,
'open_node_creator_for_connection',
);
Object.entries(hintMapper).forEach(([node, group]) => {
@ -696,20 +704,59 @@ describe('NDV', () => {
});
});
it('Stop listening for trigger event from NDV', () => {
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
workflowPage.actions.addInitialNodeToCanvas('Local File Trigger', {
keepNdvOpen: true,
action: 'On Changes To A Specific File',
isTrigger: true,
});
ndv.getters.triggerPanelExecuteButton().should('exist');
ndv.getters.triggerPanelExecuteButton().realClick();
ndv.getters.triggerPanelExecuteButton().should('contain', 'Stop Listening');
ndv.getters.triggerPanelExecuteButton().realClick();
cy.wait('@workflowRun').then(() => {
ndv.getters.triggerPanelExecuteButton().should('contain', 'Test step');
workflowPage.getters.successToast().should('exist');
it('should allow selecting item for expressions', () => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow 2');
workflowPage.actions.openNode('Set');
ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions
ndv.actions.typeIntoParameterInput('value', '{{', {
parseSpecialCharSequences: false,
});
ndv.actions.typeIntoParameterInput('value', '$json.input[0].count');
ndv.getters.inlineExpressionEditorOutput().should('have.text', '0');
ndv.actions.expressionSelectNextItem();
ndv.getters.inlineExpressionEditorOutput().should('have.text', '1');
ndv.getters.inlineExpressionEditorItemInput().should('have.value', '1');
ndv.getters.inlineExpressionEditorItemNextButton().should('be.disabled');
ndv.actions.expressionSelectPrevItem();
ndv.getters.inlineExpressionEditorOutput().should('have.text', '0');
ndv.getters.inlineExpressionEditorItemInput().should('have.value', '0');
ndv.getters.inlineExpressionEditorItemPrevButton().should('be.disabled');
ndv.actions.expressionSelectItem(1);
ndv.getters.inlineExpressionEditorOutput().should('have.text', '1');
});
it('should show data from the correct output in schema view', () => {
cy.createFixtureWorkflow('Test_workflow_multiple_outputs.json');
workflowPage.actions.zoomToFit();
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Only Item 1');
ndv.getters.inputPanel().should('be.visible');
ndv.getters
.inputPanel()
.find('[data-test-id=run-data-schema-item]')
.should('contain.text', 'onlyOnItem1');
ndv.actions.close();
workflowPage.actions.openNode('Only Item 2');
ndv.getters.inputPanel().should('be.visible');
ndv.getters
.inputPanel()
.find('[data-test-id=run-data-schema-item]')
.should('contain.text', 'onlyOnItem2');
ndv.actions.close();
workflowPage.actions.openNode('Only Item 3');
ndv.getters.inputPanel().should('be.visible');
ndv.getters
.inputPanel()
.find('[data-test-id=run-data-schema-item]')
.should('contain.text', 'onlyOnItem3');
});
});

View file

@ -1,5 +1,7 @@
import { nanoid } from 'nanoid';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV } from '../pages/ndv';
import { successToast } from '../pages/notifications';
const WorkflowPage = new WorkflowPageClass();
const ndv = new NDV();
@ -28,13 +30,13 @@ describe('Code node', () => {
it('should execute the placeholder successfully in both modes', () => {
ndv.actions.execute();
WorkflowPage.getters.successToast().contains('Node executed successfully');
successToast().contains('Node executed successfully');
ndv.getters.parameterInput('mode').click();
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
ndv.actions.execute();
WorkflowPage.getters.successToast().contains('Node executed successfully');
successToast().contains('Node executed successfully');
});
});
@ -84,7 +86,7 @@ describe('Code node', () => {
cy.getByTestId('ask-ai-cta-tooltip-no-prompt').should('exist');
cy.getByTestId('ask-ai-prompt-input')
// Type random 14 character string
.type([...Array(14)].map(() => ((Math.random() * 36) | 0).toString(36)).join(''));
.type(nanoid(14));
cy.getByTestId('ask-ai-cta').realHover();
cy.getByTestId('ask-ai-cta-tooltip-prompt-too-short').should('exist');
@ -92,14 +94,14 @@ describe('Code node', () => {
cy.getByTestId('ask-ai-prompt-input')
.clear()
// Type random 15 character string
.type([...Array(15)].map(() => ((Math.random() * 36) | 0).toString(36)).join(''));
.type(nanoid(15));
cy.getByTestId('ask-ai-cta').should('be.enabled');
cy.getByTestId('ask-ai-prompt-counter').should('contain.text', '15 / 600');
});
it('should send correct schema and replace code', () => {
const prompt = [...Array(20)].map(() => ((Math.random() * 36) | 0).toString(36)).join('');
const prompt = nanoid(20);
cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious();
@ -129,7 +131,7 @@ describe('Code node', () => {
});
it('should show error based on status code', () => {
const prompt = [...Array(20)].map(() => ((Math.random() * 36) | 0).toString(36)).join('');
const prompt = nanoid(20);
cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious();

View file

@ -1,16 +1,15 @@
import {
CODE_NODE_NAME,
MANUAL_TRIGGER_NODE_NAME,
META_KEY,
SCHEDULE_TRIGGER_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
INSTANCE_MEMBERS,
INSTANCE_OWNER,
NOTION_NODE_NAME,
} from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { getVisibleSelect } from '../utils';
import { WorkflowExecutionsTab } from '../pages';
import { errorToast, successToast } from '../pages/notifications';
const NEW_WORKFLOW_NAME = 'Something else';
const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow';
@ -35,6 +34,20 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.isWorkflowSaved();
});
it('should not save already saved workflow', () => {
cy.intercept('PATCH', '/rest/workflows/*').as('saveWorkflow');
WorkflowPage.actions.saveWorkflowOnButtonClick();
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.saveWorkflowOnButtonClick();
cy.wait('@saveWorkflow');
WorkflowPage.getters.isWorkflowSaved();
// Try to save a few times
WorkflowPage.actions.saveWorkflowUsingKeyboardShortcut();
WorkflowPage.actions.saveWorkflowUsingKeyboardShortcut();
// Should be saved only once
cy.get('@saveWorkflow.all').should('have.length', 1);
});
it('should not be able to activate unsaved workflow', () => {
WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled');
});
@ -53,6 +66,30 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.isWorkflowActivated();
});
it('should not be be able to activate workflow when nodes have errors', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
WorkflowPage.actions.saveWorkflowOnButtonClick();
successToast().should('exist');
WorkflowPage.actions.clickWorkflowActivator();
errorToast().should('exist');
});
it('should be be able to activate workflow when nodes with errors are disabled', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
WorkflowPage.actions.saveWorkflowOnButtonClick();
successToast().should('exist');
// First, try to activate the workflow with errors
WorkflowPage.actions.clickWorkflowActivator();
errorToast().should('exist');
// Now, disable the node with errors
WorkflowPage.getters.canvasNodes().last().click();
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.actions.activateWorkflow();
WorkflowPage.getters.isWorkflowActivated();
});
it('should save new workflow after renaming', () => {
WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME);
WorkflowPage.getters.isWorkflowSaved();
@ -96,13 +133,13 @@ describe('Workflow Actions', () => {
);
cy.reload();
cy.get('.el-loading-mask').should('exist');
cy.get('body').type(META_KEY, { release: false }).type('s');
cy.get('body').type(META_KEY, { release: false }).type('s');
cy.get('body').type(META_KEY, { release: false }).type('s');
WorkflowPage.actions.hitSaveWorkflow();
WorkflowPage.actions.hitSaveWorkflow();
WorkflowPage.actions.hitSaveWorkflow();
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0));
cy.waitForLoad();
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
cy.get('body').type(META_KEY, { release: false }).type('s');
WorkflowPage.actions.hitSaveWorkflow();
cy.wait('@saveWorkflow');
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1));
});
@ -132,10 +169,11 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
cy.get('#node-creator').should('not.exist');
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('a');
WorkflowPage.actions.hitSelectAll();
cy.get('.jtk-drag-selected').should('have.length', 2);
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('c');
WorkflowPage.getters.successToast().should('exist');
WorkflowPage.actions.hitCopy();
successToast().should('exist');
});
it('should paste nodes (both current and old node versions)', () => {
@ -147,47 +185,62 @@ describe('Workflow Actions', () => {
});
});
it('should allow importing nodes without names', () => {
cy.fixture('Test_workflow-actions_import_nodes_empty_name.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
WorkflowPage.actions.zoomToFit();
WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 2);
// Check if all nodes have names
WorkflowPage.getters.canvasNodes().each((node) => {
cy.wrap(node).should('have.attr', 'data-name');
});
});
});
it('should update workflow settings', () => {
cy.visit(WorkflowPages.url);
WorkflowPages.getters.workflowCards().then((cards) => {
const totalWorkflows = cards.length;
cy.intercept('GET', '/rest/workflows', (req) => {
req.on('response', (res) => {
const totalWorkflows = res.body.count ?? 0;
WorkflowPage.actions.visit();
// Open settings dialog
WorkflowPage.actions.saveWorkflowOnButtonClick();
WorkflowPage.getters.workflowMenu().should('be.visible');
WorkflowPage.getters.workflowMenu().click();
WorkflowPage.getters.workflowMenuItemSettings().should('be.visible');
WorkflowPage.getters.workflowMenuItemSettings().click();
// Change all settings
// totalWorkflows + 1 (current workflow) + 1 (no workflow option)
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', totalWorkflows + 2);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsTimezoneSelect().click();
getVisibleSelect().find('li').should('exist');
getVisibleSelect().find('li').eq(1).click({ force: true });
WorkflowPage.getters.workflowSettingsSaveFiledExecutionsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsSaveSuccessExecutionsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsSaveManualExecutionsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsSaveExecutionProgressSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click();
WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1');
// Save settings
WorkflowPage.getters.workflowSettingsSaveButton().click();
WorkflowPage.getters.workflowSettingsModal().should('not.exist');
WorkflowPage.getters.successToast().should('exist');
});
WorkflowPage.actions.visit();
// Open settings dialog
WorkflowPage.actions.saveWorkflowOnButtonClick();
WorkflowPage.getters.workflowMenu().should('be.visible');
WorkflowPage.getters.workflowMenu().click();
WorkflowPage.getters.workflowMenuItemSettings().should('be.visible');
WorkflowPage.getters.workflowMenuItemSettings().click();
// Change all settings
// totalWorkflows + 1 (current workflow) + 1 (no workflow option)
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', totalWorkflows + 2);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsTimezoneSelect().click();
getVisibleSelect().find('li').should('exist');
getVisibleSelect().find('li').eq(1).click({ force: true });
WorkflowPage.getters.workflowSettingsSaveFiledExecutionsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsSaveSuccessExecutionsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsSaveManualExecutionsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsSaveExecutionProgressSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click();
WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1');
// Save settings
WorkflowPage.getters.workflowSettingsSaveButton().click();
WorkflowPage.getters.workflowSettingsModal().should('not.exist');
successToast().should('exist');
});
}).as('loadWorkflows');
});
it('should not be able to delete unsaved workflow', () => {
@ -203,8 +256,8 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.workflowMenuItemDelete().click();
cy.get('div[role=dialog][aria-modal=true]').should('be.visible');
cy.get('button.btn--confirm').should('be.visible').click();
WorkflowPage.getters.successToast().should('exist');
cy.url().should('include', '/workflow/new');
successToast().should('exist');
cy.url().should('include', WorkflowPages.url);
});
describe('duplicate workflow', () => {
@ -232,7 +285,7 @@ describe('Workflow Actions', () => {
.contains('Duplicate')
.should('be.visible');
WorkflowPage.getters.duplicateWorkflowModal().find('button').contains('Duplicate').click();
WorkflowPage.getters.errorToast().should('not.exist');
errorToast().should('not.exist');
}
beforeEach(() => {
@ -272,18 +325,43 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.canvasNodePlusEndpointByName(EDIT_FIELDS_SET_NODE_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
});
it('should run workflow on button click', () => {
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.saveWorkflowOnButtonClick();
WorkflowPage.getters.executeWorkflowButton().click();
successToast().should('contain.text', 'Workflow executed successfully');
});
it('should run workflow using keyboard shortcut', () => {
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.saveWorkflowOnButtonClick();
WorkflowPage.actions.hitExecuteWorkflow();
successToast().should('contain.text', 'Workflow executed successfully');
});
it('should not run empty workflows', () => {
// Clear the canvas
WorkflowPage.actions.hitDeleteAllNodes();
WorkflowPage.getters.canvasNodes().should('have.length', 0);
// Button should be disabled
WorkflowPage.getters.executeWorkflowButton().should('be.disabled');
// Keyboard shortcut should not work
WorkflowPage.actions.hitExecuteWorkflow();
successToast().should('not.exist');
});
});
describe('Menu entry Push To Git', () => {
it('should not show up in the menu for members', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(WorkflowPages.url);
WorkflowPage.actions.visit();
WorkflowPage.getters.workflowMenuItemGitPush().should('not.exist');
});
it('should show up for owners', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.visit(WorkflowPages.url);
WorkflowPage.actions.visit();
WorkflowPage.getters.workflowMenuItemGitPush().should('exist');

View file

@ -8,28 +8,29 @@ describe('Expression editor modal', () => {
beforeEach(() => {
WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Schedule');
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError');
cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
});
describe('Static data', () => {
beforeEach(() => {
WorkflowPage.actions.addNodeToCanvas('Hacker News');
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.openNode('Hacker News');
WorkflowPage.actions.openExpressionEditorModal();
});
it('should resolve primitive resolvables', () => {
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ 1 + 2');
WorkflowPage.getters.expressionModalInput().click().type('{{ 1 + 2');
WorkflowPage.getters.expressionModalOutput().contains(/^3$/);
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ "ab" + "cd"');
WorkflowPage.getters.expressionModalInput().click().type('{{ "ab" + "cd"');
WorkflowPage.getters.expressionModalOutput().contains(/^abcd$/);
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ true && false');
WorkflowPage.getters.expressionModalInput().click().type('{{ true && false');
WorkflowPage.getters.expressionModalOutput().contains(/^false$/);
});
@ -37,6 +38,7 @@ describe('Expression editor modal', () => {
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters
.expressionModalInput()
.click()
.type('{{ { a : 1 }', { parseSpecialCharSequences: false });
WorkflowPage.getters.expressionModalOutput().contains(/^\[Object: \{"a": 1\}\]$/);
@ -44,18 +46,19 @@ describe('Expression editor modal', () => {
WorkflowPage.getters
.expressionModalInput()
.click()
.type('{{ { a : 1 }.a', { parseSpecialCharSequences: false });
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
});
it('should resolve array resolvables', () => {
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3]');
WorkflowPage.getters.expressionModalInput().click().type('{{ [1, 2, 3]');
WorkflowPage.getters.expressionModalOutput().contains(/^\[Array: \[1,2,3\]\]$/);
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3][0]');
WorkflowPage.getters.expressionModalInput().click().type('{{ [1, 2, 3][0]');
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
});
});
@ -67,30 +70,34 @@ describe('Expression editor modal', () => {
ndv.actions.close();
WorkflowPage.actions.addNodeToCanvas('No Operation');
WorkflowPage.actions.addNodeToCanvas('Hacker News');
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.openNode('Hacker News');
WorkflowPage.actions.openExpressionEditorModal();
});
it('should resolve $parameter[]', () => {
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]');
WorkflowPage.getters.expressionModalInput().click().type('{{ $parameter["operation"]');
WorkflowPage.getters.expressionModalOutput().should('have.text', 'getAll');
});
it('should resolve input: $json,$input,$(nodeName)', () => {
// Previous nodes have not run, input is empty
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ $json.myStr');
WorkflowPage.getters.expressionModalInput().click().type('{{ $json.myStr');
WorkflowPage.getters
.expressionModalOutput()
.should('have.text', '[Execute previous nodes for preview]');
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ $input.item.json.myStr');
WorkflowPage.getters.expressionModalInput().click().type('{{ $input.item.json.myStr');
WorkflowPage.getters
.expressionModalOutput()
.should('have.text', '[Execute previous nodes for preview]');
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type("{{ $('Schedule Trigger').item.json.myStr");
WorkflowPage.getters
.expressionModalInput()
.click()
.type("{{ $('Schedule Trigger').item.json.myStr");
WorkflowPage.getters
.expressionModalOutput()
.should('have.text', '[Execute previous nodes for preview]');
@ -104,13 +111,16 @@ describe('Expression editor modal', () => {
// Previous nodes have run, input can be resolved
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ $json.myStr');
WorkflowPage.getters.expressionModalInput().click().type('{{ $json.myStr');
WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday');
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ $input.item.json.myStr');
WorkflowPage.getters.expressionModalInput().click().type('{{ $input.item.json.myStr');
WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday');
WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type("{{ $('Schedule Trigger').item.json.myStr");
WorkflowPage.getters
.expressionModalInput()
.click()
.type("{{ $('Schedule Trigger').item.json.myStr");
WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday');
});
});

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "d0eda550-2526-42a1-aa19-dee411c8acf9",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -91,7 +91,7 @@
],
"pinData": {},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "369fe424-dd3b-4399-9de3-50bd4ce1f75b",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -570,7 +570,7 @@
],
"pinData": {},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "5ae8991f-08a2-4b27-b61c-85e3b8a83693",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -14,7 +14,7 @@
},
{
"parameters": {
"url": "https://random-data-api.com/api/v2/users?size=5",
"url": "https://internal.users.n8n.cloud/webhook/random-data-api",
"options": {}
},
"id": "22511d75-ab54-49e1-b8af-08b8b3372373",
@ -28,7 +28,7 @@
},
{
"parameters": {
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.first_name,\n firstnNameReversed: item.json.first_name_BUG.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();"
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.firstname,\n firstnNameReversed: item.json.firstname.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();"
},
"id": "4b66b15a-1685-46c1-a5e3-ebf8cdb11d21",
"name": "do something with them",
@ -78,14 +78,14 @@
}
}
],
"When clicking \"Test workflow\"": [
"When clicking Test workflow": [
{
"json": {}
}
]
},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{
@ -130,4 +130,4 @@
},
"id": "PymcwIrbqgNh3O0K",
"tags": []
}
}

View file

@ -0,0 +1,48 @@
{
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "5b397bc122efafc165b2a6e67d5e8d75b8138f0d24d6352fac713e4845b002a6"
},
"nodes": [
{
"parameters": {},
"id": "df260de7-6f28-4d07-b7b5-29588e27335b",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
780,
500
]
},
{
"parameters": {
"category": "randomData",
"randomDataSeed": "0",
"randomDataCount": 100
},
"id": "9e9a0708-86dc-474f-a60e-4315e757c08e",
"name": "DebugHelper",
"type": "n8n-nodes-base.debugHelper",
"typeVersion": 1,
"position": [
1000,
500
]
}
],
"connections": {
"When clicking \"Test workflow\"": {
"main": [
[
{
"node": "DebugHelper",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}

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