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.* packages/**/*.test.*
.git .git
.github .github
!.github/scripts
*.tsbuildinfo *.tsbuildinfo
packages/cli/dist/**/e2e.* packages/cli/dist/**/e2e.*
docker/compose docker/compose

View file

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

View file

@ -1,16 +1,26 @@
## Summary ## 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 ## 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. - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created.
- [ ] Tests included. - [ ] Tests included. <!--
> A bug is not considered fixed, unless a test is added to prevent it from happening again. A bug is not considered fixed, unless a test is added to prevent it from happening again.
> A feature is not complete without tests. 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 - `test` - Adding missing tests or correcting existing tests
- `docs` - Documentation only changes - `docs` - Documentation only changes
- `refactor` - A code change that neither fixes a bug nor adds a feature - `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) - `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. 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 { writeFileSync } = require('fs');
const { resolve } = require('path'); const { resolve } = require('path');
const baseDir = resolve(__dirname, '..'); const baseDir = resolve(__dirname, '../..');
const trimPackageJson = (packageName) => { const trimPackageJson = (packageName) => {
const filePath = resolve(baseDir, 'packages', packageName, 'package.json'); const filePath = resolve(baseDir, 'packages', packageName, 'package.json');
const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require( const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require(
filePath, filePath,
); );
if (packageName === '@n8n/chat') {
packageJson.dependencies = dependencies;
}
writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8'); writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
}; };

View file

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

View file

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

View file

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

View file

@ -1,26 +1,59 @@
name: Chromatic name: Chromatic
on: on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch: 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: jobs:
chromatic: chromatic:
if: ${{ github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'community') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version: 18.x node-version: 20.x
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Publish to Chromatic - name: Publish to Chromatic
uses: chromaui/action@latest uses: chromaui/action@v11
id: chromatic_tests
continue-on-error: true
with: with:
workingDir: packages/design-system workingDir: packages/design-system
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 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: install-and-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 10
strategy:
matrix:
node-version: [18.x, 20.x]
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
- run: corepack enable - run: corepack enable
- name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: 20.x
cache: pnpm cache: pnpm
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build - name: Build
run: pnpm build run: pnpm build
@ -35,7 +33,7 @@ jobs:
uses: actions/cache/save@v4.0.0 uses: actions/cache/save@v4.0.0
with: with:
path: ./packages/**/dist path: ./packages/**/dist
key: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint key: ${{ github.sha }}-base:build
unit-test: unit-test:
name: Unit tests name: Unit tests
@ -43,46 +41,22 @@ jobs:
needs: install-and-build needs: install-and-build
strategy: strategy:
matrix: matrix:
node-version: [18.x, 20.x] node-version: [18.x, 20.x, 22.4]
with: with:
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
nodeVersion: ${{ matrix.node-version }} nodeVersion: ${{ matrix.node-version }}
cacheKey: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint cacheKey: ${{ github.sha }}-base:build
collectCoverage: true collectCoverage: ${{ matrix.node-version == '20.x' }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
lint: lint:
name: Lint changes name: Lint
runs-on: ubuntu-latest uses: ./.github/workflows/linting-reusable.yml
needs: install-and-build needs: install-and-build
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
cacheKey: ${{ github.sha }}-base:build
- 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
notify-on-failure: notify-on-failure:
name: Notify Slack on failure name: Notify Slack on failure

View file

@ -7,6 +7,7 @@ on:
pull_request: pull_request:
paths: paths:
- packages/cli/src/databases/** - packages/cli/src/databases/**
- .github/workflows/ci-postgres-mysql.yml
concurrency: concurrency:
group: db-${{ github.event.pull_request.number || github.ref }} group: db-${{ github.event.pull_request.number || github.ref }}
@ -19,12 +20,15 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version: 18.x node-version: 20.x
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build Backend - name: Build Backend
run: pnpm build:backend run: pnpm build:backend
@ -45,12 +49,15 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version: 18.x node-version: 20.x
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Restore cached build artifacts - name: Restore cached build artifacts
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.0
with: with:
@ -59,7 +66,7 @@ jobs:
- name: Test SQLite Pooled - name: Test SQLite Pooled
working-directory: packages/cli working-directory: packages/cli
run: pnpm jest --coverage run: pnpm jest
mysql: mysql:
name: MySQL name: MySQL
@ -71,12 +78,15 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version: 18.x node-version: 20.x
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Restore cached build artifacts - name: Restore cached build artifacts
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.0
with: with:
@ -92,7 +102,7 @@ jobs:
- name: Test MySQL - name: Test MySQL
working-directory: packages/cli working-directory: packages/cli
run: pnpm test:mysql run: pnpm test:mysql --testTimeout 20000
postgres: postgres:
name: Postgres name: Postgres
@ -101,15 +111,19 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
env: env:
DB_POSTGRESDB_PASSWORD: password DB_POSTGRESDB_PASSWORD: password
DB_POSTGRESDB_POOL_SIZE: 1 # Detect connection pooling deadlocks
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version: 18.x node-version: 20.x
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Restore cached build artifacts - name: Restore cached build artifacts
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.0
with: with:

View file

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

View file

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

View file

@ -40,7 +40,7 @@ on:
containers: containers:
description: 'Number of containers to run tests in.' description: 'Number of containers to run tests in.'
required: false 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 type: string
pr_number: pr_number:
description: 'PR number to run tests for.' description: 'PR number to run tests for.'
@ -87,7 +87,7 @@ jobs:
git fetch origin pull/${{ inputs.pr_number }}/head git fetch origin pull/${{ inputs.pr_number }}/head
git checkout FETCH_HEAD git checkout FETCH_HEAD
- uses: pnpm/action-setup@v2.4.0 - uses: pnpm/action-setup@v4.0.0
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@ -99,10 +99,9 @@ jobs:
runTests: false runTests: false
install: false install: false
build: pnpm build build: pnpm build
env:
VUE_APP_MAX_PINNED_DATA_SIZE: 16384
- name: Cypress install - name: Cypress install
working-directory: cypress
run: pnpm cypress:install run: pnpm cypress:install
- name: Cache build artifacts - name: Cache build artifacts
@ -138,7 +137,7 @@ jobs:
git fetch origin pull/${{ inputs.pr_number }}/head git fetch origin pull/${{ inputs.pr_number }}/head
git checkout FETCH_HEAD git checkout FETCH_HEAD
- uses: pnpm/action-setup@v2.4.0 - uses: pnpm/action-setup@v4.0.0
- name: Restore cached pnpm modules - name: Restore cached pnpm modules
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.0
@ -155,6 +154,7 @@ jobs:
- name: Cypress run - name: Cypress run
uses: cypress-io/github-action@v6.6.1 uses: cypress-io/github-action@v6.6.1
with: with:
working-directory: cypress
install: false install: false
start: pnpm start start: pnpm start
wait-on: 'http://localhost:5678' wait-on: 'http://localhost:5678'
@ -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 # 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 # in the same parent workflow
ci-build-id: ${{ needs.prepare.outputs.uuid }} ci-build-id: ${{ needs.prepare.outputs.uuid }}
spec: '/__w/n8n/n8n/cypress/${{ inputs.spec }}' spec: '${{ inputs.spec }}'
config-file: /__w/n8n/n8n/cypress.config.js
env: env:
NODE_OPTIONS: --dns-result-order=ipv4first NODE_OPTIONS: --dns-result-order=ipv4first
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

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 }} ref: ${{ github.event.inputs.base-branch }}
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version: 18.x node-version: 20.x
- run: npm install --prefix=.github/scripts --no-package-lock - run: npm install --prefix=.github/scripts --no-package-lock

View file

@ -14,8 +14,11 @@ jobs:
permissions: permissions:
contents: write contents: write
id-token: write
timeout-minutes: 60 timeout-minutes: 60
env:
NPM_CONFIG_PROVENANCE: true
steps: steps:
- name: Checkout - name: Checkout
@ -24,9 +27,9 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version: 18.x node-version: 20.x
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
@ -42,7 +45,8 @@ jobs:
- name: Publish to NPM - name: Publish to NPM
run: | run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc 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 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 pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks
npm dist-tag rm n8n rc npm dist-tag rm n8n rc

View file

@ -22,9 +22,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version: 18.x node-version: 20.x
- run: | - run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm dist-tag add n8n@${{ github.event.inputs.version }} ${{ github.event.inputs.release-channel }} npm dist-tag add n8n@${{ github.event.inputs.version }} ${{ github.event.inputs.release-channel }}
@ -53,3 +53,11 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} 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 }} - 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 - run: corepack enable
working-directory: n8n working-directory: n8n
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version: 18.x node-version: 20.x
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: 'n8n/pnpm-lock.yaml' cache-dependency-path: 'n8n/pnpm-lock.yaml'

View file

@ -4,24 +4,28 @@ on:
workflow_call: workflow_call:
inputs: inputs:
ref: ref:
description: 'GitHub ref to test.' description: GitHub ref to test.
required: false required: false
type: string type: string
default: 'master' default: master
nodeVersion: nodeVersion:
description: 'Version of node to use.' description: Version of node to use.
required: false required: false
type: string type: string
default: '18.x' default: 20.x
cacheKey: cacheKey:
description: 'Cache key for modules and build artifacts.' description: Cache key for modules and build artifacts.
required: false required: false
default: '' default: ''
type: string type: string
collectCoverage: collectCoverage:
required: false required: false
default: 'false' default: false
type: string type: boolean
secrets:
CODECOV_TOKEN:
description: 'Codecov upload token.'
required: false
jobs: jobs:
unit-test: unit-test:
@ -37,7 +41,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Use Node.js ${{ inputs.nodeVersion }} - name: Use Node.js ${{ inputs.nodeVersion }}
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.2
with: with:
node-version: ${{ inputs.nodeVersion }} node-version: ${{ inputs.nodeVersion }}
cache: pnpm cache: pnpm
@ -45,6 +49,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build - name: Build
if: ${{ inputs.cacheKey == '' }} if: ${{ inputs.cacheKey == '' }}
run: pnpm build run: pnpm build
@ -66,7 +73,7 @@ jobs:
run: pnpm test:frontend run: pnpm test:frontend
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: ${{ inputs.collectCoverage == 'true' }} if: inputs.collectCoverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4.5.0
with: 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 nodelinter.config.json
**/package-lock.json **/package-lock.json
packages/**/.turbo packages/**/.turbo
.turbo
*.tsbuildinfo *.tsbuildinfo
cypress/videos/*
cypress/screenshots/*
cypress/downloads/*
*.swp *.swp
CHANGELOG-*.md CHANGELOG-*.md
*.mdx *.mdx

4
.npmrc
View file

@ -7,4 +7,8 @@ prefer-workspace-packages = true
link-workspace-packages = deep link-workspace-packages = deep
hoist = true hoist = true
shamefully-hoist = true shamefully-hoist = true
hoist-workspace-packages = false
loglevel = warn 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) # [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) - [Code of conduct](#code-of-conduct)
- [Directory structure](#directory-structure) - [Directory structure](#directory-structure)
- [Development setup](#development-setup) - [Development setup](#development-setup)
- [Dev Container](#dev-container)
- [Requirements](#requirements) - [Requirements](#requirements)
- [Node.js](#nodejs) - [Node.js](#nodejs)
- [pnpm](#pnpm) - [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: The most important directories:
- [/docker/image](/docker/images) - Dockerfiles to create n8n containers - [/docker/image](/docker/images) - Dockerfiles to create n8n containers
- [/docker/compose](/docker/compose) - Examples Docker Setups
- [/packages](/packages) - The different n8n modules - [/packages](/packages) - The different n8n modules
- [/packages/cli](/packages/cli) - CLI code to run front- & backend - [/packages/cli](/packages/cli) - CLI code to run front- & backend
- [/packages/core](/packages/core) - Core code which handles workflow - [/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 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: 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 ### Requirements
#### Node.js #### 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
[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 ##### 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 //#region Actions
export const interceptCtaRequestWithResponse = (becomeCreator: boolean) => { export const interceptCtaRequestWithResponse = (becomeCreator: boolean) => {
return cy.intercept('GET', `/rest/cta/become-creator`, { return cy.intercept('GET', '/rest/cta/become-creator', {
body: becomeCreator, body: becomeCreator,
}); });
}; };

View file

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

View file

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

View file

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

View file

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

View file

@ -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 * Getters
*/ */
export const getSetupWorkflowCredentialsButton = () => cy.get(`button:contains("Set up template")`); export const getSetupWorkflowCredentialsButton = () => cy.get('button:contains("Set up template")');

View file

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

View file

@ -35,7 +35,7 @@ export const INSTANCE_MEMBERS = [
]; ];
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; 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 MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code'; 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 BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain';
export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory'; export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory';
export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator'; 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_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_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 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_GOOGLE_ACCOUNT_NAME = 'Gmail account';
export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account'; export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account';

View file

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

View file

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

View file

@ -8,45 +8,46 @@ describe('Inline expression editor', () => {
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Schedule'); WorkflowPage.actions.addInitialNodeToCanvas('Schedule');
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError'); cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
}); });
describe('Static data', () => { describe('Static data', () => {
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.addNodeToCanvas('Hacker News'); WorkflowPage.actions.addNodeToCanvas('Hacker News');
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.openNode('Hacker News'); WorkflowPage.actions.openNode('Hacker News');
WorkflowPage.actions.openInlineExpressionEditor(); WorkflowPage.actions.openInlineExpressionEditor();
}); });
it('should resolve primitive resolvables', () => { it('should resolve primitive resolvables', () => {
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('1 + 2'); WorkflowPage.getters.inlineExpressionEditorInput().type('1 + 2');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^3$/); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^3$/);
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('"ab"'); WorkflowPage.getters.inlineExpressionEditorInput().type('"ab"');
WorkflowPage.getters.inlineExpressionEditorInput().type('{rightArrow}+'); WorkflowPage.getters.inlineExpressionEditorInput().type('{rightArrow}+');
WorkflowPage.getters.inlineExpressionEditorInput().type('"cd"'); WorkflowPage.getters.inlineExpressionEditorInput().type('"cd"');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^abcd$/); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^abcd$/);
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('true && false'); WorkflowPage.getters.inlineExpressionEditorInput().type('true && false');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^false$/); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^false$/);
}); });
it('should resolve object resolvables', () => { it('should resolve object resolvables', () => {
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters WorkflowPage.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.type('{ a: 1 }', { parseSpecialCharSequences: false }); .type('{ a: 1 }', { parseSpecialCharSequences: false });
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Object: \{"a": 1\}\]$/); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Object: \{"a": 1\}\]$/);
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters WorkflowPage.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.type('{ a: 1 }.a', { parseSpecialCharSequences: false }); .type('{ a: 1 }.a', { parseSpecialCharSequences: false });
@ -55,13 +56,13 @@ describe('Inline expression editor', () => {
it('should resolve array resolvables', () => { it('should resolve array resolvables', () => {
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]'); WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Array: \[1,2,3\]\]$/); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Array: \[1,2,3\]\]$/);
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]'); WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
WorkflowPage.getters.inlineExpressionEditorInput().type('[0]'); WorkflowPage.getters.inlineExpressionEditorInput().type('[0]');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/);
@ -75,13 +76,14 @@ describe('Inline expression editor', () => {
ndv.actions.close(); ndv.actions.close();
WorkflowPage.actions.addNodeToCanvas('No Operation'); WorkflowPage.actions.addNodeToCanvas('No Operation');
WorkflowPage.actions.addNodeToCanvas('Hacker News'); WorkflowPage.actions.addNodeToCanvas('Hacker News');
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.openNode('Hacker News'); WorkflowPage.actions.openNode('Hacker News');
WorkflowPage.actions.openInlineExpressionEditor(); WorkflowPage.actions.openInlineExpressionEditor();
}); });
it('should resolve $parameter[]', () => { it('should resolve $parameter[]', () => {
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
// Resolving $parameter is slow, especially on CI runner // Resolving $parameter is slow, especially on CI runner
WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]'); WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]');
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'getAll'); WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'getAll');
@ -90,19 +92,19 @@ describe('Inline expression editor', () => {
it('should resolve input: $json,$input,$(nodeName)', () => { it('should resolve input: $json,$input,$(nodeName)', () => {
// Previous nodes have not run, input is empty // Previous nodes have not run, input is empty
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr'); WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr');
WorkflowPage.getters WorkflowPage.getters
.inlineExpressionEditorOutput() .inlineExpressionEditorOutput()
.should('have.text', '[Execute previous nodes for preview]'); .should('have.text', '[Execute previous nodes for preview]');
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr'); WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr');
WorkflowPage.getters WorkflowPage.getters
.inlineExpressionEditorOutput() .inlineExpressionEditorOutput()
.should('have.text', '[Execute previous nodes for preview]'); .should('have.text', '[Execute previous nodes for preview]');
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters WorkflowPage.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.type("$('Schedule Trigger').item.json.myStr"); .type("$('Schedule Trigger').item.json.myStr");
@ -118,15 +120,15 @@ describe('Inline expression editor', () => {
// Previous nodes have run, input can be resolved // Previous nodes have run, input can be resolved
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr'); WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr');
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday'); WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday');
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr'); WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr');
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday'); WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday');
WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters WorkflowPage.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.type("$('Schedule Trigger').item.json.myStr"); .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 { import {
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME,
@ -7,7 +9,6 @@ import {
IF_NODE_NAME, IF_NODE_NAME,
HTTP_REQUEST_NODE_NAME, HTTP_REQUEST_NODE_NAME,
} from './../constants'; } from './../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
describe('Canvas Actions', () => { describe('Canvas Actions', () => {
@ -124,6 +125,8 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 2);
WorkflowPage.actions.addNodeBetweenNodes( WorkflowPage.actions.addNodeBetweenNodes(
CODE_NODE_NAME, CODE_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME,
@ -131,12 +134,15 @@ describe('Canvas Actions', () => {
); );
WorkflowPage.getters.canvasNodes().should('have.length', 4); WorkflowPage.getters.canvasNodes().should('have.length', 4);
WorkflowPage.getters.nodeConnections().should('have.length', 3); WorkflowPage.getters.nodeConnections().should('have.length', 3);
// And last node should be pushed to the right
WorkflowPage.getters WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
.canvasNodes() const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left'));
.last()
.should('have.css', 'left', '860px') WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
.should('have.css', 'top', '220px'); const httpNodeLeft = parseFloat($httpNode.css('left'));
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
});
});
}); });
it('should delete connections by pressing the delete button', () => { it('should delete connections by pressing the delete button', () => {
@ -166,8 +172,8 @@ describe('Canvas Actions', () => {
.findChildByTestId('execute-node-button') .findChildByTestId('execute-node-button')
.click({ force: true }); .click({ force: true });
WorkflowPage.actions.executeNode(CODE_NODE_NAME); WorkflowPage.actions.executeNode(CODE_NODE_NAME);
WorkflowPage.getters.successToast().should('have.length', 2); successToast().should('have.length', 2);
WorkflowPage.getters.successToast().should('contain.text', 'Node executed successfully'); successToast().should('contain.text', 'Node executed successfully');
}); });
it('should disable and enable node', () => { it('should disable and enable node', () => {
@ -198,19 +204,19 @@ describe('Canvas Actions', () => {
it('should copy selected nodes', () => { it('should copy selected nodes', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.selectAll(); WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitCopy(); WorkflowPage.actions.hitCopy();
WorkflowPage.getters.successToast().should('contain', 'Copied!'); successToast().should('contain', 'Copied!');
WorkflowPage.actions.copyNode(CODE_NODE_NAME); WorkflowPage.actions.copyNode(CODE_NODE_NAME);
WorkflowPage.getters.successToast().should('contain', 'Copied!'); successToast().should('contain', 'Copied!');
}); });
it('should select/deselect all nodes', () => { it('should select/deselect all nodes', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.selectAll(); WorkflowPage.actions.hitSelectAll();
WorkflowPage.getters.selectedNodes().should('have.length', 2); WorkflowPage.getters.selectedNodes().should('have.length', 2);
WorkflowPage.actions.deselectAll(); WorkflowPage.actions.deselectAll();
WorkflowPage.getters.selectedNodes().should('have.length', 0); 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 { import {
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME,
@ -7,8 +9,6 @@ import {
SWITCH_NODE_NAME, SWITCH_NODE_NAME,
MERGE_NODE_NAME, MERGE_NODE_NAME,
} from './../constants'; } from './../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV, WorkflowExecutionsTab } from '../pages';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
const ExecutionsTab = new WorkflowExecutionsTab(); const ExecutionsTab = new WorkflowExecutionsTab();
@ -69,7 +69,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true); 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(); WorkflowPage.actions.zoomToFit();
@ -164,8 +166,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500); cy.wait(500);
WorkflowPage.actions.selectAll(); WorkflowPage.actions.hitDeleteAllNodes();
cy.get('body').type('{backspace}');
WorkflowPage.getters.canvasNodes().should('have.length', 0); WorkflowPage.getters.canvasNodes().should('have.length', 0);
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); 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(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500); cy.wait(500);
WorkflowPage.actions.selectAll(); WorkflowPage.actions.hitDeleteAllNodes();
cy.get('body').type('{backspace}');
WorkflowPage.getters.canvasNodes().should('have.length', 0); WorkflowPage.getters.canvasNodes().should('have.length', 0);
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); 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.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.last() .last()
.should('have.css', 'left', '740px') .then(($node) => {
.should('have.css', 'top', '320px'); 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', () => { it('should zoom in', () => {
@ -258,7 +268,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.pinchToZoom(1, 'zoomOut'); WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
// Zoom in 1x + Zoom out 1x should reset to default (=1) // Zoom in 1x + Zoom out 1x should reset to default (=1)
WorkflowPage.getters.nodeView().should('have.css', 'transform', `matrix(1, 0, 0, 1, 0, 0)`); WorkflowPage.getters.nodeView().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)');
WorkflowPage.actions.pinchToZoom(1, 'zoomOut'); WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
WorkflowPage.getters WorkflowPage.getters
@ -315,7 +325,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.get('body').type('{esc}'); cy.get('body').type('{esc}');
// Keyboard shortcut // Keyboard shortcut
WorkflowPage.actions.selectAll(); WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 2); WorkflowPage.getters.disabledNodes().should('have.length', 2);
WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.actions.hitDisableNodeShortcut();
@ -324,12 +334,12 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 1); WorkflowPage.getters.disabledNodes().should('have.length', 1);
WorkflowPage.actions.selectAll(); WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 2); WorkflowPage.getters.disabledNodes().should('have.length', 2);
// Context menu // Context menu
WorkflowPage.actions.selectAll(); WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.openContextMenu(); WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation'); WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 0); WorkflowPage.getters.disabledNodes().should('have.length', 0);
@ -341,7 +351,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.openContextMenu(); WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation'); WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 1); WorkflowPage.getters.disabledNodes().should('have.length', 1);
WorkflowPage.actions.selectAll(); WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.openContextMenu(); WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation'); WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 2); 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'); 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)', () => { it('should duplicate nodes (context menu or shortcut)', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); 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.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.selectAll(); WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDuplicateNodeShortcut(); WorkflowPage.actions.hitDuplicateNode();
WorkflowPage.getters.canvasNodes().should('have.length', 5); WorkflowPage.getters.canvasNodes().should('have.length', 5);
}); });

View file

@ -6,13 +6,19 @@ import {
BACKEND_BASE_URL, BACKEND_BASE_URL,
} from '../constants'; } from '../constants';
import { WorkflowPage, NDV } from '../pages'; import { WorkflowPage, NDV } from '../pages';
import { errorToast } from '../pages/notifications';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('Data pinning', () => { describe('Data pinning', () => {
const maxPinnedDataSize = 16384;
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.window().then((win) => {
win.maxPinnedDataSize = maxPinnedDataSize;
});
}); });
it('Should be able to pin node output', () => { it('Should be able to pin node output', () => {
@ -108,6 +114,8 @@ describe('Data pinning', () => {
.parent() .parent()
.should('have.class', 'is-disabled'); .should('have.class', 'is-disabled');
cy.get('body').type('{esc}');
// Unpin using context menu // Unpin using context menu
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME); workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
ndv.actions.setPinnedData([{ test: 1 }]); ndv.actions.setPinnedData([{ test: 1 }]);
@ -136,12 +144,21 @@ describe('Data pinning', () => {
ndv.actions.pastePinnedData([ 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', () => { 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(); ndv.actions.close();
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true); workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
setExpressionOnStringValueInSet(`{{ $('${HTTP_REQUEST_NODE_NAME}').item`); setExpressionOnStringValueInSet(`{{ $('${HTTP_REQUEST_NODE_NAME}').item`);
const output = '[Object: {"json": {"http": 123}, "pairedItem": {"item": 0}}]'; const output = '[Object: {"json": {"http": 123}, "pairedItem": {"item": 0}}]';

View file

@ -1,5 +1,5 @@
import { v4 as uuid } from 'uuid';
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages'; import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
import { successToast } from '../pages/notifications';
const workflowPage = new WorkflowPageClass(); const workflowPage = new WorkflowPageClass();
const ndv = new NDV(); 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', () => { 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 // Execute the workflow
workflowPage.getters.zoomToFitButton().click(); workflowPage.getters.zoomToFitButton().click();
workflowPage.getters.executeWorkflowButton().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) // 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'); workflowPage.actions.openNode('Discourse1');
ndv.getters.inputPanel().should('be.visible'); ndv.getters.inputPanel().should('be.visible');

View file

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

View file

@ -1,10 +1,10 @@
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
import { import {
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME,
SCHEDULE_TRIGGER_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME,
} from './../constants'; } from './../constants';
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -18,6 +18,8 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow-actions_paste-data.json').then((data) => { cy.fixture('Test_workflow-actions_paste-data.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));
}); });
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
ndv.actions.executePrevious(); ndv.actions.executePrevious();
ndv.actions.switchInputMode('Table'); ndv.actions.switchInputMode('Table');
@ -49,6 +51,7 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => { cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));
}); });
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
ndv.actions.switchInputMode('Table'); ndv.actions.switchInputMode('Table');
@ -73,6 +76,7 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value'); ndv.actions.mapToParameter('value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}'); 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.parameterExpressionPreview('value').should('include.text', '0');
ndv.getters.inputTbodyCell(1, 0).realHover(); ndv.getters.inputTbodyCell(1, 0).realHover();
@ -110,6 +114,7 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => { cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));
}); });
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
ndv.actions.switchInputMode('JSON'); ndv.actions.switchInputMode('JSON');
@ -148,6 +153,7 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => { cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));
}); });
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
ndv.actions.clearParameterInput('value'); ndv.actions.clearParameterInput('value');
@ -169,20 +175,26 @@ describe('Data mapping', () => {
}); });
it('maps expressions from previous nodes', () => { it('maps expressions from previous nodes', () => {
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`); cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow');
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set1'); workflowPage.actions.openNode('Set1');
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME); ndv.actions.executePrevious();
ndv.actions.expandSchemaViewNode(SCHEDULE_TRIGGER_NODE_NAME);
ndv.getters.inputDataContainer().find('span').contains('count').realMouseDown();
const dataPill = ndv.getters
.inputDataContainer()
.findChildByTestId('run-data-schema-item')
.contains('count')
.should('be.visible');
dataPill.realMouseDown();
ndv.actions.mapToParameter('value'); ndv.actions.mapToParameter('value');
ndv.getters ndv.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`); .should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`);
ndv.actions.switchInputMode('Table'); ndv.actions.switchInputMode('Table');
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
ndv.actions.mapDataFromHeader(1, 'value'); ndv.actions.mapDataFromHeader(1, 'value');
ndv.getters ndv.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
@ -193,7 +205,6 @@ describe('Data mapping', () => {
ndv.actions.selectInputNode('Set'); ndv.actions.selectInputNode('Set');
ndv.actions.executePrevious();
ndv.getters.executingLoader().should('not.exist'); ndv.getters.executingLoader().should('not.exist');
ndv.getters.inputDataContainer().should('exist'); ndv.getters.inputDataContainer().should('exist');
ndv.actions.validateExpressionPreview('value', '0 [object Object]'); ndv.actions.validateExpressionPreview('value', '0 [object Object]');
@ -249,6 +260,7 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => { cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));
}); });
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
@ -280,6 +292,7 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => { cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));
}); });
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
ndv.actions.typeIntoParameterInput('value', 'test_value'); ndv.actions.typeIntoParameterInput('value', 'test_value');
@ -290,14 +303,8 @@ describe('Data mapping', () => {
ndv.actions.executePrevious(); ndv.actions.executePrevious();
ndv.getters.executingLoader().should('not.exist'); ndv.getters.executingLoader().should('not.exist');
ndv.getters.inputDataContainer().should('exist'); ndv.getters.inputDataContainer().should('exist');
ndv.getters ndv.actions.switchInputMode('Table');
.inputDataContainer() ndv.actions.mapDataFromHeader(1, 'value');
.should('exist')
.find('span')
.contains('test_name')
.realMouseDown();
ndv.actions.mapToParameter('value');
ndv.actions.validateExpressionPreview('value', 'test_value'); ndv.actions.validateExpressionPreview('value', 'test_value');
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME); ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
ndv.actions.validateExpressionPreview('value', 'test_value'); ndv.actions.validateExpressionPreview('value', 'test_value');
@ -307,21 +314,15 @@ describe('Data mapping', () => {
cy.fixture('Test_workflow_3.json').then((data) => { cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));
}); });
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); 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="checkbox"]').should('exist');
ndv.getters.parameterInput('includeOtherFields').find('input[type="text"]').should('not.exist'); ndv.getters.parameterInput('includeOtherFields').find('input[type="text"]').should('not.exist');
ndv.getters const pill = ndv.getters.inputDataContainer().find('span').contains('count');
.inputDataContainer() pill.should('be.visible');
.should('exist') pill.realMouseDown();
.find('span') pill.realMouseMove(100, 100);
.contains('count')
.realMouseDown()
.realMouseMove(100, 100);
cy.wait(50);
ndv.getters ndv.getters
.parameterInput('includeOtherFields') .parameterInput('includeOtherFields')
@ -332,13 +333,13 @@ describe('Data mapping', () => {
.find('input[type="text"]') .find('input[type="text"]')
.should('exist') .should('exist')
.invoke('css', 'border') .invoke('css', 'border')
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)')); .should('include', 'dashed rgb(90, 76, 194)');
ndv.getters ndv.getters
.parameterInput('value') .parameterInput('value')
.find('input[type="text"]') .find('input[type="text"]')
.should('exist') .should('exist')
.invoke('css', 'border') .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 { WorkflowPage, NDV } from '../pages';
import { BACKEND_BASE_URL } from '../constants';
import { getVisibleSelect } from '../utils';
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('Schedule Trigger node', async () => { describe('Schedule Trigger node', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
}); });
@ -18,49 +15,4 @@ describe('Schedule Trigger node', async () => {
ndv.getters.outputPanel().contains('timestamp'); ndv.getters.outputPanel().contains('timestamp');
ndv.getters.backToCanvas().click(); 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 { WorkflowPage, NDV, CredentialsModal } from '../pages';
import { v4 as uuid } from 'uuid';
import { cowBase64 } from '../support/binaryTestFiles'; import { cowBase64 } from '../support/binaryTestFiles';
import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants'; import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
@ -75,34 +75,34 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
} }
}; };
describe('Webhook Trigger node', async () => { describe('Webhook Trigger node', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
}); });
it('should listen for a GET request', () => { 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', () => { 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', () => { 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', () => { 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', () => { 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', () => { 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', () => { it('should listen for a GET request and respond with Respond to Webhook node', () => {
const webhookPath = uuid(); const webhookPath = nanoid();
simpleWebhookCall({ simpleWebhookCall({
method: 'GET', method: 'GET',
webhookPath, webhookPath,
@ -121,14 +121,16 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook); cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { cy.request<{ MyValue: number }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
(response) => {
expect(response.status).to.eq(200); expect(response.status).to.eq(200);
expect(response.body.MyValue).to.eq(1234); expect(response.body.MyValue).to.eq(1234);
}); },
);
}); });
it('should listen for a GET request and respond custom status code 201', () => { it('should listen for a GET request and respond custom status code 201', () => {
const webhookPath = uuid(); const webhookPath = nanoid();
simpleWebhookCall({ simpleWebhookCall({
method: 'GET', method: 'GET',
webhookPath, webhookPath,
@ -145,7 +147,7 @@ describe('Webhook Trigger node', async () => {
}); });
it('should listen for a GET request and respond with last node', () => { it('should listen for a GET request and respond with last node', () => {
const webhookPath = uuid(); const webhookPath = nanoid();
simpleWebhookCall({ simpleWebhookCall({
method: 'GET', method: 'GET',
webhookPath, webhookPath,
@ -161,14 +163,16 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook); cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { cy.request<{ MyValue: number }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
(response) => {
expect(response.status).to.eq(200); expect(response.status).to.eq(200);
expect(response.body.MyValue).to.eq(1234); expect(response.body.MyValue).to.eq(1234);
}); },
);
}); });
it('should listen for a GET request and respond with last node binary data', () => { it('should listen for a GET request and respond with last node binary data', () => {
const webhookPath = uuid(); const webhookPath = nanoid();
simpleWebhookCall({ simpleWebhookCall({
method: 'GET', method: 'GET',
webhookPath, webhookPath,
@ -200,14 +204,16 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook); cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { cy.request<{ data: unknown }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
(response) => {
expect(response.status).to.eq(200); expect(response.status).to.eq(200);
expect(Object.keys(response.body).includes('data')).to.be.true; expect(Object.keys(response.body).includes('data')).to.be.true;
}); },
);
}); });
it('should listen for a GET request and respond with an empty body', () => { it('should listen for a GET request and respond with an empty body', () => {
const webhookPath = uuid(); const webhookPath = nanoid();
simpleWebhookCall({ simpleWebhookCall({
method: 'GET', method: 'GET',
webhookPath, webhookPath,
@ -217,14 +223,16 @@ describe('Webhook Trigger node', async () => {
}); });
ndv.actions.execute(); ndv.actions.execute();
cy.wait(waitForWebhook); cy.wait(waitForWebhook);
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { cy.request<{ MyValue: unknown }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
(response) => {
expect(response.status).to.eq(200); expect(response.status).to.eq(200);
expect(response.body.MyValue).to.be.undefined; expect(response.body.MyValue).to.be.undefined;
}); },
);
}); });
it('should listen for a GET request with Basic Authentication', () => { it('should listen for a GET request with Basic Authentication', () => {
const webhookPath = uuid(); const webhookPath = nanoid();
simpleWebhookCall({ simpleWebhookCall({
method: 'GET', method: 'GET',
webhookPath, webhookPath,
@ -267,7 +275,7 @@ describe('Webhook Trigger node', async () => {
}); });
it('should listen for a GET request with Header Authentication', () => { it('should listen for a GET request with Header Authentication', () => {
const webhookPath = uuid(); const webhookPath = nanoid();
simpleWebhookCall({ simpleWebhookCall({
method: 'GET', method: 'GET',
webhookPath, 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 { import {
CredentialsModal, CredentialsModal,
CredentialsPage, CredentialsPage,
@ -8,6 +8,7 @@ import {
WorkflowsPage, WorkflowsPage,
} from '../pages'; } from '../pages';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import * as projects from '../composables/projects';
/** /**
* User U1 - Instance owner * User U1 - Instance owner
@ -30,11 +31,11 @@ const workflowSharingModal = new WorkflowSharingModal();
const ndv = new NDV(); const ndv = new NDV();
describe('Sharing', { disableAutoLogin: true }, () => { describe('Sharing', { disableAutoLogin: true }, () => {
before(() => cy.enableFeature('sharing', true)); before(() => cy.enableFeature('sharing'));
let workflowW2Url = ''; let workflowW2Url = '';
it('should create C1, W1, W2, share W1 with U3, as U2', () => { it('should create C1, W1, W2, share W1 with U3, as U2', () => {
cy.signin(INSTANCE_MEMBERS[0]); cy.signinAsMember(0);
cy.visit(credentialsPage.url); cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
@ -67,7 +68,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
}); });
it('should create C2, share C2 with U1 and U2, as U3', () => { it('should create C2, share C2 with U1 and U2, as U3', () => {
cy.signin(INSTANCE_MEMBERS[1]); cy.signinAsMember(1);
cy.visit(credentialsPage.url); cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
@ -83,7 +84,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
}); });
it('should open W1, add node using C2 as U3', () => { it('should open W1, add node using C2 as U3', () => {
cy.signin(INSTANCE_MEMBERS[1]); cy.signinAsMember(1);
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 1); 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', () => { it('should open W1, add node using C2 as U2', () => {
cy.signin(INSTANCE_MEMBERS[0]); cy.signinAsMember(0);
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2); workflowsPage.getters.workflowCards().should('have.length', 2);
@ -119,7 +120,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
}); });
it('should not have access to W2, as U3', () => { it('should not have access to W2, as U3', () => {
cy.signin(INSTANCE_MEMBERS[1]); cy.signinAsMember(1);
cy.visit(workflowW2Url); cy.visit(workflowW2Url);
cy.waitForLoad(); cy.waitForLoad();
@ -128,13 +129,17 @@ describe('Sharing', { disableAutoLogin: true }, () => {
}); });
it('should have access to W1, W2, as U1', () => { it('should have access to W1, W2, as U1', () => {
cy.signin(INSTANCE_OWNER); cy.signinAsOwner();
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2); workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCard('Workflow W1').click(); workflowsPage.getters.workflowCard('Workflow W1').click();
workflowPage.actions.openNode('Notion'); 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(); ndv.actions.close();
cy.waitForLoad(); cy.waitForLoad();
@ -144,7 +149,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
}); });
it('should automatically test C2 when opened by U2 sharee', () => { it('should automatically test C2 when opened by U2 sharee', () => {
cy.signin(INSTANCE_MEMBERS[0]); cy.signinAsMember(0);
cy.visit(credentialsPage.url); cy.visit(credentialsPage.url);
credentialsPage.getters.credentialCard('Credential C2').click(); 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)', () => { 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); cy.visit(credentialsPage.url);
credentialsPage.getters.createCredentialButton().click(); credentialsPage.getters.createCredentialButton().click();
@ -164,18 +169,18 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsModal.actions.close(); credentialsModal.actions.close();
cy.signout(); cy.signout();
cy.signin(INSTANCE_ADMIN); cy.signinAsAdmin();
cy.visit(credentialsPage.url); cy.visit(credentialsPage.url);
credentialsPage.getters.credentialCard('Credential C3').click(); credentialsPage.getters.credentialCard('Credential C3').click();
credentialsModal.getters.testSuccessTag().should('be.visible'); credentialsModal.getters.testSuccessTag().should('be.visible');
cy.get('input').should('not.have.length'); cy.get('input').should('not.have.length');
credentialsModal.actions.changeTab('Sharing'); credentialsModal.actions.changeTab('Sharing');
cy.contains( 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'); ).should('be.visible');
credentialsModal.getters.usersSelect().click(); credentialsModal.getters.usersSelect().click();
cy.getByTestId('user-email') cy.getByTestId('project-sharing-info')
.filter(':visible') .filter(':visible')
.should('have.length', 3) .should('have.length', 3)
.contains(INSTANCE_ADMIN.email) .contains(INSTANCE_ADMIN.email)
@ -188,3 +193,146 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsModal.actions.close(); 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 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', () => { it('should detach a tag inline by clicking on X on tag pill', () => {
wf.getters.createTagButton().click(); wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS); wf.actions.addTags(TEST_TAGS);
wf.getters.nthTagPill(1).click(); wf.getters.nthTagPill(1).click();
wf.getters.tagsDropdown().find('.el-tag__close').first().click(); wf.getters.tagsDropdown().find('.el-tag__close').first().click();
cy.get('body').click(0, 0); cy.get('body').click(0, 0);
wf.getters.workflowTags().click();
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1); wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
}); });
@ -88,6 +67,7 @@ describe('Workflow tags', () => {
wf.getters.nthTagPill(1).click(); wf.getters.nthTagPill(1).click();
wf.getters.tagsInDropdown().filter('.selected').first().click(); wf.getters.tagsInDropdown().filter('.selected').first().click();
cy.get('body').click(0, 0); cy.get('body').click(0, 0);
wf.getters.workflowTags().click();
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1); 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 { 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 { PersonalSettingsPage } from '../pages/settings-personal';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import { errorToast, successToast } from '../pages/notifications';
/** /**
* User A - Instance owner * User A - Instance owner
@ -24,7 +25,6 @@ const updatedPersonalData = {
}; };
const usersSettingsPage = new SettingsUsersPage(); const usersSettingsPage = new SettingsUsersPage();
const workflowPage = new WorkflowPage();
const personalSettingsPage = new PersonalSettingsPage(); const personalSettingsPage = new PersonalSettingsPage();
const settingsSidebar = new SettingsSidebar(); const settingsSidebar = new SettingsSidebar();
const mainSidebar = new MainSidebar(); const mainSidebar = new MainSidebar();
@ -34,6 +34,27 @@ describe('User Management', { disableAutoLogin: true }, () => {
cy.enableFeature('sharing'); 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', () => { it('should prevent non-owners to access UM settings', () => {
usersSettingsPage.actions.loginAndVisit( usersSettingsPage.actions.loginAndVisit(
INSTANCE_MEMBERS[0].email, INSTANCE_MEMBERS[0].email,
@ -153,7 +174,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
usersSettingsPage.getters.deleteDataRadioButton().click(); usersSettingsPage.getters.deleteDataRadioButton().click();
usersSettingsPage.getters.deleteDataInput().type('delete all data'); usersSettingsPage.getters.deleteDataInput().type('delete all data');
usersSettingsPage.getters.deleteUserButton().click(); usersSettingsPage.getters.deleteUserButton().click();
workflowPage.getters.successToast().should('contain', 'User deleted'); successToast().should('contain', 'User deleted');
}); });
it('should delete user and transfer their data', () => { it('should delete user and transfer their data', () => {
@ -163,10 +184,10 @@ describe('User Management', { disableAutoLogin: true }, () => {
usersSettingsPage.getters.userSelectDropDown().click(); usersSettingsPage.getters.userSelectDropDown().click();
usersSettingsPage.getters.userSelectOptions().first().click(); usersSettingsPage.getters.userSelectOptions().first().click();
usersSettingsPage.getters.deleteUserButton().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.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.actions.updateFirstAndLastName( personalSettingsPage.actions.updateFirstAndLastName(
updatedPersonalData.newFirstName, updatedPersonalData.newFirstName,
@ -175,42 +196,39 @@ describe('User Management', { disableAutoLogin: true }, () => {
personalSettingsPage.getters personalSettingsPage.getters
.currentUserName() .currentUserName()
.should('contain', `${updatedPersonalData.newFirstName} ${updatedPersonalData.newLastName}`); .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.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.getters.changePasswordLink().click(); personalSettingsPage.getters.changePasswordLink().click();
for (let weakPass of updatedPersonalData.invalidPasswords) { for (const weakPass of updatedPersonalData.invalidPasswords) {
personalSettingsPage.actions.tryToSetWeakPassword(INSTANCE_OWNER.password, weakPass); personalSettingsPage.actions.tryToSetWeakPassword(INSTANCE_OWNER.password, weakPass);
} }
}); });
it(`shouldn't allow user to change password if old password is wrong`, () => { it("shouldn't allow user to change password if old password is wrong", () => {
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password); personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.getters.changePasswordLink().click(); personalSettingsPage.getters.changePasswordLink().click();
personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword); personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword);
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.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.getters.changePasswordLink().click(); personalSettingsPage.getters.changePasswordLink().click();
personalSettingsPage.actions.updatePassword( personalSettingsPage.actions.updatePassword(
INSTANCE_OWNER.password, INSTANCE_OWNER.password,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,
); );
workflowPage.getters.successToast().should('contain', 'Password updated'); successToast().should('contain', 'Password updated');
personalSettingsPage.actions.loginWithNewData( personalSettingsPage.actions.loginWithNewData(
INSTANCE_OWNER.email, INSTANCE_OWNER.email,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,
); );
}); });
it(`shouldn't allow users to set invalid email`, () => { it("shouldn't allow users to set invalid email", () => {
personalSettingsPage.actions.loginAndVisit( personalSettingsPage.actions.loginAndVisit(
INSTANCE_OWNER.email, INSTANCE_OWNER.email,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,
@ -221,13 +239,13 @@ describe('User Management', { disableAutoLogin: true }, () => {
personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('.')[0]); personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('.')[0]);
}); });
it(`should change user email`, () => { it('should change user email', () => {
personalSettingsPage.actions.loginAndVisit( personalSettingsPage.actions.loginAndVisit(
INSTANCE_OWNER.email, INSTANCE_OWNER.email,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,
); );
personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail); personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail);
workflowPage.getters.successToast().should('contain', 'Personal details updated'); successToast().should('contain', 'Personal details updated');
personalSettingsPage.actions.loginWithNewData( personalSettingsPage.actions.loginWithNewData(
updatedPersonalData.newEmail, updatedPersonalData.newEmail,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,

View file

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

View file

@ -1,4 +1,7 @@
import { type ICredentialType } from 'n8n-workflow';
import { import {
AGENT_NODE_NAME,
AI_TOOL_HTTP_NODE_NAME,
GMAIL_NODE_NAME, GMAIL_NODE_NAME,
HTTP_REQUEST_NODE_NAME, HTTP_REQUEST_NODE_NAME,
NEW_GOOGLE_ACCOUNT_NAME, NEW_GOOGLE_ACCOUNT_NAME,
@ -11,6 +14,7 @@ import {
TRELLO_NODE_NAME, TRELLO_NODE_NAME,
} from '../constants'; } from '../constants';
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages'; import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
import { successToast } from '../pages/notifications';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
const credentialsPage = new CredentialsPage(); const credentialsPage = new CredentialsPage();
@ -19,6 +23,7 @@ const workflowPage = new WorkflowPage();
const nodeDetailsView = new NDV(); const nodeDetailsView = new NDV();
const NEW_CREDENTIAL_NAME = 'Something else'; const NEW_CREDENTIAL_NAME = 'Something else';
const NEW_CREDENTIAL_NAME2 = 'Something else entirely';
describe('Credentials', () => { describe('Credentials', () => {
beforeEach(() => { beforeEach(() => {
@ -42,39 +47,6 @@ describe('Credentials', () => {
credentialsPage.getters.credentialCards().should('have.length', 1); 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', () => { it('should sort credentials', () => {
credentialsPage.actions.search(''); credentialsPage.actions.search('');
credentialsPage.actions.sortBy('nameDesc'); credentialsPage.actions.sortBy('nameDesc');
@ -185,7 +157,7 @@ describe('Credentials', () => {
credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.deleteButton().click(); credentialsModal.getters.deleteButton().click();
cy.get('.el-message-box').find('button').contains('Yes').click(); cy.get('.el-message-box').find('button').contains('Yes').click();
workflowPage.getters.successToast().contains('Credential deleted'); successToast().contains('Credential deleted');
workflowPage.getters workflowPage.getters
.nodeCredentialsSelect() .nodeCredentialsSelect()
.find('input') .find('input')
@ -211,6 +183,49 @@ describe('Credentials', () => {
.nodeCredentialsSelect() .nodeCredentialsSelect()
.find('input') .find('input')
.should('have.value', NEW_CREDENTIAL_NAME); .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', () => { it('should setup generic authentication for HTTP node', () => {
@ -242,7 +257,7 @@ describe('Credentials', () => {
req.headers['cache-control'] = 'no-cache, no-store'; req.headers['cache-control'] = 'no-cache, no-store';
req.on('response', (res) => { req.on('response', (res) => {
const credentials = res.body || []; const credentials: ICredentialType[] = res.body || [];
const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api'); const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api');
@ -254,8 +269,9 @@ describe('Credentials', () => {
}); });
workflowPage.actions.visit(true); workflowPage.actions.visit(true);
workflowPage.actions.addNodeToCanvas('Slack'); workflowPage.actions.addNodeToCanvas('Manual');
workflowPage.actions.openNode('Slack'); workflowPage.actions.addNodeToCanvas('Slack', true, true, 'Get a channel');
workflowPage.getters.nodeCredentialsSelect().should('exist');
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialAuthTypeRadioButtons().first().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 { WorkflowPage } from '../pages';
import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab'; import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
import type { RouteHandler } from 'cypress/types/net-stubbing'; import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const executionsTab = new WorkflowExecutionsTab(); const executionsTab = new WorkflowExecutionsTab();
@ -10,7 +12,7 @@ const executionsRefreshInterval = 4000;
describe('Current Workflow Executions', () => { describe('Current Workflow Executions', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`); cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
}); });
it('should render executions tab correctly', () => { it('should render executions tab correctly', () => {
@ -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', () => { it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
const throttleResponse: RouteHandler = (req) => { const throttleResponse: RouteHandler = async (req) => {
return new Promise((resolve) => { return await new Promise((resolve) => {
setTimeout(() => resolve(req.continue()), 2000); setTimeout(() => resolve(req.continue()), 2000);
}); });
}; };
@ -71,6 +73,160 @@ describe('Current Workflow Executions', () => {
cy.wait(executionsRefreshInterval); cy.wait(executionsRefreshInterval);
cy.url().should('not.include', '/executions'); 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 = () => { const createMockExecutions = () => {
@ -82,3 +238,10 @@ const createMockExecutions = () => {
executionsTab.actions.toggleNodeEnabled('Error'); executionsTab.actions.toggleNodeEnabled('Error');
executionsTab.actions.createManualExecutions(4); 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 { NodeCreator } from '../pages/features/node-creator';
import CustomNodeFixture from '../fixtures/Custom_node.json'; import CustomNodeFixture from '../fixtures/Custom_node.json';
import { CredentialsModal, WorkflowPage } from '../pages'; import { CredentialsModal, WorkflowPage } from '../pages';
@ -5,6 +6,13 @@ import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_cred
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json'; import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
import CustomCredential from '../fixtures/Custom_credential.json'; import CustomCredential from '../fixtures/Custom_credential.json';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import {
confirmCommunityNodeUninstall,
confirmCommunityNodeUpdate,
getCommunityCards,
installFirstCommunityNode,
visitCommunityNodesSettings,
} from '../pages/settings-community-nodes';
const credentialsModal = new CredentialsModal(); const credentialsModal = new CredentialsModal();
const nodeCreatorFeature = new NodeCreator(); 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 // We separate-out the custom nodes because they require injecting nodes and credentials
// so the /nodes and /credentials endpoints are intercepted and non-cached. // 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. // 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(() => { beforeEach(() => {
cy.intercept('/types/nodes.json', { middleware: true }, (req) => { cy.intercept('/types/nodes.json', { middleware: true }, (req) => {
req.headers['cache-control'] = 'no-cache, no-store'; req.headers['cache-control'] = 'no-cache, no-store';
@ -33,9 +41,9 @@ describe('Community Nodes', () => {
req.headers['cache-control'] = 'no-cache, no-store'; req.headers['cache-control'] = 'no-cache, no-store';
req.on('response', (res) => { req.on('response', (res) => {
const credentials = res.body || []; const credentials: ICredentialType[] = res.body || [];
credentials.push(CustomCredential); credentials.push(CustomCredential as ICredentialType);
}); });
}); });
@ -94,3 +102,89 @@ describe('Community Nodes', () => {
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential'); 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', () => { describe('Variables', () => {
it('should show the unlicensed action box when the feature is disabled', () => { it('should show the unlicensed action box when the feature is disabled', () => {
cy.disableFeature('variables', false); cy.disableFeature('variables');
cy.visit(variablesPage.url); cy.visit(variablesPage.url);
variablesPage.getters.unavailableResourcesList().should('be.visible'); variablesPage.getters.unavailableResourcesList().should('be.visible');
@ -18,14 +18,15 @@ describe('Variables', () => {
beforeEach(() => { beforeEach(() => {
cy.intercept('GET', '/rest/variables').as('loadVariables'); cy.intercept('GET', '/rest/variables').as('loadVariables');
cy.intercept('GET', '/rest/login').as('login');
cy.visit(variablesPage.url); 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', () => { it('should show the licensed action box when the feature is enabled', () => {
variablesPage.getters.emptyResourcesList().should('be.visible'); 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', () => { it('should create a new variable using empty state row', () => {

View file

@ -1,5 +1,4 @@
import { WorkflowPage, NDV } from '../pages'; import { WorkflowPage, NDV } from '../pages';
import { v4 as uuid } from 'uuid';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -7,7 +6,7 @@ const ndv = new NDV();
describe('NDV', () => { describe('NDV', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.renameWithUniqueName();
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
}); });
@ -113,6 +112,9 @@ describe('NDV', () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Set3'); workflowPage.actions.openNode('Set3');
ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table');
ndv.getters ndv.getters
.inputRunSelector() .inputRunSelector()
.should('exist') .should('exist')
@ -124,9 +126,6 @@ describe('NDV', () => {
.find('input') .find('input')
.should('include.value', '2 of 2 (6 items)'); .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.actions.changeOutputRunSelector('1 of 2 (6 items)');
ndv.getters.inputRunSelector().find('input').should('include.value', '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)'); 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'); 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', () => { it('can pair items between input and output across branches and runs', () => {
cy.fixture('Test_workflow_5.json').then((data) => { cy.fixture('Test_workflow_5.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));

View file

@ -1,7 +1,5 @@
import { META_KEY } from '../constants'; import { META_KEY } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { getPopper } from '../utils';
import { Interception } from 'cypress/types/net-stubbing';
const workflowPage = new WorkflowPageClass(); const workflowPage = new WorkflowPageClass();
@ -26,6 +24,9 @@ function checkStickiesStyle(
describe('Canvas Actions', () => { describe('Canvas Actions', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); 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', () => { it('adds sticky to canvas with default text and position', () => {
@ -34,15 +35,12 @@ describe('Canvas Actions', () => {
addDefaultSticky(); addDefaultSticky();
workflowPage.actions.deselectAll(); workflowPage.actions.deselectAll();
workflowPage.actions.addStickyFromContextMenu(); workflowPage.actions.addStickyFromContextMenu();
workflowPage.actions.hitAddStickyShortcut(); workflowPage.actions.hitAddSticky();
workflowPage.getters.stickies().should('have.length', 3); workflowPage.getters.stickies().should('have.length', 3);
// Should not add a sticky for ctrl+shift+s // Should not add a sticky for ctrl+shift+s
cy.get('body') cy.get('body').type(`{${META_KEY}+shift+s}`);
.type(META_KEY, { delay: 500, release: false })
.type('{shift}', { release: false })
.type('s');
workflowPage.getters.stickies().should('have.length', 3); workflowPage.getters.stickies().should('have.length', 3);
workflowPage.getters workflowPage.getters
@ -82,32 +80,6 @@ describe('Canvas Actions', () => {
workflowPage.getters.stickies().should('have.length', 0); 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', () => { it('edits sticky and updates content as markdown', () => {
workflowPage.actions.addSticky(); 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) { function moveSticky(target: Position) {
cy.drag('[data-test-id="sticky"]', [target.left, target.top], { abs: true }); cy.drag('[data-test-id="sticky"]', [target.left, target.top], { abs: true });
stickyShouldBePositionedCorrectly(target); stickyShouldBePositionedCorrectly(target);

View file

@ -1,5 +1,5 @@
import { WorkflowPage, NDV, CredentialsModal } from '../pages'; import { WorkflowPage, NDV, CredentialsModal } from '../pages';
import { getVisiblePopper, getVisibleSelect } from '../utils'; import { getVisiblePopper } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -37,21 +37,49 @@ describe('Resource Locator', () => {
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE); 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', () => { it('should show appropriate error when credentials are not valid', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet'); workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
workflowPage.getters.nodeCredentialsSelect().click();
// Add oAuth credentials // Add oAuth credentials
getVisibleSelect().find('li').last().click(); workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsCreateOption().click();
credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
credentialsModal.actions.fillCredentialsForm(); credentialsModal.actions.fillCredentialsForm();
cy.get('.el-message-box').find('button').contains('Close').click(); cy.get('.el-message-box').find('button').contains('Close').click();
ndv.getters.resourceLocatorInput('documentId').click(); ndv.getters.resourceLocatorInput('documentId').click();
ndv.getters.resourceLocatorErrorMessage().should('contain', INVALID_CREDENTIALS_MESSAGE); 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', () => { it('should reset resource locator when dependent field is changed', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet'); workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
@ -75,7 +103,7 @@ describe('Resource Locator', () => {
ndv.actions.setInvalidExpression({ fieldName: 'fieldId' }); 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(); ndv.getters.resourceLocatorInput('rlc').click();

View file

@ -6,86 +6,51 @@ import {
getPublicApiUpgradeCTA, getPublicApiUpgradeCTA,
} from '../pages'; } from '../pages';
import planData from '../fixtures/Plan_data_opt_in_trial.json'; import planData from '../fixtures/Plan_data_opt_in_trial.json';
import { INSTANCE_OWNER } from '../constants';
const mainSidebar = new MainSidebar(); const mainSidebar = new MainSidebar();
const bannerStack = new BannerStack(); const bannerStack = new BannerStack();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
describe('Cloud', { disableAutoLogin: true }, () => { describe('Cloud', () => {
before(() => { before(() => {
const now = new Date(); const now = new Date();
const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000); const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
planData.expirationDate = fiveDaysFromNow.toJSON(); 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', () => { describe('BannerStack', () => {
it('should render trial banner for opt-in cloud user', () => { it('should render trial banner for opt-in cloud user', () => {
cy.intercept('GET', '/rest/admin/cloud-plan', { visitWorkflowPage();
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');
bannerStack.getters.banner().should('be.visible'); bannerStack.getters.banner().should('be.visible');
mainSidebar.actions.signout(); mainSidebar.actions.signout();
bannerStack.getters.banner().should('not.be.visible'); 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', () => { describe('Admin Home', () => {
it('Should show admin button', () => { it('Should show admin button', () => {
cy.intercept('GET', '/rest/settings', (req) => { visitWorkflowPage();
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);
mainSidebar.getters.adminPanel().should('be.visible'); mainSidebar.getters.adminPanel().should('be.visible');
}); });
@ -93,25 +58,8 @@ describe('Cloud', { disableAutoLogin: true }, () => {
describe('Public API', () => { describe('Public API', () => {
it('Should show upgrade CTA for Public API if user is trialing', () => { 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(); visitPublicApiPage();
cy.wait(['@loadSettings', '@projects', '@roles', '@getPlanData']);
getPublicApiUpgradeCTA().should('be.visible'); 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 { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../constants';
import { SigninPage } from '../pages'; import { SigninPage } from '../pages';
import { PersonalSettingsPage } from '../pages/settings-personal'; import { PersonalSettingsPage } from '../pages/settings-personal';
import { MfaLoginPage } from '../pages/mfa-login'; import { MfaLoginPage } from '../pages/mfa-login';
import generateOTPToken from 'cypress-otp'; import { MainSidebar } from './../pages/sidebar/main-sidebar';
const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD'; const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD';
@ -34,16 +34,15 @@ const signinPage = new SigninPage();
const personalSettingsPage = new PersonalSettingsPage(); const personalSettingsPage = new PersonalSettingsPage();
const mainSidebar = new MainSidebar(); const mainSidebar = new MainSidebar();
describe('Two-factor authentication', () => { describe('Two-factor authentication', { disableAutoLogin: true }, () => {
beforeEach(() => { beforeEach(() => {
Cypress.session.clearAllSavedSessions();
cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, { cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, {
owner: user, owner: user,
members: [], members: [],
admin, admin,
}); });
cy.on('uncaught:exception', (err, runnable) => { cy.on('uncaught:exception', (error) => {
expect(err.message).to.include('Not logged in'); expect(error.message).to.include('Not logged in');
return false; return false;
}); });
cy.intercept('GET', '/rest/mfa/qr').as('getMfaQrCode'); cy.intercept('GET', '/rest/mfa/qr').as('getMfaQrCode');

View file

@ -1,7 +1,6 @@
import { import {
HTTP_REQUEST_NODE_NAME, HTTP_REQUEST_NODE_NAME,
IF_NODE_NAME, IF_NODE_NAME,
INSTANCE_OWNER,
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME,
} from '../constants'; } from '../constants';
@ -19,9 +18,9 @@ describe('Debug', () => {
it('should be able to debug executions', () => { it('should be able to debug executions', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution'); 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(); 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,36 +1,47 @@
import { TemplatesPage } from '../pages/templates'; import { TemplatesPage } from '../pages/templates';
import { WorkflowPage } from '../pages/workflow';
import { WorkflowsPage } from '../pages/workflows'; import { WorkflowsPage } from '../pages/workflows';
import { MainSidebar } from '../pages/sidebar/main-sidebar'; 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 templatesPage = new TemplatesPage();
const workflowPage = new WorkflowPage();
const workflowsPage = new WorkflowsPage(); const workflowsPage = new WorkflowsPage();
const mainSidebar = new MainSidebar(); const mainSidebar = new MainSidebar();
describe('Workflow templates', () => { describe('Workflow templates', () => {
beforeEach(() => { const mockTemplateHost = (host: string) => {
cy.intercept('GET', '**/rest/settings', (req) => { cy.overrideSettings({
// Disable cache templates: { enabled: true, host },
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'); };
describe('For api.n8n.io', () => {
beforeEach(() => {
mockTemplateHost('https://api.n8n.io/api/');
}); });
it('Opens website when clicking templates sidebar link', () => { it('Opens website when clicking templates sidebar link', () => {
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
mainSidebar.getters.menuItem('Templates').should('be.visible'); mainSidebar.getters.templates().should('be.visible');
// Templates should be a link to the website // Templates should be a link to the website
mainSidebar.getters.templates().parent('a').should('have.attr', 'href').and('include', 'https://n8n.io/workflows'); mainSidebar.getters
.templates()
.parent('a')
.should('have.attr', 'href')
.and('include', 'https://n8n.io/workflows');
// Link should contain instance address and n8n version // Link should contain instance address and n8n version
mainSidebar.getters.templates().parent('a').then(($a) => { mainSidebar.getters
.templates()
.parent('a')
.then(($a) => {
const href = $a.attr('href'); const href = $a.attr('href');
const params = new URLSearchParams(href); const params = new URLSearchParams(href);
// Link should have all mandatory parameters expected on the website // Link should have all mandatory parameters expected on the website
expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include(window.location.origin); 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_n8n_version')).to.match(/[0-9]+\.[0-9]+\.[0-9]+/);
expect(params.get('utm_awc')).to.match(/[0-9]+/); expect(params.get('utm_awc')).to.match(/[0-9]+/);
}); });
@ -38,9 +49,198 @@ describe('Workflow templates', () => {
}); });
it('Redirects to website when visiting templates page directly', () => { 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.visit(templatesPage.url);
cy.origin('https://n8n.io', () => {
cy.url().should('include', 'https://n8n.io/workflows'); cy.wait('@templatesPage');
}) });
});
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, CODE_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME,
IF_NODE_NAME, IF_NODE_NAME,
INSTANCE_OWNER,
SCHEDULE_TRIGGER_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME,
} from '../constants'; } from '../constants';
import { import {
@ -103,7 +102,7 @@ const switchBetweenEditorAndHistory = () => {
const switchBetweenEditorAndWorkflowlist = () => { const switchBetweenEditorAndWorkflowlist = () => {
cy.getByTestId('menu-item').first().click(); 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(); cy.getByTestId('resources-list-item').first().click();
@ -125,15 +124,10 @@ describe('Editor actions should work', () => {
beforeEach(() => { beforeEach(() => {
cy.enableFeature('debugInEditor'); cy.enableFeature('debugInEditor');
cy.enableFeature('workflowHistory'); cy.enableFeature('workflowHistory');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); cy.signinAsOwner();
createNewWorkflowAndActivate(); createNewWorkflowAndActivate();
}); });
it('after saving a new workflow', () => {
editWorkflowAndDeactivate();
editWorkflowMoreAndActivate();
});
it('after switching between Editor and Executions', () => { it('after switching between Editor and Executions', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
@ -148,7 +142,7 @@ describe('Editor actions should work', () => {
it('after switching between Editor and Debug', () => { it('after switching between Editor and Debug', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution'); cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun'); cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun');
editWorkflowAndDeactivate(); editWorkflowAndDeactivate();
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
@ -186,9 +180,9 @@ describe('Editor zoom should work after route changes', () => {
beforeEach(() => { beforeEach(() => {
cy.enableFeature('debugInEditor'); cy.enableFeature('debugInEditor');
cy.enableFeature('workflowHistory'); cy.enableFeature('workflowHistory');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); cy.signinAsOwner();
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Lots_of_nodes.json', `Lots of nodes`); cy.createFixtureWorkflow('Lots_of_nodes.json', 'Lots of nodes');
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
}); });
@ -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/*/version/*').as('getVersion');
cy.intercept('GET', '/rest/workflow-history/workflow/*').as('getHistory'); cy.intercept('GET', '/rest/workflow-history/workflow/*').as('getHistory');
cy.intercept('GET', '/rest/users').as('getUsers'); 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/active-workflows').as('getActiveWorkflows');
cy.intercept('GET', '/rest/credentials').as('getCredentials'); cy.intercept('GET', '/rest/projects').as('getProjects');
switchBetweenEditorAndHistory(); switchBetweenEditorAndHistory();
zoomInAndCheckNodes(); zoomInAndCheckNodes();

View file

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

View file

@ -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 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 { WorkflowPage } from '../pages/workflow';
import { errorToast } from '../pages/notifications';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
describe('Demo', () => { describe('Demo', () => {
beforeEach(() => {
cy.overrideSettings({ previewMode: true });
cy.signout();
});
it('can import template', () => { it('can import template', () => {
vistDemoPage(); visitDemoPage();
errorToast().should('not.exist');
importWorkflow(workflow); importWorkflow(workflow);
workflowPage.getters.canvasNodes().should('have.length', 3); workflowPage.getters.canvasNodes().should('have.length', 3);
}); });
it('can override theme to dark', () => { it('can override theme to dark', () => {
vistDemoPage('dark'); visitDemoPage('dark');
cy.get('body').should('have.attr', 'data-theme', 'dark'); cy.get('body').should('have.attr', 'data-theme', 'dark');
errorToast().should('not.exist');
}); });
it('can override theme to light', () => { it('can override theme to light', () => {
vistDemoPage('light'); visitDemoPage('light');
cy.get('body').should('have.attr', 'data-theme', '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', () => { describe('Node IO Filter', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Node_IO_filter.json', `Node IO filter`); cy.createFixtureWorkflow('Node_IO_filter.json', 'Node IO filter');
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
}); });
@ -15,13 +15,13 @@ describe('Node IO Filter', () => {
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
ndv.actions.close(); ndv.actions.close();
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
cy.wait(500);
ndv.getters.outputDataContainer().should('be.visible'); ndv.getters.outputDataContainer().should('be.visible');
ndv.getters.outputPanel().findChildByTestId('ndv-search').should('exist');
cy.document().trigger('keyup', { key: '/' }); cy.document().trigger('keyup', { key: '/' });
const searchInput = ndv.getters.searchInput(); 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.pagination().find('li').should('have.length', 3);
ndv.getters.outputDataContainer().find('mark').should('not.exist'); ndv.getters.outputDataContainer().find('mark').should('not.exist');
@ -36,19 +36,18 @@ describe('Node IO Filter', () => {
it('should filter input/output data separately', () => { it('should filter input/output data separately', () => {
workflowPage.getters.canvasNodes().eq(1).dblclick(); workflowPage.getters.canvasNodes().eq(1).dblclick();
cy.wait(500);
ndv.getters.outputDataContainer().should('be.visible'); ndv.getters.outputDataContainer().should('be.visible');
ndv.getters.inputDataContainer().should('be.visible'); ndv.getters.inputDataContainer().should('be.visible');
ndv.actions.switchInputMode('Table'); ndv.actions.switchInputMode('Table');
ndv.getters.outputPanel().findChildByTestId('ndv-search').should('exist');
cy.document().trigger('keyup', { key: '/' }); 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 let focusedInput = ndv.getters
.inputPanel() .inputPanel()
.findChildByTestId('ndv-search') .findChildByTestId('ndv-search')
.filter(':focus') .should('have.focus');
.should('exist');
const getInputPagination = () => const getInputPagination = () =>
ndv.getters.inputPanel().findChildByTestId('ndv-data-pagination'); ndv.getters.inputPanel().findChildByTestId('ndv-data-pagination');
@ -82,13 +81,9 @@ describe('Node IO Filter', () => {
ndv.getters.outputDataContainer().trigger('mouseover'); ndv.getters.outputDataContainer().trigger('mouseover');
cy.document().trigger('keyup', { key: '/' }); 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 focusedInput = ndv.getters.outputPanel().findChildByTestId('ndv-search').should('have.focus');
.outputPanel()
.findChildByTestId('ndv-search')
.filter(':focus')
.should('exist');
getInputPagination().find('li').should('have.length', 3); getInputPagination().find('li').should('have.length', 3);
getInputCounter().contains('21 items').should('exist'); getInputCounter().contains('21 items').should('exist');

View file

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

View file

@ -1,6 +1,4 @@
import { WorkflowPage } from "../pages"; import { errorToast, successToast } from '../pages/notifications';
const workflowPage = new WorkflowPage();
const INVALID_NAMES = [ const INVALID_NAMES = [
'https://n8n.io', 'https://n8n.io',
@ -27,14 +25,14 @@ const VALID_NAMES = [
]; ];
describe('Personal Settings', () => { describe('Personal Settings', () => {
it ('should allow to change first and last name', () => { it('should allow to change first and last name', () => {
cy.visit('/settings/personal'); cy.visit('/settings/personal');
VALID_NAMES.forEach((name) => { VALID_NAMES.forEach((name) => {
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name[0]); cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name[0]);
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name[1]); cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name[1]);
cy.getByTestId('save-settings-button').click(); cy.getByTestId('save-settings-button').click();
workflowPage.getters.successToast().should('contain', 'Personal details updated'); successToast().should('contain', 'Personal details updated');
workflowPage.getters.successToast().find('.el-notification__closeBtn').click(); successToast().find('.el-notification__closeBtn').click();
}); });
}); });
it('not allow malicious values for personal data', () => { 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="firstName"]').clear().type(name);
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name); cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name);
cy.getByTestId('save-settings-button').click(); cy.getByTestId('save-settings-button').click();
workflowPage.getters errorToast().should('contain', 'Malicious firstName | Malicious lastName');
.errorToast() errorToast().find('.el-notification__closeBtn').click();
.should('contain', 'Malicious firstName | Malicious lastName');
workflowPage.getters.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 * as formStep from '../composables/setup-template-form-step';
import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button'; import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button';
import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal'; 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 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 // NodeView uses beforeunload listener that will show a browser
// native popup, which will block cypress from continuing / exiting. // native popup, which will block cypress from continuing / exiting.
@ -29,19 +38,19 @@ Cypress.on('window:before:load', (win) => {
describe('Template credentials setup', () => { describe('Template credentials setup', () => {
beforeEach(() => { beforeEach(() => {
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${testTemplate.id}`, { cy.intercept(
fixture: testTemplate.fixture, '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', () => { 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'); clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');
templateCredentialsSetupPage.getters templateCredentialsSetupPage.getters
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`) .title("Set up 'Promote new Shopify products on Twitter and Telegram' template")
.should('be.visible'); .should('be.visible');
}); });
@ -58,7 +67,7 @@ describe('Template credentials setup', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id); templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters templateCredentialsSetupPage.getters
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`) .title("Set up 'Promote new Shopify products on Twitter and Telegram' template")
.should('be.visible'); .should('be.visible');
templateCredentialsSetupPage.getters templateCredentialsSetupPage.getters
@ -108,7 +117,7 @@ describe('Template credentials setup', () => {
// Focus the canvas so the copy to clipboard works // Focus the canvas so the copy to clipboard works
workflowPage.getters.canvasNodes().eq(0).realClick(); workflowPage.getters.canvasNodes().eq(0).realClick();
workflowPage.actions.selectAll(); workflowPage.actions.hitSelectAll();
workflowPage.actions.hitCopy(); workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
@ -117,6 +126,7 @@ describe('Template credentials setup', () => {
const workflow = JSON.parse(workflowJSON); const workflow = JSON.parse(workflowJSON);
expect(workflow.meta).to.haveOwnProperty('templateId', testTemplate.id.toString()); expect(workflow.meta).to.haveOwnProperty('templateId', testTemplate.id.toString());
expect(workflow.meta).not.to.haveOwnProperty('templateCredsSetupCompleted');
workflow.nodes.forEach((node: any) => { workflow.nodes.forEach((node: any) => {
expect(Object.keys(node.credentials ?? {})).to.have.lengthOf(1); 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)', () => { it('should work with a template that has no credentials (ADO-1603)', () => {
const templateWithoutCreds = templateCredentialsSetupPage.testData.templateWithoutCredentials; const { id, data } = templateWithoutCredentials;
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${templateWithoutCreds.id}`, { cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${id}`, data);
fixture: templateWithoutCreds.fixture, templateCredentialsSetupPage.visitTemplateCredentialSetupPage(id);
});
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(templateWithoutCreds.id);
const expectedAppNames = ['1. Email (IMAP)', '2. Nextcloud']; const expectedAppNames = ['1. Email (IMAP)', '2. Nextcloud'];
const expectedAppDescriptions = [ const expectedAppDescriptions = [
@ -151,7 +159,7 @@ describe('Template credentials setup', () => {
workflowPage.getters.canvasNodes().should('have.length', 3); workflowPage.getters.canvasNodes().should('have.length', 3);
}); });
describe('Credential setup from workflow editor', () => { describe('Credential setup from workflow editor', { disableAutoLogin: true }, () => {
beforeEach(() => { beforeEach(() => {
cy.resetDatabase(); cy.resetDatabase();
cy.signinAsOwner(); cy.signinAsOwner();
@ -189,7 +197,7 @@ describe('Template credentials setup', () => {
// Focus the canvas so the copy to clipboard works // Focus the canvas so the copy to clipboard works
workflowPage.getters.canvasNodes().eq(0).realClick(); workflowPage.getters.canvasNodes().eq(0).realClick();
workflowPage.actions.selectAll(); workflowPage.actions.hitSelectAll();
workflowPage.actions.hitCopy(); workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');

View file

@ -1,11 +1,10 @@
import { INSTANCE_ADMIN, INSTANCE_OWNER } from '../constants';
import { SettingsPage } from '../pages/settings'; import { SettingsPage } from '../pages/settings';
const settingsPage = new SettingsPage(); const settingsPage = new SettingsPage();
describe('Admin user', { disableAutoLogin: true }, () => { describe('Admin user', { disableAutoLogin: true }, () => {
it('should see same Settings sub menu items as instance owner', () => { it('should see same Settings sub menu items as instance owner', () => {
cy.signin(INSTANCE_OWNER); cy.signinAsOwner();
cy.visit(settingsPage.url); cy.visit(settingsPage.url);
let ownerMenuItems = 0; let ownerMenuItems = 0;
@ -15,7 +14,7 @@ describe('Admin user', { disableAutoLogin: true }, () => {
}); });
cy.signout(); cy.signout();
cy.signin(INSTANCE_ADMIN); cy.signinAsAdmin();
cy.visit(settingsPage.url); cy.visit(settingsPage.url);
settingsPage.getters.menuItems().should('have.length', ownerMenuItems); 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 { WorkflowsPage } from '../pages/workflows';
import { import {
closeVersionUpdatesPanel, closeVersionUpdatesPanel,
@ -11,11 +10,7 @@ const workflowsPage = new WorkflowsPage();
describe('Versions', () => { describe('Versions', () => {
it('should open updates panel', () => { it('should open updates panel', () => {
cy.intercept('GET', '/rest/settings', (req) => { cy.overrideSettings({
req.continue((res) => {
if (res.body.hasOwnProperty('data')) {
res.body.data = {
...res.body.data,
releaseChannel: 'stable', releaseChannel: 'stable',
versionCli: '1.0.0', versionCli: '1.0.0',
versionNotifications: { versionNotifications: {
@ -23,40 +18,10 @@ describe('Versions', () => {
endpoint: 'https://api.n8n.io/api/versions/', endpoint: 'https://api.n8n.io/api/versions/',
infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html', 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>',
},
{
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.visit(workflowsPage.url);
cy.wait('@settings'); cy.wait('@loadSettings');
getVersionUpdatesPanelOpenButton().should('contain', '2 updates'); getVersionUpdatesPanelOpenButton().should('contain', '2 updates');
openVersionUpdatesPanel(); 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 { WorkflowPage } from '../pages';
import { MessageBox as MessageBoxClass } from '../pages/modals/message-box'; import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
import { errorToast, successToast } from '../pages/notifications';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const messageBox = new MessageBoxClass(); const messageBox = new MessageBoxClass();
@ -29,9 +30,9 @@ describe('Import workflow', () => {
workflowPage.getters.canvasNodes().should('have.length', 4); 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', () => { it('clicking outside modal should not show error toast', () => {
@ -42,7 +43,7 @@ describe('Import workflow', () => {
cy.get('body').click(0, 0); cy.get('body').click(0, 0);
workflowPage.getters.errorToast().should('not.exist'); errorToast().should('not.exist');
}); });
it('canceling modal should not show error toast', () => { it('canceling modal should not show error toast', () => {
@ -52,7 +53,7 @@ describe('Import workflow', () => {
workflowPage.getters.workflowMenuItemImportFromURLItem().click(); workflowPage.getters.workflowMenuItemImportFromURLItem().click();
messageBox.getters.cancel().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.workflowMenuItemImportFromFile().click();
workflowPage.getters workflowPage.getters
.workflowImportInput() .workflowImportInput()
.selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true }); .selectFile('fixtures/Test_workflow-actions_paste-data.json', { force: true });
cy.waitForLoad(false); cy.waitForLoad(false);
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.getters.canvasNodes().should('have.length', 5); workflowPage.getters.canvasNodes().should('have.length', 5);

View file

@ -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.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').type('manual'); 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.searchBar().find('input').clear().type('manual123');
nodeCreatorFeature.getters.creatorItem().should('have.length', 0); nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
nodeCreatorFeature.getters nodeCreatorFeature.getters
@ -159,7 +159,7 @@ describe('Node Creator', () => {
it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => { it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => {
nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.getCreatorItem('Manually').click(); nodeCreatorFeature.getters.getCreatorItem('Trigger manually').click();
nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
@ -308,7 +308,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
NDVModal.actions.close(); NDVModal.actions.close();
WorkflowPage.actions.deleteNode('When clicking "Test workflow"'); WorkflowPage.actions.deleteNode('When clicking Test workflow');
WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click(); WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click(); 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 { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants';
import { getVisibleSelect } from '../utils';
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../constants';
import { NDV, WorkflowPage } from '../pages'; import { NDV, WorkflowPage } from '../pages';
import { NodeCreator } from '../pages/features/node-creator'; import { NodeCreator } from '../pages/features/node-creator';
import { clickCreateNewCredential } from '../composables/ndv'; import { clickCreateNewCredential } from '../composables/ndv';
@ -12,7 +10,7 @@ const ndv = new NDV();
describe('NDV', () => { describe('NDV', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.renameWithUniqueName();
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
}); });
@ -24,6 +22,14 @@ describe('NDV', () => {
ndv.getters.container().should('not.be.visible'); 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', () => { it('should test webhook node', () => {
workflowPage.actions.addInitialNodeToCanvas('Webhook'); workflowPage.actions.addInitialNodeToCanvas('Webhook');
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
@ -46,12 +52,13 @@ describe('NDV', () => {
}); });
it('should change input and go back to canvas', () => { 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.actions.zoomToFit();
workflowPage.getters.canvasNodes().last().dblclick(); workflowPage.getters.canvasNodes().last().dblclick();
ndv.actions.switchInputMode('Table');
ndv.getters.inputSelect().click(); ndv.getters.inputSelect().click();
ndv.getters.inputOption().last().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.inputDataContainer().should('contain', 'start');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
ndv.getters.container().should('not.be.visible'); ndv.getters.container().should('not.be.visible');
@ -59,7 +66,7 @@ describe('NDV', () => {
}); });
it('should disconect Switch outputs if rules order was changed', () => { it('should disconect Switch outputs if rules order was changed', () => {
cy.createFixtureWorkflow('NDV-test-switch_reorder.json', `NDV test switch reorder`); cy.createFixtureWorkflow('NDV-test-switch_reorder.json', 'NDV test switch reorder');
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
@ -105,13 +112,26 @@ describe('NDV', () => {
}); });
it('should show all validation errors when opening pasted node', () => { it('should show all validation errors when opening pasted node', () => {
cy.fixture('Test_workflow_ndv_errors.json').then((data) => { cy.createFixtureWorkflow('Test_workflow_ndv_errors.json', 'Validation errors');
cy.get('body').paste(JSON.stringify(data));
workflowPage.getters.canvasNodes().should('have.have.length', 1); workflowPage.getters.canvasNodes().should('have.have.length', 1);
workflowPage.actions.openNode('Airtable'); workflowPage.actions.openNode('Airtable');
cy.get('.has-issues').should('have.length', 3); cy.get('.has-issues').should('have.length', 3);
cy.get('[class*=hasIssues]').should('have.length', 1); 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', () => { it('should save workflow using keyboard shortcut from NDV', () => {
@ -135,7 +155,7 @@ describe('NDV', () => {
'prop2', 'prop2',
]; ];
function setupSchemaWorkflow() { 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.zoomToFit();
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
ndv.actions.execute(); ndv.actions.execute();
@ -209,7 +229,7 @@ describe('NDV', () => {
it('should display large schema', () => { it('should display large schema', () => {
cy.createFixtureWorkflow( cy.createFixtureWorkflow(
'Test_workflow_schema_test_pinned_data.json', 'Test_workflow_schema_test_pinned_data.json',
`NDV test schema view ${uuid()}`, 'NDV test schema view 2',
); );
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
@ -231,6 +251,9 @@ describe('NDV', () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Set3'); workflowPage.actions.openNode('Set3');
ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table');
ndv.getters ndv.getters
.inputRunSelector() .inputRunSelector()
.should('exist') .should('exist')
@ -242,9 +265,6 @@ describe('NDV', () => {
.find('input') .find('input')
.should('include.value', '2 of 2 (6 items)'); .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.actions.changeOutputRunSelector('1 of 2 (6 items)');
ndv.getters.inputRunSelector().find('input').should('include.value', '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'); ndv.getters.inputTbodyCell(1, 0).should('have.text', '1111');
@ -284,7 +304,7 @@ describe('NDV', () => {
it('should display parameter hints correctly', () => { it('should display parameter hints correctly', () => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`); cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow 1');
workflowPage.actions.openNode('Set1'); workflowPage.actions.openNode('Set1');
ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions 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.getters.parameterInput('name').click(); // remove focus from input, hide expression preview
ndv.actions.validateExpressionPreview('value', output || input); ndv.actions.validateExpressionPreview('value', output ?? input);
ndv.getters.parameterInput('value').clear(); ndv.getters.parameterInput('value').clear();
}); });
}); });
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', () => { it('should flag issues as soon as params are set', () => {
workflowPage.actions.addInitialNodeToCanvas('Webhook'); workflowPage.actions.addInitialNodeToCanvas('Webhook');
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
@ -395,7 +383,11 @@ describe('NDV', () => {
}); });
it('should not retrieve remote options when a parameter value changes', () => { 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' }); workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
// Type something into the field // Type something into the field
ndv.actions.typeIntoParameterInput('otherField', 'test'); ndv.actions.typeIntoParameterInput('otherField', 'test');
@ -411,7 +403,7 @@ describe('NDV', () => {
} }
it('should traverse floating nodes with mouse', () => { it('should traverse floating nodes with mouse', () => {
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`); cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes');
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist'); getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist'); getFloatingNodeByPosition('outputMain').should('exist');
@ -457,7 +449,7 @@ describe('NDV', () => {
}); });
it('should traverse floating nodes with keyboard', () => { it('should traverse floating nodes with keyboard', () => {
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`); cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes');
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist'); getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist'); getFloatingNodeByPosition('outputMain').should('exist');
@ -548,21 +540,21 @@ describe('NDV', () => {
}); });
it('should show node name and version in settings', () => { 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)'); workflowPage.actions.openNode('Edit Fields (old)');
ndv.actions.openSettings(); 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(); ndv.actions.close();
workflowPage.actions.openNode('Edit Fields (latest)'); workflowPage.actions.openNode('Edit Fields (latest)');
ndv.actions.openSettings(); 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(); ndv.actions.close();
workflowPage.actions.openNode('Edit Fields (no typeVersion)'); workflowPage.actions.openNode('Edit Fields (no typeVersion)');
ndv.actions.openSettings(); 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(); ndv.actions.close();
workflowPage.actions.openNode('Function'); workflowPage.actions.openNode('Function');
@ -572,7 +564,7 @@ describe('NDV', () => {
}); });
it('Should render xml and html tags as strings and can search', () => { it('Should render xml and html tags as strings and can search', () => {
cy.createFixtureWorkflow('Test_workflow_xml_output.json', `test`); cy.createFixtureWorkflow('Test_workflow_xml_output.json', 'test');
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
@ -605,8 +597,7 @@ describe('NDV', () => {
ndv.getters.outputDisplayMode().find('label').eq(2).click({ force: true }); ndv.getters.outputDisplayMode().find('label').eq(2).click({ force: true });
ndv.getters ndv.getters
.outputDataContainer() .outputDataContainer()
.findChildByTestId('run-data-schema-item') .findChildByTestId('run-data-schema-item-value')
.find('> span')
.should('include.text', '<?xml version="1.0" encoding="UTF-8"?>'); .should('include.text', '<?xml version="1.0" encoding="UTF-8"?>');
}); });
@ -627,7 +618,7 @@ describe('NDV', () => {
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
// Manual tigger node should show success indicator // 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'); ndv.getters.nodeRunSuccessIndicator().should('exist');
// Code node should show error // Code node should show error
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
@ -673,6 +664,23 @@ describe('NDV', () => {
ndv.getters.parameterInput('operation').find('input').should('have.value', 'Delete'); 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', () => { it('Should open appropriate node creator after clicking on connection hint link', () => {
const nodeCreator = new NodeCreator(); const nodeCreator = new NodeCreator();
const hintMapper = { const hintMapper = {
@ -685,7 +693,7 @@ describe('NDV', () => {
}; };
cy.createFixtureWorkflow( cy.createFixtureWorkflow(
'open_node_creator_for_connection.json', 'open_node_creator_for_connection.json',
`open_node_creator_for_connection ${uuid()}`, 'open_node_creator_for_connection',
); );
Object.entries(hintMapper).forEach(([node, group]) => { Object.entries(hintMapper).forEach(([node, group]) => {
@ -696,20 +704,59 @@ describe('NDV', () => {
}); });
}); });
it('Stop listening for trigger event from NDV', () => { it('should allow selecting item for expressions', () => {
cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); workflowPage.actions.visit();
workflowPage.actions.addInitialNodeToCanvas('Local File Trigger', {
keepNdvOpen: true, cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow 2');
action: 'On Changes To A Specific File', workflowPage.actions.openNode('Set');
isTrigger: true,
ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions
ndv.actions.typeIntoParameterInput('value', '{{', {
parseSpecialCharSequences: false,
}); });
ndv.getters.triggerPanelExecuteButton().should('exist'); ndv.actions.typeIntoParameterInput('value', '$json.input[0].count');
ndv.getters.triggerPanelExecuteButton().realClick(); ndv.getters.inlineExpressionEditorOutput().should('have.text', '0');
ndv.getters.triggerPanelExecuteButton().should('contain', 'Stop Listening');
ndv.getters.triggerPanelExecuteButton().realClick(); ndv.actions.expressionSelectNextItem();
cy.wait('@workflowRun').then(() => { ndv.getters.inlineExpressionEditorOutput().should('have.text', '1');
ndv.getters.triggerPanelExecuteButton().should('contain', 'Test step'); ndv.getters.inlineExpressionEditorItemInput().should('have.value', '1');
workflowPage.getters.successToast().should('exist'); 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 { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV } from '../pages/ndv'; import { NDV } from '../pages/ndv';
import { successToast } from '../pages/notifications';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
const ndv = new NDV(); const ndv = new NDV();
@ -28,13 +30,13 @@ describe('Code node', () => {
it('should execute the placeholder successfully in both modes', () => { it('should execute the placeholder successfully in both modes', () => {
ndv.actions.execute(); ndv.actions.execute();
WorkflowPage.getters.successToast().contains('Node executed successfully'); successToast().contains('Node executed successfully');
ndv.getters.parameterInput('mode').click(); ndv.getters.parameterInput('mode').click();
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item'); ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
ndv.actions.execute(); 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-cta-tooltip-no-prompt').should('exist');
cy.getByTestId('ask-ai-prompt-input') cy.getByTestId('ask-ai-prompt-input')
// Type random 14 character string // 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').realHover();
cy.getByTestId('ask-ai-cta-tooltip-prompt-too-short').should('exist'); cy.getByTestId('ask-ai-cta-tooltip-prompt-too-short').should('exist');
@ -92,14 +94,14 @@ describe('Code node', () => {
cy.getByTestId('ask-ai-prompt-input') cy.getByTestId('ask-ai-prompt-input')
.clear() .clear()
// Type random 15 character string // 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-cta').should('be.enabled');
cy.getByTestId('ask-ai-prompt-counter').should('contain.text', '15 / 600'); cy.getByTestId('ask-ai-prompt-counter').should('contain.text', '15 / 600');
}); });
it('should send correct schema and replace code', () => { 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(); cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious(); ndv.actions.executePrevious();
@ -129,7 +131,7 @@ describe('Code node', () => {
}); });
it('should show error based on status code', () => { 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(); cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious(); ndv.actions.executePrevious();

View file

@ -1,16 +1,15 @@
import { import {
CODE_NODE_NAME, CODE_NODE_NAME,
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
META_KEY,
SCHEDULE_TRIGGER_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME,
INSTANCE_MEMBERS, NOTION_NODE_NAME,
INSTANCE_OWNER,
} from '../constants'; } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import { WorkflowExecutionsTab } from '../pages'; import { WorkflowExecutionsTab } from '../pages';
import { errorToast, successToast } from '../pages/notifications';
const NEW_WORKFLOW_NAME = 'Something else'; const NEW_WORKFLOW_NAME = 'Something else';
const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow'; const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow';
@ -35,6 +34,20 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.isWorkflowSaved(); 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', () => { it('should not be able to activate unsaved workflow', () => {
WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled'); WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled');
}); });
@ -53,6 +66,30 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.isWorkflowActivated(); 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', () => { it('should save new workflow after renaming', () => {
WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME); WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME);
WorkflowPage.getters.isWorkflowSaved(); WorkflowPage.getters.isWorkflowSaved();
@ -96,13 +133,13 @@ describe('Workflow Actions', () => {
); );
cy.reload(); cy.reload();
cy.get('.el-loading-mask').should('exist'); cy.get('.el-loading-mask').should('exist');
cy.get('body').type(META_KEY, { release: false }).type('s'); WorkflowPage.actions.hitSaveWorkflow();
cy.get('body').type(META_KEY, { release: false }).type('s'); WorkflowPage.actions.hitSaveWorkflow();
cy.get('body').type(META_KEY, { release: false }).type('s'); WorkflowPage.actions.hitSaveWorkflow();
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0)); cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0));
cy.waitForLoad(); cy.waitForLoad();
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
cy.get('body').type(META_KEY, { release: false }).type('s'); WorkflowPage.actions.hitSaveWorkflow();
cy.wait('@saveWorkflow'); cy.wait('@saveWorkflow');
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1)); cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1));
}); });
@ -132,10 +169,11 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.canvasNodes().should('have.have.length', 2); WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
cy.get('#node-creator').should('not.exist'); 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('.jtk-drag-selected').should('have.length', 2);
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('c'); WorkflowPage.actions.hitCopy();
WorkflowPage.getters.successToast().should('exist'); successToast().should('exist');
}); });
it('should paste nodes (both current and old node versions)', () => { it('should paste nodes (both current and old node versions)', () => {
@ -147,10 +185,24 @@ 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', () => { it('should update workflow settings', () => {
cy.visit(WorkflowPages.url); cy.visit(WorkflowPages.url);
WorkflowPages.getters.workflowCards().then((cards) => { cy.intercept('GET', '/rest/workflows', (req) => {
const totalWorkflows = cards.length; req.on('response', (res) => {
const totalWorkflows = res.body.count ?? 0;
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
// Open settings dialog // Open settings dialog
@ -186,8 +238,9 @@ describe('Workflow Actions', () => {
// Save settings // Save settings
WorkflowPage.getters.workflowSettingsSaveButton().click(); WorkflowPage.getters.workflowSettingsSaveButton().click();
WorkflowPage.getters.workflowSettingsModal().should('not.exist'); WorkflowPage.getters.workflowSettingsModal().should('not.exist');
WorkflowPage.getters.successToast().should('exist'); successToast().should('exist');
}); });
}).as('loadWorkflows');
}); });
it('should not be able to delete unsaved workflow', () => { it('should not be able to delete unsaved workflow', () => {
@ -203,8 +256,8 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.workflowMenuItemDelete().click(); WorkflowPage.getters.workflowMenuItemDelete().click();
cy.get('div[role=dialog][aria-modal=true]').should('be.visible'); cy.get('div[role=dialog][aria-modal=true]').should('be.visible');
cy.get('button.btn--confirm').should('be.visible').click(); cy.get('button.btn--confirm').should('be.visible').click();
WorkflowPage.getters.successToast().should('exist'); successToast().should('exist');
cy.url().should('include', '/workflow/new'); cy.url().should('include', WorkflowPages.url);
}); });
describe('duplicate workflow', () => { describe('duplicate workflow', () => {
@ -232,7 +285,7 @@ describe('Workflow Actions', () => {
.contains('Duplicate') .contains('Duplicate')
.should('be.visible'); .should('be.visible');
WorkflowPage.getters.duplicateWorkflowModal().find('button').contains('Duplicate').click(); WorkflowPage.getters.duplicateWorkflowModal().find('button').contains('Duplicate').click();
WorkflowPage.getters.errorToast().should('not.exist'); errorToast().should('not.exist');
} }
beforeEach(() => { beforeEach(() => {
@ -272,18 +325,43 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.canvasNodePlusEndpointByName(EDIT_FIELDS_SET_NODE_NAME).click(); WorkflowPage.getters.canvasNodePlusEndpointByName(EDIT_FIELDS_SET_NODE_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); 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', () => { describe('Menu entry Push To Git', () => {
it('should not show up in the menu for members', () => { it('should not show up in the menu for members', () => {
cy.signin(INSTANCE_MEMBERS[0]); cy.signinAsMember(0);
cy.visit(WorkflowPages.url); cy.visit(WorkflowPages.url);
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
WorkflowPage.getters.workflowMenuItemGitPush().should('not.exist'); WorkflowPage.getters.workflowMenuItemGitPush().should('not.exist');
}); });
it('should show up for owners', () => { it('should show up for owners', () => {
cy.signin(INSTANCE_OWNER); cy.signinAsOwner();
cy.visit(WorkflowPages.url); cy.visit(WorkflowPages.url);
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
WorkflowPage.getters.workflowMenuItemGitPush().should('exist'); WorkflowPage.getters.workflowMenuItemGitPush().should('exist');

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@
{ {
"parameters": {}, "parameters": {},
"id": "5ae8991f-08a2-4b27-b61c-85e3b8a83693", "id": "5ae8991f-08a2-4b27-b61c-85e3b8a83693",
"name": "When clicking \"Test workflow\"", "name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger", "type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
@ -14,7 +14,7 @@
}, },
{ {
"parameters": { "parameters": {
"url": "https://random-data-api.com/api/v2/users?size=5", "url": "https://internal.users.n8n.cloud/webhook/random-data-api",
"options": {} "options": {}
}, },
"id": "22511d75-ab54-49e1-b8af-08b8b3372373", "id": "22511d75-ab54-49e1-b8af-08b8b3372373",
@ -28,7 +28,7 @@
}, },
{ {
"parameters": { "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", "id": "4b66b15a-1685-46c1-a5e3-ebf8cdb11d21",
"name": "do something with them", "name": "do something with them",
@ -78,14 +78,14 @@
} }
} }
], ],
"When clicking \"Test workflow\"": [ "When clicking Test workflow": [
{ {
"json": {} "json": {}
} }
] ]
}, },
"connections": { "connections": {
"When clicking \"Test workflow\"": { "When clicking Test workflow": {
"main": [ "main": [
[ [
{ {

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