mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
Merge branch 'master' of github.com:n8n-io/n8n into invoiceninja-add-more-query-params
This commit is contained in:
commit
8f2d1f3e4d
7
.devcontainer/Dockerfile
Normal file
7
.devcontainer/Dockerfile
Normal 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
|
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
24
.devcontainer/docker-compose.yml
Normal file
24
.devcontainer/docker-compose.yml
Normal 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
|
|
@ -10,6 +10,7 @@ packages/**/.turbo
|
|||
packages/**/*.test.*
|
||||
.git
|
||||
.github
|
||||
!.github/scripts
|
||||
*.tsbuildinfo
|
||||
packages/cli/dist/**/e2e.*
|
||||
docker/compose
|
||||
|
|
2
.github/docker-compose.yml
vendored
2
.github/docker-compose.yml
vendored
|
@ -19,7 +19,7 @@ services:
|
|||
restart: always
|
||||
environment:
|
||||
- POSTGRES_DB=n8n
|
||||
- POSTGRES_USER=root
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=password
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
|
28
.github/pull_request_template.md
vendored
28
.github/pull_request_template.md
vendored
|
@ -1,16 +1,26 @@
|
|||
## Summary
|
||||
> Describe what the PR does and how to test. Photos and videos are recommended.
|
||||
|
||||
<!--
|
||||
Describe what the PR does and how to test.
|
||||
Photos and videos are recommended.
|
||||
-->
|
||||
|
||||
## Related Linear tickets, Github issues, and Community forum posts
|
||||
|
||||
## Related tickets and issues
|
||||
> Include links to **Linear ticket** or Github issue or Community forum post. Important in order to close *automatically* and provide context to reviewers.
|
||||
|
||||
|
||||
<!--
|
||||
Include links to **Linear ticket** or Github issue or Community forum post.
|
||||
Important in order to close *automatically* and provide context to reviewers.
|
||||
-->
|
||||
|
||||
## Review / Merge checklist
|
||||
- [ ] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md))
|
||||
|
||||
- [ ] PR title and summary are descriptive. ([conventions](../blob/master/.github/pull_request_title_conventions.md)) <!--
|
||||
**Remember, the title automatically goes into the changelog.
|
||||
Use `(no-changelog)` otherwise.**
|
||||
-->
|
||||
- [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created.
|
||||
- [ ] Tests included.
|
||||
> A bug is not considered fixed, unless a test is added to prevent it from happening again.
|
||||
> A feature is not complete without tests.
|
||||
- [ ] Tests included. <!--
|
||||
A bug is not considered fixed, unless a test is added to prevent it from happening again.
|
||||
A feature is not complete without tests.
|
||||
-->
|
||||
- [ ] PR Labeled with `release/backport` (if the PR is an urgent fix that needs to be backported)
|
||||
|
|
2
.github/pull_request_title_conventions.md
vendored
2
.github/pull_request_title_conventions.md
vendored
|
@ -37,7 +37,7 @@ Must be one of the following:
|
|||
- `test` - Adding missing tests or correcting existing tests
|
||||
- `docs` - Documentation only changes
|
||||
- `refactor` - A code change that neither fixes a bug nor adds a feature
|
||||
- `build` - Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
- `build` - Changes that affect the build system or external dependencies (example scopes: broccoli, npm)
|
||||
- `ci` - Changes to our CI configuration files and scripts (e.g. Github actions)
|
||||
|
||||
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any BREAKING CHANGE (see Footer section below), the commit will always appear in the changelog.
|
||||
|
|
44
.github/scripts/ensure-provenance-fields.mjs
vendored
Normal file
44
.github/scripts/ensure-provenance-fields.mjs
vendored
Normal 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');
|
||||
}
|
|
@ -1,12 +1,15 @@
|
|||
const { writeFileSync } = require('fs');
|
||||
const { resolve } = require('path');
|
||||
const baseDir = resolve(__dirname, '..');
|
||||
const baseDir = resolve(__dirname, '../..');
|
||||
|
||||
const trimPackageJson = (packageName) => {
|
||||
const filePath = resolve(baseDir, 'packages', packageName, 'package.json');
|
||||
const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require(
|
||||
filePath,
|
||||
);
|
||||
if (packageName === '@n8n/chat') {
|
||||
packageJson.dependencies = dependencies;
|
||||
}
|
||||
writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
|
||||
};
|
||||
|
|
@ -17,9 +17,9 @@ jobs:
|
|||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
4
.github/workflows/check-pr-title.yml
vendored
4
.github/workflows/check-pr-title.yml
vendored
|
@ -19,9 +19,9 @@ jobs:
|
|||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
4
.github/workflows/check-tests.yml
vendored
4
.github/workflows/check-tests.yml
vendored
|
@ -20,9 +20,9 @@ jobs:
|
|||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4.0.1
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- run: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
|
|
43
.github/workflows/chromatic.yml
vendored
43
.github/workflows/chromatic.yml
vendored
|
@ -1,26 +1,59 @@
|
|||
name: Chromatic
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branch:
|
||||
- 'master'
|
||||
paths:
|
||||
- packages/design-system/**
|
||||
- .github/workflows/chromatic.yml
|
||||
|
||||
concurrency:
|
||||
group: chromatic-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
chromatic:
|
||||
if: ${{ github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'community') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Publish to Chromatic
|
||||
uses: chromaui/action@latest
|
||||
uses: chromaui/action@v11
|
||||
id: chromatic_tests
|
||||
continue-on-error: true
|
||||
with:
|
||||
workingDir: packages/design-system
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
exitZeroOnChanges: false
|
||||
|
||||
- name: Success comment
|
||||
if: steps.chromatic_tests.outcome == 'success'
|
||||
uses: peter-evans/create-or-update-comment@v4.0.0
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
:white_check_mark: No visual regressions found.
|
||||
|
||||
- name: Fail comment
|
||||
if: steps.chromatic_tests.outcome != 'success'
|
||||
uses: peter-evans/create-or-update-comment@v4.0.0
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
[:warning: Visual regressions found](${{steps.chromatic_tests.outputs.url}}): ${{steps.chromatic_tests.outputs.changeCount}}
|
||||
|
|
56
.github/workflows/ci-master.yml
vendored
56
.github/workflows/ci-master.yml
vendored
|
@ -9,25 +9,23 @@ jobs:
|
|||
install-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
timeout-minutes: 30
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- run: corepack enable
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: 20.x
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
|
@ -35,7 +33,7 @@ jobs:
|
|||
uses: actions/cache/save@v4.0.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint
|
||||
key: ${{ github.sha }}-base:build
|
||||
|
||||
unit-test:
|
||||
name: Unit tests
|
||||
|
@ -43,46 +41,22 @@ jobs:
|
|||
needs: install-and-build
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
node-version: [18.x, 20.x, 22.4]
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
nodeVersion: ${{ matrix.node-version }}
|
||||
cacheKey: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint
|
||||
collectCoverage: true
|
||||
cacheKey: ${{ github.sha }}-base:build
|
||||
collectCoverage: ${{ matrix.node-version == '20.x' }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
lint:
|
||||
name: Lint changes
|
||||
runs-on: ubuntu-latest
|
||||
name: Lint
|
||||
uses: ./.github/workflows/linting-reusable.yml
|
||||
needs: install-and-build
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: ${{ inputs.branch }}
|
||||
|
||||
- run: corepack enable
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint
|
||||
|
||||
- name: Lint
|
||||
env:
|
||||
CI_LINT_MASTER: true
|
||||
run: pnpm lint
|
||||
cacheKey: ${{ github.sha }}-base:build
|
||||
|
||||
notify-on-failure:
|
||||
name: Notify Slack on failure
|
||||
|
|
34
.github/workflows/ci-postgres-mysql.yml
vendored
34
.github/workflows/ci-postgres-mysql.yml
vendored
|
@ -7,6 +7,7 @@ on:
|
|||
pull_request:
|
||||
paths:
|
||||
- packages/cli/src/databases/**
|
||||
- .github/workflows/ci-postgres-mysql.yml
|
||||
|
||||
concurrency:
|
||||
group: db-${{ github.event.pull_request.number || github.ref }}
|
||||
|
@ -19,12 +20,15 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
|
||||
- name: Build Backend
|
||||
run: pnpm build:backend
|
||||
|
||||
|
@ -45,12 +49,15 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
with:
|
||||
|
@ -59,7 +66,7 @@ jobs:
|
|||
|
||||
- name: Test SQLite Pooled
|
||||
working-directory: packages/cli
|
||||
run: pnpm jest --coverage
|
||||
run: pnpm jest
|
||||
|
||||
mysql:
|
||||
name: MySQL
|
||||
|
@ -71,12 +78,15 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
with:
|
||||
|
@ -92,7 +102,7 @@ jobs:
|
|||
|
||||
- name: Test MySQL
|
||||
working-directory: packages/cli
|
||||
run: pnpm test:mysql
|
||||
run: pnpm test:mysql --testTimeout 20000
|
||||
|
||||
postgres:
|
||||
name: Postgres
|
||||
|
@ -101,15 +111,19 @@ jobs:
|
|||
timeout-minutes: 20
|
||||
env:
|
||||
DB_POSTGRESDB_PASSWORD: password
|
||||
DB_POSTGRESDB_POOL_SIZE: 1 # Detect connection pooling deadlocks
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
with:
|
||||
|
|
48
.github/workflows/ci-pull-requests.yml
vendored
48
.github/workflows/ci-pull-requests.yml
vendored
|
@ -3,7 +3,7 @@ name: Build, unit test and lint branch
|
|||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
install:
|
||||
install-and-build:
|
||||
name: Install & Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
@ -13,57 +13,41 @@ jobs:
|
|||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- run: corepack enable
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Run typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache/save@v4.0.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-base:18-test-lint
|
||||
key: ${{ github.sha }}-base:build
|
||||
|
||||
unit-test:
|
||||
name: Unit tests
|
||||
uses: ./.github/workflows/units-tests-reusable.yml
|
||||
needs: install
|
||||
needs: install-and-build
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
cacheKey: ${{ github.sha }}-base:18-test-lint
|
||||
cacheKey: ${{ github.sha }}-base:build
|
||||
|
||||
lint:
|
||||
name: Lint changes
|
||||
runs-on: ubuntu-latest
|
||||
needs: install
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
name: Lint
|
||||
uses: ./.github/workflows/linting-reusable.yml
|
||||
needs: install-and-build
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- run: corepack enable
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v4.0.1
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-base:18-test-lint
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
cacheKey: ${{ github.sha }}-base:build
|
||||
|
|
3
.github/workflows/docker-base-image.yml
vendored
3
.github/workflows/docker-base-image.yml
vendored
|
@ -7,10 +7,11 @@ on:
|
|||
description: 'Node.js version to build this image with.'
|
||||
type: choice
|
||||
required: true
|
||||
default: '18'
|
||||
default: '20'
|
||||
options:
|
||||
- '18'
|
||||
- '20'
|
||||
- '22'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
13
.github/workflows/e2e-reusable.yml
vendored
13
.github/workflows/e2e-reusable.yml
vendored
|
@ -40,7 +40,7 @@ on:
|
|||
containers:
|
||||
description: 'Number of containers to run tests in.'
|
||||
required: false
|
||||
default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]'
|
||||
default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]'
|
||||
type: string
|
||||
pr_number:
|
||||
description: 'PR number to run tests for.'
|
||||
|
@ -87,7 +87,7 @@ jobs:
|
|||
git fetch origin pull/${{ inputs.pr_number }}/head
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
- uses: pnpm/action-setup@v4.0.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
@ -99,10 +99,9 @@ jobs:
|
|||
runTests: false
|
||||
install: false
|
||||
build: pnpm build
|
||||
env:
|
||||
VUE_APP_MAX_PINNED_DATA_SIZE: 16384
|
||||
|
||||
- name: Cypress install
|
||||
working-directory: cypress
|
||||
run: pnpm cypress:install
|
||||
|
||||
- name: Cache build artifacts
|
||||
|
@ -138,7 +137,7 @@ jobs:
|
|||
git fetch origin pull/${{ inputs.pr_number }}/head
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
- uses: pnpm/action-setup@v4.0.0
|
||||
|
||||
- name: Restore cached pnpm modules
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
|
@ -155,6 +154,7 @@ jobs:
|
|||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6.6.1
|
||||
with:
|
||||
working-directory: cypress
|
||||
install: false
|
||||
start: pnpm start
|
||||
wait-on: 'http://localhost:5678'
|
||||
|
@ -164,8 +164,7 @@ jobs:
|
|||
# We have to provide custom ci-build-id key to make sure that this workflow could be run multiple times
|
||||
# in the same parent workflow
|
||||
ci-build-id: ${{ needs.prepare.outputs.uuid }}
|
||||
spec: '/__w/n8n/n8n/cypress/${{ inputs.spec }}'
|
||||
config-file: /__w/n8n/n8n/cypress.config.js
|
||||
spec: '${{ inputs.spec }}'
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
|
|
57
.github/workflows/linting-reusable.yml
vendored
Normal file
57
.github/workflows/linting-reusable.yml
vendored
Normal 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
27
.github/workflows/notify-pr-status.yml
vendored
Normal 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) }} }'
|
4
.github/workflows/release-create-pr.yml
vendored
4
.github/workflows/release-create-pr.yml
vendored
|
@ -36,9 +36,9 @@ jobs:
|
|||
ref: ${{ github.event.inputs.base-branch }}
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- run: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
|
|
10
.github/workflows/release-publish.yml
vendored
10
.github/workflows/release-publish.yml
vendored
|
@ -14,8 +14,11 @@ jobs:
|
|||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -24,9 +27,9 @@ jobs:
|
|||
fetch-depth: 0
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
|
@ -42,7 +45,8 @@ jobs:
|
|||
- name: Publish to NPM
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
node scripts/trim-fe-packageJson.js
|
||||
node .github/scripts/trim-fe-packageJson.js
|
||||
node .github/scripts/ensure-provenance-fields.mjs
|
||||
sed -i "s/default: 'dev'/default: 'stable'/g" packages/cli/dist/config/schema.js
|
||||
pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks
|
||||
npm dist-tag rm n8n rc
|
||||
|
|
12
.github/workflows/release-push-to-channel.yml
vendored
12
.github/workflows/release-push-to-channel.yml
vendored
|
@ -22,9 +22,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
npm dist-tag add n8n@${{ github.event.inputs.version }} ${{ github.event.inputs.release-channel }}
|
||||
|
@ -53,3 +53,11 @@ jobs:
|
|||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.release-channel }} ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }}
|
||||
|
||||
update-docs:
|
||||
name: Update latest and next in the docs
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-to-npm, release-to-docker-hub]
|
||||
steps:
|
||||
- continue-on-error: true
|
||||
run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/update-latest-next'
|
||||
|
|
4
.github/workflows/test-workflows.yml
vendored
4
.github/workflows/test-workflows.yml
vendored
|
@ -26,9 +26,9 @@ jobs:
|
|||
- run: corepack enable
|
||||
working-directory: n8n
|
||||
|
||||
- uses: actions/setup-node@v4.0.1
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'n8n/pnpm-lock.yaml'
|
||||
|
||||
|
|
29
.github/workflows/units-tests-reusable.yml
vendored
29
.github/workflows/units-tests-reusable.yml
vendored
|
@ -4,24 +4,28 @@ on:
|
|||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'GitHub ref to test.'
|
||||
description: GitHub ref to test.
|
||||
required: false
|
||||
type: string
|
||||
default: 'master'
|
||||
default: master
|
||||
nodeVersion:
|
||||
description: 'Version of node to use.'
|
||||
description: Version of node to use.
|
||||
required: false
|
||||
type: string
|
||||
default: '18.x'
|
||||
default: 20.x
|
||||
cacheKey:
|
||||
description: 'Cache key for modules and build artifacts.'
|
||||
description: Cache key for modules and build artifacts.
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
collectCoverage:
|
||||
required: false
|
||||
default: 'false'
|
||||
type: string
|
||||
default: false
|
||||
type: boolean
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
description: 'Codecov upload token.'
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
|
@ -37,7 +41,7 @@ jobs:
|
|||
|
||||
- run: corepack enable
|
||||
- name: Use Node.js ${{ inputs.nodeVersion }}
|
||||
uses: actions/setup-node@v4.0.1
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: ${{ inputs.nodeVersion }}
|
||||
cache: pnpm
|
||||
|
@ -45,6 +49,9 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
|
||||
- name: Build
|
||||
if: ${{ inputs.cacheKey == '' }}
|
||||
run: pnpm build
|
||||
|
@ -66,7 +73,7 @@ jobs:
|
|||
run: pnpm test:frontend
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: ${{ inputs.collectCoverage == 'true' }}
|
||||
uses: codecov/codecov-action@v3
|
||||
if: inputs.collectCoverage
|
||||
uses: codecov/codecov-action@v4.5.0
|
||||
with:
|
||||
files: packages/@n8n/chat/coverage/cobertura-coverage.xml,packages/@n8n/nodes-langchain/coverage/cobertura-coverage.xml,packages/@n8n/permissions/coverage/cobertura-coverage.xml,packages/@n8n/client-oauth2/coverage/cobertura-coverage.xml,packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -16,10 +16,8 @@ _START_PACKAGE
|
|||
nodelinter.config.json
|
||||
**/package-lock.json
|
||||
packages/**/.turbo
|
||||
.turbo
|
||||
*.tsbuildinfo
|
||||
cypress/videos/*
|
||||
cypress/screenshots/*
|
||||
cypress/downloads/*
|
||||
*.swp
|
||||
CHANGELOG-*.md
|
||||
*.mdx
|
||||
|
|
4
.npmrc
4
.npmrc
|
@ -7,4 +7,8 @@ prefer-workspace-packages = true
|
|||
link-workspace-packages = deep
|
||||
hoist = true
|
||||
shamefully-hoist = true
|
||||
hoist-workspace-packages = false
|
||||
loglevel = warn
|
||||
package-manager-strict=false
|
||||
# https://github.com/pnpm/pnpm/issues/7024
|
||||
package-import-method=clone-or-copy
|
||||
|
|
529
CHANGELOG.md
529
CHANGELOG.md
|
@ -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 CVE‑2023‑37466 ([#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)
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ Great that you are here and you want to contribute to n8n
|
|||
- [Code of conduct](#code-of-conduct)
|
||||
- [Directory structure](#directory-structure)
|
||||
- [Development setup](#development-setup)
|
||||
- [Dev Container](#dev-container)
|
||||
- [Requirements](#requirements)
|
||||
- [Node.js](#nodejs)
|
||||
- [pnpm](#pnpm)
|
||||
|
@ -41,7 +42,6 @@ n8n is split up in different modules which are all in a single mono repository.
|
|||
The most important directories:
|
||||
|
||||
- [/docker/image](/docker/images) - Dockerfiles to create n8n containers
|
||||
- [/docker/compose](/docker/compose) - Examples Docker Setups
|
||||
- [/packages](/packages) - The different n8n modules
|
||||
- [/packages/cli](/packages/cli) - CLI code to run front- & backend
|
||||
- [/packages/core](/packages/core) - Core code which handles workflow
|
||||
|
@ -60,15 +60,19 @@ The most important directories:
|
|||
If you want to change or extend n8n you have to make sure that all the needed
|
||||
dependencies are installed and the packages get linked correctly. Here's a short guide on how that can be done:
|
||||
|
||||
### Dev Container
|
||||
|
||||
If you already have VS Code and Docker installed, you can click [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/n8n-io/n8n) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
||||
|
||||
### Requirements
|
||||
|
||||
#### Node.js
|
||||
|
||||
[Node.js](https://nodejs.org/en/) version 16.9 or newer is required for development purposes.
|
||||
[Node.js](https://nodejs.org/en/) version 18.10 or newer is required for development purposes.
|
||||
|
||||
#### pnpm
|
||||
|
||||
[pnpm](https://pnpm.io/) version 8.9 or newer is required for development purposes. We recommend installing it with [corepack](#corepack).
|
||||
[pnpm](https://pnpm.io/) version 9.1 or newer is required for development purposes. We recommend installing it with [corepack](#corepack).
|
||||
|
||||
##### pnpm workspaces
|
||||
|
||||
|
|
34
cypress/.eslintrc.js
Normal file
34
cypress/.eslintrc.js
Normal 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
3
cypress/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
videos/
|
||||
screenshots/
|
||||
downloads/
|
4
cypress/augmentation.d.ts
vendored
Normal file
4
cypress/augmentation.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module 'cypress-otp' {
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function generateOTPToken(secret: string): string;
|
||||
}
|
|
@ -10,7 +10,7 @@ export const getCloseBecomeTemplateCreatorCtaButton = () =>
|
|||
//#region Actions
|
||||
|
||||
export const interceptCtaRequestWithResponse = (becomeCreator: boolean) => {
|
||||
return cy.intercept('GET', `/rest/cta/become-creator`, {
|
||||
return cy.intercept('GET', '/rest/cta/become-creator', {
|
||||
body: becomeCreator,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -7,15 +7,15 @@ export function getManualChatModal() {
|
|||
}
|
||||
|
||||
export function getManualChatInput() {
|
||||
return cy.getByTestId('workflow-chat-input');
|
||||
return getManualChatModal().get('.chat-inputs textarea');
|
||||
}
|
||||
|
||||
export function getManualChatSendButton() {
|
||||
return getManualChatModal().getByTestId('workflow-chat-send-button');
|
||||
return getManualChatModal().get('.chat-input-send-button');
|
||||
}
|
||||
|
||||
export function getManualChatMessages() {
|
||||
return getManualChatModal().get('.messages .message');
|
||||
return getManualChatModal().get('.chat-messages-list .chat-message');
|
||||
}
|
||||
|
||||
export function getManualChatModalCloseButton() {
|
||||
|
|
|
@ -42,7 +42,7 @@ export function closeCredentialModal() {
|
|||
getCredentialModalCloseButton().click();
|
||||
}
|
||||
|
||||
export function setCredentialValues(values: Record<string, any>, save = true) {
|
||||
export function setCredentialValues(values: Record<string, string>, save = true) {
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
setCredentialConnectionParameterInputByName(key, value);
|
||||
});
|
||||
|
|
3
cypress/composables/modals/save-changes-modal.ts
Normal file
3
cypress/composables/modals/save-changes-modal.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function getSaveChangesModal() {
|
||||
return cy.get('.el-overlay').contains('Save changes before leaving?');
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* Getters
|
||||
*/
|
||||
|
||||
import { getVisibleSelect } from "../utils";
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
export function getCredentialSelect(eq = 0) {
|
||||
return cy.getByTestId('node-credentials-select').eq(eq);
|
||||
|
@ -75,7 +75,7 @@ export function setParameterInputByName(name: string, value: string) {
|
|||
}
|
||||
|
||||
export function toggleParameterCheckboxInputByName(name: string) {
|
||||
getParameterInputByName(name).find('input[type="checkbox"]').realClick()
|
||||
getParameterInputByName(name).find('input[type="checkbox"]').realClick();
|
||||
}
|
||||
|
||||
export function setParameterSelectByContent(name: string, content: string) {
|
||||
|
|
66
cypress/composables/projects.ts
Normal file
66
cypress/composables/projects.ts
Normal 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();
|
||||
},
|
||||
};
|
|
@ -2,4 +2,4 @@
|
|||
* Getters
|
||||
*/
|
||||
|
||||
export const getSetupWorkflowCredentialsButton = () => cy.get(`button:contains("Set up template")`);
|
||||
export const getSetupWorkflowCredentialsButton = () => cy.get('button:contains("Set up template")');
|
||||
|
|
|
@ -51,7 +51,7 @@ export function getNodeByName(name: string) {
|
|||
export function disableNode(name: string) {
|
||||
const target = getNodeByName(name);
|
||||
target.rightclick(name ? 'center' : 'topLeft', { force: true });
|
||||
cy.getByTestId(`context-menu-item-toggle_activation`).click();
|
||||
cy.getByTestId('context-menu-item-toggle_activation').click();
|
||||
}
|
||||
|
||||
export function getConnectionBySourceAndTarget(source: string, target: string) {
|
||||
|
|
|
@ -35,7 +35,7 @@ export const INSTANCE_MEMBERS = [
|
|||
];
|
||||
|
||||
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
|
||||
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Test workflow"';
|
||||
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking ‘Test workflow’';
|
||||
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
|
||||
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
||||
export const CODE_NODE_NAME = 'Code';
|
||||
|
@ -53,12 +53,14 @@ export const AGENT_NODE_NAME = 'AI Agent';
|
|||
export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain';
|
||||
export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory';
|
||||
export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator';
|
||||
export const AI_TOOL_CODE_NODE_NAME = 'Custom Code Tool';
|
||||
export const AI_TOOL_CODE_NODE_NAME = 'Code Tool';
|
||||
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
|
||||
export const AI_TOOL_HTTP_NODE_NAME = 'HTTP Request Tool';
|
||||
export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model';
|
||||
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
|
||||
export const WEBHOOK_NODE_NAME = 'Webhook';
|
||||
|
||||
export const META_KEY = Cypress.platform === 'darwin' ? '{meta}' : '{ctrl}';
|
||||
export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl';
|
||||
|
||||
export const NEW_GOOGLE_ACCOUNT_NAME = 'Gmail account';
|
||||
export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account';
|
||||
|
|
|
@ -18,10 +18,11 @@ module.exports = defineConfig({
|
|||
screenshotOnRunFailure: true,
|
||||
experimentalInteractiveRunEvents: true,
|
||||
experimentalSessionAndOrigin: true,
|
||||
},
|
||||
env: {
|
||||
MAX_PINNED_DATA_SIZE: process.env.VUE_APP_MAX_PINNED_DATA_SIZE
|
||||
? parseInt(process.env.VUE_APP_MAX_PINNED_DATA_SIZE, 10)
|
||||
: 16 * 1024,
|
||||
specPattern: 'e2e/**/*.ts',
|
||||
supportFile: 'support/e2e.ts',
|
||||
fixturesFolder: 'fixtures',
|
||||
downloadsFolder: 'downloads',
|
||||
screenshotsFolder: 'screenshots',
|
||||
videosFolder: 'videos',
|
||||
},
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
||||
|
||||
const WorkflowsPage = new WorkflowsPageClass();
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
@ -16,7 +16,7 @@ describe('Workflows', () => {
|
|||
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
|
||||
WorkflowsPage.getters.newWorkflowButtonCard().click();
|
||||
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json', `Empty State Card Workflow ${uuid()}`);
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
|
||||
|
||||
WorkflowPage.getters.workflowTags().should('contain.text', 'some-tag-1');
|
||||
WorkflowPage.getters.workflowTags().should('contain.text', 'some-tag-2');
|
||||
|
@ -27,7 +27,7 @@ describe('Workflows', () => {
|
|||
cy.visit(WorkflowsPage.url);
|
||||
WorkflowsPage.getters.createWorkflowButton().click();
|
||||
|
||||
cy.createFixtureWorkflow('Test_workflow_2.json', `My New Workflow ${uuid()}`);
|
||||
cy.createFixtureWorkflow('Test_workflow_2.json', getUniqueWorkflowName('My New Workflow'));
|
||||
|
||||
WorkflowPage.getters.workflowTags().should('contain.text', 'other-tag-1');
|
||||
WorkflowPage.getters.workflowTags().should('contain.text', 'other-tag-2');
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { CODE_NODE_NAME, SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from './../constants';
|
||||
import { SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
|
||||
import {
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
CODE_NODE_NAME,
|
||||
SET_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
|
||||
import { NDV } from '../pages/ndv';
|
||||
|
@ -16,24 +20,6 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.actions.visit();
|
||||
});
|
||||
|
||||
it('should undo/redo adding nodes', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
|
||||
});
|
||||
|
||||
it('should undo/redo adding connected nodes', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should undo/redo adding node in the middle', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
@ -43,10 +29,9 @@ describe('Undo/Redo', () => {
|
|||
SET_NODE_NAME,
|
||||
);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.css', 'left', '860px')
|
||||
.should('have.css', 'top', '220px');
|
||||
WorkflowPage.getters.canvasNodeByName('Code').then(($codeNode) => {
|
||||
const cssLeft = parseInt($codeNode.css('left'));
|
||||
const cssTop = parseInt($codeNode.css('top'));
|
||||
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
||||
|
@ -63,8 +48,9 @@ describe('Undo/Redo', () => {
|
|||
// Last node should be added back to original position
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.css', 'left', '860px')
|
||||
.should('have.css', 'top', '220px');
|
||||
.should('have.css', 'left', cssLeft + 'px')
|
||||
.should('have.css', 'top', cssTop + 'px');
|
||||
});
|
||||
});
|
||||
|
||||
it('should undo/redo deleting node using context menu', () => {
|
||||
|
@ -118,8 +104,7 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
cy.get('body').type('{esc}');
|
||||
cy.get('body').type('{esc}');
|
||||
WorkflowPage.actions.selectAll();
|
||||
cy.get('body').type('{backspace}');
|
||||
WorkflowPage.actions.hitDeleteAllNodes();
|
||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
||||
|
@ -132,22 +117,30 @@ describe('Undo/Redo', () => {
|
|||
it('should undo/redo moving nodes', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => {
|
||||
const initialPosition = $node.position();
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.css', 'left', '740px')
|
||||
.should('have.css', 'top', '320px');
|
||||
|
||||
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => {
|
||||
const cssLeft = parseInt($node.css('left'));
|
||||
const cssTop = parseInt($node.css('top'));
|
||||
expect(cssLeft).to.be.greaterThan(initialPosition.left);
|
||||
expect(cssTop).to.be.greaterThan(initialPosition.top);
|
||||
});
|
||||
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.css', 'left', '640px')
|
||||
.should('have.css', 'top', '220px');
|
||||
.canvasNodeByName(CODE_NODE_NAME)
|
||||
.should('have.css', 'left', `${initialPosition.left}px`)
|
||||
.should('have.css', 'top', `${initialPosition.top}px`);
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.css', 'left', '740px')
|
||||
.should('have.css', 'top', '320px');
|
||||
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => {
|
||||
const cssLeft = parseInt($node.css('left'));
|
||||
const cssTop = parseInt($node.css('top'));
|
||||
expect(cssLeft).to.be.greaterThan(initialPosition.left);
|
||||
expect(cssTop).to.be.greaterThan(initialPosition.top);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should undo/redo deleting a connection using context menu', () => {
|
||||
|
@ -204,7 +197,7 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
cy.get('body').type('{esc}');
|
||||
cy.get('body').type('{esc}');
|
||||
WorkflowPage.actions.selectAll();
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
||||
WorkflowPage.actions.hitUndo();
|
||||
|
@ -213,21 +206,6 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should undo/redo renaming node using NDV', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
ndv.actions.rename(CODE_NODE_NEW_NAME);
|
||||
cy.get('body').type('{esc}');
|
||||
WorkflowPage.actions.hitUndo();
|
||||
cy.get('body').type('{esc}');
|
||||
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).should('exist');
|
||||
WorkflowPage.actions.hitRedo();
|
||||
cy.get('body').type('{esc}');
|
||||
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NEW_NAME).should('exist');
|
||||
});
|
||||
|
||||
it('should undo/redo renaming node using keyboard shortcut', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
@ -289,8 +267,12 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.first()
|
||||
.should('have.css', 'left', `${initialPosition.left + 120}px`)
|
||||
.should('have.css', 'top', `${initialPosition.top + 140}px`);
|
||||
.then(($node) => {
|
||||
const cssLeft = parseInt($node.css('left'));
|
||||
const cssTop = parseInt($node.css('top'));
|
||||
expect(cssLeft).to.be.greaterThan(initialPosition.left);
|
||||
expect(cssTop).to.be.greaterThan(initialPosition.top);
|
||||
});
|
||||
|
||||
// Delete the set node
|
||||
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click();
|
||||
|
@ -319,8 +301,12 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.first()
|
||||
.should('have.css', 'left', `${initialPosition.left + 120}px`)
|
||||
.should('have.css', 'top', `${initialPosition.top + 140}px`);
|
||||
.then(($node) => {
|
||||
const cssLeft = parseInt($node.css('left'));
|
||||
const cssTop = parseInt($node.css('top'));
|
||||
expect(cssLeft).to.be.greaterThan(initialPosition.left);
|
||||
expect(cssTop).to.be.greaterThan(initialPosition.top);
|
||||
});
|
||||
// Third redo: Should delete the Set node
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
|
@ -337,9 +323,6 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
|
||||
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch'))
|
||||
.should('have.css', 'left', `637px`)
|
||||
.should('have.css', 'top', `501px`);
|
||||
|
||||
cy.fixture('Test_workflow_form_switch.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
|
@ -352,9 +335,6 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
|
||||
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch'))
|
||||
.should('have.css', 'left', `637px`)
|
||||
.should('have.css', 'top', `501px`);
|
||||
});
|
||||
|
||||
it('should not undo/redo when NDV or a modal is open', () => {
|
||||
|
|
|
@ -8,45 +8,46 @@ describe('Inline expression editor', () => {
|
|||
beforeEach(() => {
|
||||
WorkflowPage.actions.visit();
|
||||
WorkflowPage.actions.addInitialNodeToCanvas('Schedule');
|
||||
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError');
|
||||
cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
|
||||
});
|
||||
|
||||
describe('Static data', () => {
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openInlineExpressionEditor();
|
||||
});
|
||||
|
||||
it('should resolve primitive resolvables', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('1 + 2');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^3$/);
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('"ab"');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{rightArrow}+');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('"cd"');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^abcd$/);
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('true && false');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^false$/);
|
||||
});
|
||||
|
||||
it('should resolve object resolvables', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.type('{ a: 1 }', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Object: \{"a": 1\}\]$/);
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.type('{ a: 1 }.a', { parseSpecialCharSequences: false });
|
||||
|
@ -55,13 +56,13 @@ describe('Inline expression editor', () => {
|
|||
|
||||
it('should resolve array resolvables', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('[0]');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/);
|
||||
|
@ -75,13 +76,14 @@ describe('Inline expression editor', () => {
|
|||
ndv.actions.close();
|
||||
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openInlineExpressionEditor();
|
||||
});
|
||||
|
||||
it('should resolve $parameter[]', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
// Resolving $parameter is slow, especially on CI runner
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'getAll');
|
||||
|
@ -90,19 +92,19 @@ describe('Inline expression editor', () => {
|
|||
it('should resolve input: $json,$input,$(nodeName)', () => {
|
||||
// Previous nodes have not run, input is empty
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.type("$('Schedule Trigger').item.json.myStr");
|
||||
|
@ -118,15 +120,15 @@ describe('Inline expression editor', () => {
|
|||
|
||||
// Previous nodes have run, input can be resolved
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.type("$('Schedule Trigger').item.json.myStr");
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { successToast } from '../pages/notifications';
|
||||
import {
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
|
@ -7,7 +9,6 @@ import {
|
|||
IF_NODE_NAME,
|
||||
HTTP_REQUEST_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
describe('Canvas Actions', () => {
|
||||
|
@ -124,6 +125,8 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 2);
|
||||
WorkflowPage.actions.addNodeBetweenNodes(
|
||||
CODE_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
|
@ -131,12 +134,15 @@ describe('Canvas Actions', () => {
|
|||
);
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
||||
// And last node should be pushed to the right
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.last()
|
||||
.should('have.css', 'left', '860px')
|
||||
.should('have.css', 'top', '220px');
|
||||
|
||||
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
|
||||
const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left'));
|
||||
|
||||
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
|
||||
const httpNodeLeft = parseFloat($httpNode.css('left'));
|
||||
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete connections by pressing the delete button', () => {
|
||||
|
@ -166,8 +172,8 @@ describe('Canvas Actions', () => {
|
|||
.findChildByTestId('execute-node-button')
|
||||
.click({ force: true });
|
||||
WorkflowPage.actions.executeNode(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.successToast().should('have.length', 2);
|
||||
WorkflowPage.getters.successToast().should('contain.text', 'Node executed successfully');
|
||||
successToast().should('have.length', 2);
|
||||
successToast().should('contain.text', 'Node executed successfully');
|
||||
});
|
||||
|
||||
it('should disable and enable node', () => {
|
||||
|
@ -198,19 +204,19 @@ describe('Canvas Actions', () => {
|
|||
it('should copy selected nodes', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.selectAll();
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
|
||||
WorkflowPage.actions.hitCopy();
|
||||
WorkflowPage.getters.successToast().should('contain', 'Copied!');
|
||||
successToast().should('contain', 'Copied!');
|
||||
|
||||
WorkflowPage.actions.copyNode(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.successToast().should('contain', 'Copied!');
|
||||
successToast().should('contain', 'Copied!');
|
||||
});
|
||||
|
||||
it('should select/deselect all nodes', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.selectAll();
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
||||
WorkflowPage.actions.deselectAll();
|
||||
WorkflowPage.getters.selectedNodes().should('have.length', 0);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { NDV, WorkflowExecutionsTab } from '../pages';
|
||||
import {
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
|
@ -7,8 +9,6 @@ import {
|
|||
SWITCH_NODE_NAME,
|
||||
MERGE_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { NDV, WorkflowExecutionsTab } from '../pages';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
const ExecutionsTab = new WorkflowExecutionsTab();
|
||||
|
@ -69,7 +69,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
|
||||
WorkflowPage.getters.nodeViewBackground().click(600 + i * 100, 200, { force: true });
|
||||
WorkflowPage.getters
|
||||
.nodeViewBackground()
|
||||
.click((i + 1) * 200, (i + 1) * 200, { force: true });
|
||||
}
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
|
||||
|
@ -164,8 +166,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
cy.wait(500);
|
||||
WorkflowPage.actions.selectAll();
|
||||
cy.get('body').type('{backspace}');
|
||||
WorkflowPage.actions.hitDeleteAllNodes();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 0);
|
||||
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
|
@ -181,8 +182,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
cy.wait(500);
|
||||
WorkflowPage.actions.selectAll();
|
||||
cy.get('body').type('{backspace}');
|
||||
WorkflowPage.actions.hitDeleteAllNodes();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 0);
|
||||
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
|
@ -199,13 +199,23 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.last()
|
||||
.should('have.css', 'left', '740px')
|
||||
.should('have.css', 'top', '320px');
|
||||
.then(($node) => {
|
||||
const { left, top } = $node.position();
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
||||
clickToFinish: true,
|
||||
});
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.last()
|
||||
.then(($node) => {
|
||||
const { left: newLeft, top: newTop } = $node.position();
|
||||
expect(newLeft).to.be.greaterThan(left);
|
||||
expect(newTop).to.be.greaterThan(top);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should zoom in', () => {
|
||||
|
@ -258,7 +268,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||
// Zoom in 1x + Zoom out 1x should reset to default (=1)
|
||||
WorkflowPage.getters.nodeView().should('have.css', 'transform', `matrix(1, 0, 0, 1, 0, 0)`);
|
||||
WorkflowPage.getters.nodeView().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)');
|
||||
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||
WorkflowPage.getters
|
||||
|
@ -315,7 +325,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
cy.get('body').type('{esc}');
|
||||
|
||||
// Keyboard shortcut
|
||||
WorkflowPage.actions.selectAll();
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
|
@ -324,12 +334,12 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
WorkflowPage.actions.selectAll();
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
||||
|
||||
// Context menu
|
||||
WorkflowPage.actions.selectAll();
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
WorkflowPage.actions.openContextMenu();
|
||||
WorkflowPage.actions.contextMenuAction('toggle_activation');
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 0);
|
||||
|
@ -341,7 +351,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.actions.openContextMenu();
|
||||
WorkflowPage.actions.contextMenuAction('toggle_activation');
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
WorkflowPage.actions.selectAll();
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
WorkflowPage.actions.openContextMenu();
|
||||
WorkflowPage.actions.contextMenuAction('toggle_activation');
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
||||
|
@ -364,6 +374,17 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.canvasNodeByName(RENAME_NODE_NAME2).should('exist');
|
||||
});
|
||||
|
||||
it('should not allow empty strings for node names', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').trigger('keydown', { key: 'F2' });
|
||||
cy.get('.rename-prompt').should('be.visible');
|
||||
cy.get('body').type('{backspace}');
|
||||
cy.get('body').type('{enter}');
|
||||
cy.get('.rename-prompt').should('contain', 'Invalid Name');
|
||||
});
|
||||
|
||||
it('should duplicate nodes (context menu or shortcut)', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
|
@ -372,8 +393,8 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
|
||||
WorkflowPage.actions.selectAll();
|
||||
WorkflowPage.actions.hitDuplicateNodeShortcut();
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
WorkflowPage.actions.hitDuplicateNode();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 5);
|
||||
});
|
||||
|
||||
|
|
|
@ -6,13 +6,19 @@ import {
|
|||
BACKEND_BASE_URL,
|
||||
} from '../constants';
|
||||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { errorToast } from '../pages/notifications';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('Data pinning', () => {
|
||||
const maxPinnedDataSize = 16384;
|
||||
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.window().then((win) => {
|
||||
win.maxPinnedDataSize = maxPinnedDataSize;
|
||||
});
|
||||
});
|
||||
|
||||
it('Should be able to pin node output', () => {
|
||||
|
@ -108,6 +114,8 @@ describe('Data pinning', () => {
|
|||
.parent()
|
||||
.should('have.class', 'is-disabled');
|
||||
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
// Unpin using context menu
|
||||
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||
ndv.actions.setPinnedData([{ test: 1 }]);
|
||||
|
@ -136,12 +144,21 @@ describe('Data pinning', () => {
|
|||
|
||||
ndv.actions.pastePinnedData([
|
||||
{
|
||||
test: '1'.repeat(Cypress.env('MAX_PINNED_DATA_SIZE')),
|
||||
test: '1'.repeat(maxPinnedDataSize),
|
||||
},
|
||||
]);
|
||||
workflowPage.getters
|
||||
.errorToast()
|
||||
.should('contain', 'Workflow has reached the maximum allowed pinned data size');
|
||||
errorToast().should('contain', 'Workflow has reached the maximum allowed pinned data size');
|
||||
});
|
||||
|
||||
it('Should show an error when pin data JSON in invalid', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
|
||||
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
|
||||
ndv.getters.container().should('be.visible');
|
||||
ndv.getters.pinDataButton().should('not.exist');
|
||||
ndv.getters.editPinnedDataButton().should('be.visible');
|
||||
|
||||
ndv.actions.setPinnedData('[ { "name": "First item", "code": 2dsa }]');
|
||||
errorToast().should('contain', 'Unable to save due to invalid JSON');
|
||||
});
|
||||
|
||||
it('Should be able to reference paired items in a node located before pinned data', () => {
|
||||
|
@ -155,6 +172,7 @@ describe('Data pinning', () => {
|
|||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
|
||||
|
||||
setExpressionOnStringValueInSet(`{{ $('${HTTP_REQUEST_NODE_NAME}').item`);
|
||||
|
||||
const output = '[Object: {"json": {"http": 123}, "pairedItem": {"item": 0}}]';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
import { successToast } from '../pages/notifications';
|
||||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
const ndv = new NDV();
|
||||
|
@ -10,13 +10,13 @@ describe('ADO-1338-ndv-missing-input-panel', () => {
|
|||
});
|
||||
|
||||
it('should show the input and output panels when node is missing input and output data', () => {
|
||||
cy.createFixtureWorkflow('Test_ado_1338.json', uuid());
|
||||
cy.createFixtureWorkflow('Test_ado_1338.json');
|
||||
|
||||
// Execute the workflow
|
||||
workflowPage.getters.zoomToFitButton().click();
|
||||
workflowPage.getters.executeWorkflowButton().click();
|
||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
||||
workflowPage.getters.successToast().should('be.visible');
|
||||
successToast().should('be.visible');
|
||||
|
||||
workflowPage.actions.openNode('Discourse1');
|
||||
ndv.getters.inputPanel().should('be.visible');
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const wf = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import {
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
@ -18,6 +18,8 @@ describe('Data mapping', () => {
|
|||
cy.fixture('Test_workflow-actions_paste-data.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
});
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.executePrevious();
|
||||
ndv.actions.switchInputMode('Table');
|
||||
|
@ -49,6 +51,7 @@ describe('Data mapping', () => {
|
|||
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
});
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.switchInputMode('Table');
|
||||
|
@ -73,6 +76,7 @@ describe('Data mapping', () => {
|
|||
ndv.actions.mapToParameter('value');
|
||||
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '0');
|
||||
|
||||
ndv.getters.inputTbodyCell(1, 0).realHover();
|
||||
|
@ -110,6 +114,7 @@ describe('Data mapping', () => {
|
|||
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
});
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.switchInputMode('JSON');
|
||||
|
@ -148,6 +153,7 @@ describe('Data mapping', () => {
|
|||
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
});
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.clearParameterInput('value');
|
||||
|
@ -169,20 +175,26 @@ describe('Data mapping', () => {
|
|||
});
|
||||
|
||||
it('maps expressions from previous nodes', () => {
|
||||
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`);
|
||||
cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow');
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.openNode('Set1');
|
||||
|
||||
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
|
||||
ndv.getters.inputDataContainer().find('span').contains('count').realMouseDown();
|
||||
ndv.actions.executePrevious();
|
||||
ndv.actions.expandSchemaViewNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
|
||||
const dataPill = ndv.getters
|
||||
.inputDataContainer()
|
||||
.findChildByTestId('run-data-schema-item')
|
||||
.contains('count')
|
||||
.should('be.visible');
|
||||
dataPill.realMouseDown();
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`);
|
||||
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
ndv.actions.mapDataFromHeader(1, 'value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
|
@ -193,7 +205,6 @@ describe('Data mapping', () => {
|
|||
|
||||
ndv.actions.selectInputNode('Set');
|
||||
|
||||
ndv.actions.executePrevious();
|
||||
ndv.getters.executingLoader().should('not.exist');
|
||||
ndv.getters.inputDataContainer().should('exist');
|
||||
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
|
||||
|
@ -249,6 +260,7 @@ describe('Data mapping', () => {
|
|||
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
});
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.openNode('Set');
|
||||
|
||||
|
@ -280,6 +292,7 @@ describe('Data mapping', () => {
|
|||
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
});
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.typeIntoParameterInput('value', 'test_value');
|
||||
|
@ -290,14 +303,8 @@ describe('Data mapping', () => {
|
|||
ndv.actions.executePrevious();
|
||||
ndv.getters.executingLoader().should('not.exist');
|
||||
ndv.getters.inputDataContainer().should('exist');
|
||||
ndv.getters
|
||||
.inputDataContainer()
|
||||
.should('exist')
|
||||
.find('span')
|
||||
.contains('test_name')
|
||||
.realMouseDown();
|
||||
ndv.actions.mapToParameter('value');
|
||||
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.mapDataFromHeader(1, 'value');
|
||||
ndv.actions.validateExpressionPreview('value', 'test_value');
|
||||
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
ndv.actions.validateExpressionPreview('value', 'test_value');
|
||||
|
@ -307,21 +314,15 @@ describe('Data mapping', () => {
|
|||
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
});
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.clearParameterInput('value');
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
ndv.getters.parameterInput('includeOtherFields').find('input[type="checkbox"]').should('exist');
|
||||
ndv.getters.parameterInput('includeOtherFields').find('input[type="text"]').should('not.exist');
|
||||
ndv.getters
|
||||
.inputDataContainer()
|
||||
.should('exist')
|
||||
.find('span')
|
||||
.contains('count')
|
||||
.realMouseDown()
|
||||
.realMouseMove(100, 100);
|
||||
cy.wait(50);
|
||||
const pill = ndv.getters.inputDataContainer().find('span').contains('count');
|
||||
pill.should('be.visible');
|
||||
pill.realMouseDown();
|
||||
pill.realMouseMove(100, 100);
|
||||
|
||||
ndv.getters
|
||||
.parameterInput('includeOtherFields')
|
||||
|
@ -332,13 +333,13 @@ describe('Data mapping', () => {
|
|||
.find('input[type="text"]')
|
||||
.should('exist')
|
||||
.invoke('css', 'border')
|
||||
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)'));
|
||||
.should('include', 'dashed rgb(90, 76, 194)');
|
||||
|
||||
ndv.getters
|
||||
.parameterInput('value')
|
||||
.find('input[type="text"]')
|
||||
.should('exist')
|
||||
.invoke('css', 'border')
|
||||
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)'));
|
||||
.should('include', 'dashed rgb(90, 76, 194)');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import { WorkflowPage, WorkflowsPage, NDV } from '../pages';
|
||||
import { BACKEND_BASE_URL } from '../constants';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { WorkflowPage, NDV } from '../pages';
|
||||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('Schedule Trigger node', async () => {
|
||||
describe('Schedule Trigger node', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
});
|
||||
|
@ -18,49 +15,4 @@ describe('Schedule Trigger node', async () => {
|
|||
ndv.getters.outputPanel().contains('timestamp');
|
||||
ndv.getters.backToCanvas().click();
|
||||
});
|
||||
|
||||
it('should execute once per second when activated', () => {
|
||||
workflowPage.actions.renameWorkflow('Schedule Trigger Workflow');
|
||||
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
|
||||
workflowPage.actions.openNode('Schedule Trigger');
|
||||
|
||||
cy.getByTestId('parameter-input-field').click();
|
||||
getVisibleSelect().find('.option-headline').contains('Seconds').click();
|
||||
cy.getByTestId('parameter-input-secondsInterval').clear().type('1');
|
||||
|
||||
ndv.getters.backToCanvas().click();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
workflowPage.actions.activateWorkflow();
|
||||
workflowPage.getters.activatorSwitch().should('have.class', 'is-checked');
|
||||
|
||||
cy.url().then((url) => {
|
||||
const workflowId = url.split('/').pop();
|
||||
|
||||
cy.wait(1200);
|
||||
cy.request('GET', `${BACKEND_BASE_URL}/rest/executions`).then((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(workflowId).to.not.be.undefined;
|
||||
expect(response.body.data.results.length).to.be.greaterThan(0);
|
||||
const matchingExecutions = response.body.data.results.filter(
|
||||
(execution: any) => execution.workflowId === workflowId,
|
||||
);
|
||||
expect(matchingExecutions).to.have.length(1);
|
||||
|
||||
cy.wait(1200);
|
||||
cy.request('GET', `${BACKEND_BASE_URL}/rest/executions`).then((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(response.body.data.results.length).to.be.greaterThan(0);
|
||||
const matchingExecutions = response.body.data.results.filter(
|
||||
(execution: any) => execution.workflowId === workflowId,
|
||||
);
|
||||
expect(matchingExecutions).to.have.length(2);
|
||||
|
||||
workflowPage.actions.activateWorkflow();
|
||||
workflowPage.getters.activatorSwitch().should('not.have.class', 'is-checked');
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.actions.deleteWorkFlow('Schedule Trigger Workflow');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { cowBase64 } from '../support/binaryTestFiles';
|
||||
import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
@ -75,34 +75,34 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
|||
}
|
||||
};
|
||||
|
||||
describe('Webhook Trigger node', async () => {
|
||||
describe('Webhook Trigger node', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
});
|
||||
|
||||
it('should listen for a GET request', () => {
|
||||
simpleWebhookCall({ method: 'GET', webhookPath: uuid(), executeNow: true });
|
||||
simpleWebhookCall({ method: 'GET', webhookPath: nanoid(), executeNow: true });
|
||||
});
|
||||
|
||||
it('should listen for a POST request', () => {
|
||||
simpleWebhookCall({ method: 'POST', webhookPath: uuid(), executeNow: true });
|
||||
simpleWebhookCall({ method: 'POST', webhookPath: nanoid(), executeNow: true });
|
||||
});
|
||||
|
||||
it('should listen for a DELETE request', () => {
|
||||
simpleWebhookCall({ method: 'DELETE', webhookPath: uuid(), executeNow: true });
|
||||
simpleWebhookCall({ method: 'DELETE', webhookPath: nanoid(), executeNow: true });
|
||||
});
|
||||
it('should listen for a HEAD request', () => {
|
||||
simpleWebhookCall({ method: 'HEAD', webhookPath: uuid(), executeNow: true });
|
||||
simpleWebhookCall({ method: 'HEAD', webhookPath: nanoid(), executeNow: true });
|
||||
});
|
||||
it('should listen for a PATCH request', () => {
|
||||
simpleWebhookCall({ method: 'PATCH', webhookPath: uuid(), executeNow: true });
|
||||
simpleWebhookCall({ method: 'PATCH', webhookPath: nanoid(), executeNow: true });
|
||||
});
|
||||
it('should listen for a PUT request', () => {
|
||||
simpleWebhookCall({ method: 'PUT', webhookPath: uuid(), executeNow: true });
|
||||
simpleWebhookCall({ method: 'PUT', webhookPath: nanoid(), executeNow: true });
|
||||
});
|
||||
|
||||
it('should listen for a GET request and respond with Respond to Webhook node', () => {
|
||||
const webhookPath = uuid();
|
||||
const webhookPath = nanoid();
|
||||
simpleWebhookCall({
|
||||
method: 'GET',
|
||||
webhookPath,
|
||||
|
@ -121,14 +121,16 @@ describe('Webhook Trigger node', async () => {
|
|||
workflowPage.actions.executeWorkflow();
|
||||
cy.wait(waitForWebhook);
|
||||
|
||||
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
||||
cy.request<{ MyValue: number }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
|
||||
(response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(response.body.MyValue).to.eq(1234);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should listen for a GET request and respond custom status code 201', () => {
|
||||
const webhookPath = uuid();
|
||||
const webhookPath = nanoid();
|
||||
simpleWebhookCall({
|
||||
method: 'GET',
|
||||
webhookPath,
|
||||
|
@ -145,7 +147,7 @@ describe('Webhook Trigger node', async () => {
|
|||
});
|
||||
|
||||
it('should listen for a GET request and respond with last node', () => {
|
||||
const webhookPath = uuid();
|
||||
const webhookPath = nanoid();
|
||||
simpleWebhookCall({
|
||||
method: 'GET',
|
||||
webhookPath,
|
||||
|
@ -161,14 +163,16 @@ describe('Webhook Trigger node', async () => {
|
|||
workflowPage.actions.executeWorkflow();
|
||||
cy.wait(waitForWebhook);
|
||||
|
||||
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
||||
cy.request<{ MyValue: number }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
|
||||
(response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(response.body.MyValue).to.eq(1234);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should listen for a GET request and respond with last node binary data', () => {
|
||||
const webhookPath = uuid();
|
||||
const webhookPath = nanoid();
|
||||
simpleWebhookCall({
|
||||
method: 'GET',
|
||||
webhookPath,
|
||||
|
@ -200,14 +204,16 @@ describe('Webhook Trigger node', async () => {
|
|||
workflowPage.actions.executeWorkflow();
|
||||
cy.wait(waitForWebhook);
|
||||
|
||||
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
||||
cy.request<{ data: unknown }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
|
||||
(response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(Object.keys(response.body).includes('data')).to.be.true;
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should listen for a GET request and respond with an empty body', () => {
|
||||
const webhookPath = uuid();
|
||||
const webhookPath = nanoid();
|
||||
simpleWebhookCall({
|
||||
method: 'GET',
|
||||
webhookPath,
|
||||
|
@ -217,14 +223,16 @@ describe('Webhook Trigger node', async () => {
|
|||
});
|
||||
ndv.actions.execute();
|
||||
cy.wait(waitForWebhook);
|
||||
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
||||
cy.request<{ MyValue: unknown }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(
|
||||
(response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(response.body.MyValue).to.be.undefined;
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should listen for a GET request with Basic Authentication', () => {
|
||||
const webhookPath = uuid();
|
||||
const webhookPath = nanoid();
|
||||
simpleWebhookCall({
|
||||
method: 'GET',
|
||||
webhookPath,
|
||||
|
@ -267,7 +275,7 @@ describe('Webhook Trigger node', async () => {
|
|||
});
|
||||
|
||||
it('should listen for a GET request with Header Authentication', () => {
|
||||
const webhookPath = uuid();
|
||||
const webhookPath = nanoid();
|
||||
simpleWebhookCall({
|
||||
method: 'GET',
|
||||
webhookPath,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../constants';
|
||||
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN, NOTION_NODE_NAME } from '../constants';
|
||||
import {
|
||||
CredentialsModal,
|
||||
CredentialsPage,
|
||||
|
@ -8,6 +8,7 @@ import {
|
|||
WorkflowsPage,
|
||||
} from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import * as projects from '../composables/projects';
|
||||
|
||||
/**
|
||||
* User U1 - Instance owner
|
||||
|
@ -30,11 +31,11 @@ const workflowSharingModal = new WorkflowSharingModal();
|
|||
const ndv = new NDV();
|
||||
|
||||
describe('Sharing', { disableAutoLogin: true }, () => {
|
||||
before(() => cy.enableFeature('sharing', true));
|
||||
before(() => cy.enableFeature('sharing'));
|
||||
|
||||
let workflowW2Url = '';
|
||||
it('should create C1, W1, W2, share W1 with U3, as U2', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[0]);
|
||||
cy.signinAsMember(0);
|
||||
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
|
@ -67,7 +68,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
it('should create C2, share C2 with U1 and U2, as U3', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[1]);
|
||||
cy.signinAsMember(1);
|
||||
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
|
@ -83,7 +84,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
it('should open W1, add node using C2 as U3', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[1]);
|
||||
cy.signinAsMember(1);
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCards().should('have.length', 1);
|
||||
|
@ -99,7 +100,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
it('should open W1, add node using C2 as U2', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[0]);
|
||||
cy.signinAsMember(0);
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCards().should('have.length', 2);
|
||||
|
@ -119,7 +120,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
it('should not have access to W2, as U3', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[1]);
|
||||
cy.signinAsMember(1);
|
||||
|
||||
cy.visit(workflowW2Url);
|
||||
cy.waitForLoad();
|
||||
|
@ -128,13 +129,17 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
it('should have access to W1, W2, as U1', () => {
|
||||
cy.signin(INSTANCE_OWNER);
|
||||
cy.signinAsOwner();
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCards().should('have.length', 2);
|
||||
workflowsPage.getters.workflowCard('Workflow W1').click();
|
||||
workflowPage.actions.openNode('Notion');
|
||||
ndv.getters.credentialInput().should('have.value', 'Credential C1').should('be.disabled');
|
||||
ndv.getters
|
||||
.credentialInput()
|
||||
.find('input')
|
||||
.should('have.value', 'Credential C1')
|
||||
.should('be.enabled');
|
||||
ndv.actions.close();
|
||||
|
||||
cy.waitForLoad();
|
||||
|
@ -144,7 +149,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
it('should automatically test C2 when opened by U2 sharee', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[0]);
|
||||
cy.signinAsMember(0);
|
||||
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.credentialCard('Credential C2').click();
|
||||
|
@ -152,7 +157,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
it('should work for admin role on credentials created by others (also can share it with themselves)', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[0]);
|
||||
cy.signinAsMember(0);
|
||||
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
|
@ -164,18 +169,18 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
credentialsModal.actions.close();
|
||||
|
||||
cy.signout();
|
||||
cy.signin(INSTANCE_ADMIN);
|
||||
cy.signinAsAdmin();
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.credentialCard('Credential C3').click();
|
||||
credentialsModal.getters.testSuccessTag().should('be.visible');
|
||||
cy.get('input').should('not.have.length');
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
cy.contains(
|
||||
'You can view this credential because you have permission to read and share',
|
||||
'Sharing a credential allows people to use it in their workflows. They cannot access credential details.',
|
||||
).should('be.visible');
|
||||
|
||||
credentialsModal.getters.usersSelect().click();
|
||||
cy.getByTestId('user-email')
|
||||
cy.getByTestId('project-sharing-info')
|
||||
.filter(':visible')
|
||||
.should('have.length', 3)
|
||||
.contains(INSTANCE_ADMIN.email)
|
||||
|
@ -188,3 +193,146 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
credentialsModal.actions.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Credential Usage in Cross Shared Workflows', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetDatabase();
|
||||
cy.enableFeature('advancedPermissions');
|
||||
cy.enableFeature('projectRole:admin');
|
||||
cy.enableFeature('projectRole:editor');
|
||||
cy.changeQuota('maxTeamProjects', -1);
|
||||
cy.reload();
|
||||
cy.signinAsOwner();
|
||||
cy.visit(credentialsPage.url);
|
||||
});
|
||||
|
||||
it('should only show credentials from the same team project', () => {
|
||||
cy.enableFeature('advancedPermissions');
|
||||
cy.enableFeature('projectRole:admin');
|
||||
cy.enableFeature('projectRole:editor');
|
||||
cy.changeQuota('maxTeamProjects', -1);
|
||||
|
||||
// Create a notion credential in the home project
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create a notion credential in one project
|
||||
projects.actions.createProject('Development');
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create a notion credential in another project
|
||||
projects.actions.createProject('Test');
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
// Create a workflow with a notion node in the same project
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the credential in this project (+ the 'Create new' option) should
|
||||
// be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should only show credentials in their personal project for members if the workflow was shared with them', () => {
|
||||
const workflowName = 'Test workflow';
|
||||
cy.enableFeature('sharing');
|
||||
cy.reload();
|
||||
|
||||
// Create a notion credential as the owner and a workflow that is shared
|
||||
// with member 0
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
//cy.visit(workflowsPage.url);
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.setWorkflowName(workflowName);
|
||||
workflowPage.actions.openShareModal();
|
||||
workflowSharingModal.actions.addUser(INSTANCE_MEMBERS[0].email);
|
||||
workflowSharingModal.actions.save();
|
||||
|
||||
// As the member, create a new notion credential
|
||||
cy.signinAsMember();
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCard(workflowName).click();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the own credential the shared one (+ the 'Create new' option)
|
||||
// should be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 2);
|
||||
});
|
||||
|
||||
it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
|
||||
const workflowName = 'Test workflow';
|
||||
cy.enableFeature('sharing');
|
||||
|
||||
// As member 1, create a new notion credential. This should not show up.
|
||||
cy.signinAsMember(1);
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// As admin, create a new notion credential. This should show up.
|
||||
cy.signinAsAdmin();
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// As member 0, create a new notion credential and a workflow and share it
|
||||
// with the global owner and the admin.
|
||||
cy.signinAsMember();
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.setWorkflowName(workflowName);
|
||||
workflowPage.actions.openShareModal();
|
||||
workflowSharingModal.actions.addUser(INSTANCE_OWNER.email);
|
||||
workflowSharingModal.actions.addUser(INSTANCE_ADMIN.email);
|
||||
workflowSharingModal.actions.save();
|
||||
|
||||
// As the global owner, create a new notion credential and open the shared
|
||||
// workflow
|
||||
cy.signinAsOwner();
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCard(workflowName).click();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the personal credentials of the workflow owner and the global owner
|
||||
// should show up.
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 4);
|
||||
});
|
||||
|
||||
it('should show all personal credentials if the global owner owns the workflow', () => {
|
||||
cy.enableFeature('sharing');
|
||||
|
||||
// As member 0, create a new notion credential.
|
||||
cy.signinAsMember();
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// As the global owner, create a workflow and add a notion node
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Show all personal credentials
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.have.length', 2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -51,34 +51,13 @@ describe('Workflow tags', () => {
|
|||
wf.getters.tagPills().should('have.length', 0); // none attached
|
||||
});
|
||||
|
||||
it('should update a tag via modal', () => {
|
||||
wf.actions.openTagManagerModal();
|
||||
|
||||
const [first] = TEST_TAGS;
|
||||
|
||||
cy.contains('Create a tag').click();
|
||||
cy.getByTestId('tags-table').find('input').type(first).type('{enter}');
|
||||
cy.getByTestId('tags-table').should('contain.text', first);
|
||||
cy.getByTestId('edit-tag-button').eq(-1).click({ force: true });
|
||||
cy.wait(300);
|
||||
cy.getByTestId('tags-table')
|
||||
.find('.el-input--large')
|
||||
.should('be.visible')
|
||||
.type(' Updated')
|
||||
.type('{enter}');
|
||||
cy.contains('Done').click();
|
||||
wf.getters.createTagButton().click();
|
||||
wf.getters.tagsInDropdown().should('have.length', 1); // one stored
|
||||
wf.getters.tagsInDropdown().contains('Updated').should('exist');
|
||||
wf.getters.tagPills().should('have.length', 0); // none attached
|
||||
});
|
||||
|
||||
it('should detach a tag inline by clicking on X on tag pill', () => {
|
||||
wf.getters.createTagButton().click();
|
||||
wf.actions.addTags(TEST_TAGS);
|
||||
wf.getters.nthTagPill(1).click();
|
||||
wf.getters.tagsDropdown().find('.el-tag__close').first().click();
|
||||
cy.get('body').click(0, 0);
|
||||
wf.getters.workflowTags().click();
|
||||
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
|
||||
});
|
||||
|
||||
|
@ -88,6 +67,7 @@ describe('Workflow tags', () => {
|
|||
wf.getters.nthTagPill(1).click();
|
||||
wf.getters.tagsInDropdown().filter('.selected').first().click();
|
||||
cy.get('body').click(0, 0);
|
||||
wf.getters.workflowTags().click();
|
||||
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../constants';
|
||||
import { MainSidebar, SettingsSidebar, SettingsUsersPage, WorkflowPage } from '../pages';
|
||||
import { MainSidebar, SettingsSidebar, SettingsUsersPage } from '../pages';
|
||||
import { PersonalSettingsPage } from '../pages/settings-personal';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
|
||||
/**
|
||||
* User A - Instance owner
|
||||
|
@ -24,7 +25,6 @@ const updatedPersonalData = {
|
|||
};
|
||||
|
||||
const usersSettingsPage = new SettingsUsersPage();
|
||||
const workflowPage = new WorkflowPage();
|
||||
const personalSettingsPage = new PersonalSettingsPage();
|
||||
const settingsSidebar = new SettingsSidebar();
|
||||
const mainSidebar = new MainSidebar();
|
||||
|
@ -34,6 +34,27 @@ describe('User Management', { disableAutoLogin: true }, () => {
|
|||
cy.enableFeature('sharing');
|
||||
});
|
||||
|
||||
it('should login and logout', () => {
|
||||
cy.visit('/');
|
||||
cy.get('input[name="email"]').type(INSTANCE_OWNER.email);
|
||||
cy.get('input[name="password"]').type(INSTANCE_OWNER.password);
|
||||
cy.getByTestId('form-submit-button').click();
|
||||
mainSidebar.getters.logo().should('be.visible');
|
||||
mainSidebar.actions.goToSettings();
|
||||
settingsSidebar.getters.users().should('be.visible');
|
||||
|
||||
mainSidebar.actions.closeSettings();
|
||||
mainSidebar.actions.openUserMenu();
|
||||
cy.getByTestId('user-menu-item-logout').click();
|
||||
|
||||
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
|
||||
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
|
||||
cy.getByTestId('form-submit-button').click();
|
||||
mainSidebar.getters.logo().should('be.visible');
|
||||
mainSidebar.actions.goToSettings();
|
||||
cy.getByTestId('menu-item').filter('#settings-users').should('not.exist');
|
||||
});
|
||||
|
||||
it('should prevent non-owners to access UM settings', () => {
|
||||
usersSettingsPage.actions.loginAndVisit(
|
||||
INSTANCE_MEMBERS[0].email,
|
||||
|
@ -153,7 +174,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
|
|||
usersSettingsPage.getters.deleteDataRadioButton().click();
|
||||
usersSettingsPage.getters.deleteDataInput().type('delete all data');
|
||||
usersSettingsPage.getters.deleteUserButton().click();
|
||||
workflowPage.getters.successToast().should('contain', 'User deleted');
|
||||
successToast().should('contain', 'User deleted');
|
||||
});
|
||||
|
||||
it('should delete user and transfer their data', () => {
|
||||
|
@ -163,10 +184,10 @@ describe('User Management', { disableAutoLogin: true }, () => {
|
|||
usersSettingsPage.getters.userSelectDropDown().click();
|
||||
usersSettingsPage.getters.userSelectOptions().first().click();
|
||||
usersSettingsPage.getters.deleteUserButton().click();
|
||||
workflowPage.getters.successToast().should('contain', 'User deleted');
|
||||
successToast().should('contain', 'User deleted');
|
||||
});
|
||||
|
||||
it(`should allow user to change their personal data`, () => {
|
||||
it('should allow user to change their personal data', () => {
|
||||
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
|
||||
personalSettingsPage.actions.updateFirstAndLastName(
|
||||
updatedPersonalData.newFirstName,
|
||||
|
@ -175,42 +196,39 @@ describe('User Management', { disableAutoLogin: true }, () => {
|
|||
personalSettingsPage.getters
|
||||
.currentUserName()
|
||||
.should('contain', `${updatedPersonalData.newFirstName} ${updatedPersonalData.newLastName}`);
|
||||
workflowPage.getters.successToast().should('contain', 'Personal details updated');
|
||||
successToast().should('contain', 'Personal details updated');
|
||||
});
|
||||
|
||||
it(`shouldn't allow user to set weak password`, () => {
|
||||
it("shouldn't allow user to set weak password", () => {
|
||||
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
|
||||
personalSettingsPage.getters.changePasswordLink().click();
|
||||
for (let weakPass of updatedPersonalData.invalidPasswords) {
|
||||
for (const weakPass of updatedPersonalData.invalidPasswords) {
|
||||
personalSettingsPage.actions.tryToSetWeakPassword(INSTANCE_OWNER.password, weakPass);
|
||||
}
|
||||
});
|
||||
|
||||
it(`shouldn't allow user to change password if old password is wrong`, () => {
|
||||
it("shouldn't allow user to change password if old password is wrong", () => {
|
||||
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
|
||||
personalSettingsPage.getters.changePasswordLink().click();
|
||||
personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword);
|
||||
workflowPage.getters
|
||||
.errorToast()
|
||||
.closest('div')
|
||||
.should('contain', 'Provided current password is incorrect.');
|
||||
errorToast().closest('div').should('contain', 'Provided current password is incorrect.');
|
||||
});
|
||||
|
||||
it(`should change current user password`, () => {
|
||||
it('should change current user password', () => {
|
||||
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
|
||||
personalSettingsPage.getters.changePasswordLink().click();
|
||||
personalSettingsPage.actions.updatePassword(
|
||||
INSTANCE_OWNER.password,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
workflowPage.getters.successToast().should('contain', 'Password updated');
|
||||
successToast().should('contain', 'Password updated');
|
||||
personalSettingsPage.actions.loginWithNewData(
|
||||
INSTANCE_OWNER.email,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
});
|
||||
|
||||
it(`shouldn't allow users to set invalid email`, () => {
|
||||
it("shouldn't allow users to set invalid email", () => {
|
||||
personalSettingsPage.actions.loginAndVisit(
|
||||
INSTANCE_OWNER.email,
|
||||
updatedPersonalData.newPassword,
|
||||
|
@ -221,13 +239,13 @@ describe('User Management', { disableAutoLogin: true }, () => {
|
|||
personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('.')[0]);
|
||||
});
|
||||
|
||||
it(`should change user email`, () => {
|
||||
it('should change user email', () => {
|
||||
personalSettingsPage.actions.loginAndVisit(
|
||||
INSTANCE_OWNER.email,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail);
|
||||
workflowPage.getters.successToast().should('contain', 'Personal details updated');
|
||||
successToast().should('contain', 'Personal details updated');
|
||||
personalSettingsPage.actions.loginWithNewData(
|
||||
updatedPersonalData.newEmail,
|
||||
updatedPersonalData.newPassword,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||
import { clearNotifications, errorToast, successToast } from '../pages/notifications';
|
||||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
const executionsTab = new WorkflowExecutionsTab();
|
||||
|
@ -12,7 +12,7 @@ describe('Execution', () => {
|
|||
});
|
||||
|
||||
it('should test manual workflow', () => {
|
||||
cy.createFixtureWorkflow('Manual_wait_set.json', `Manual wait set ${uuid()}`);
|
||||
cy.createFixtureWorkflow('Manual_wait_set.json');
|
||||
|
||||
// Check workflow buttons
|
||||
workflowPage.getters.executeWorkflowButton().should('be.visible');
|
||||
|
@ -62,17 +62,17 @@ describe('Execution', () => {
|
|||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
||||
// Clear execution data
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
workflowPage.getters.clearExecutionDataButton().click();
|
||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||
|
||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
||||
workflowPage.getters.successToast().should('be.visible');
|
||||
});
|
||||
|
||||
it('should test manual workflow stop', () => {
|
||||
cy.createFixtureWorkflow('Manual_wait_set.json', `Manual wait set ${uuid()}`);
|
||||
cy.createFixtureWorkflow('Manual_wait_set.json');
|
||||
|
||||
// Check workflow buttons
|
||||
workflowPage.getters.executeWorkflowButton().should('be.visible');
|
||||
|
@ -106,6 +106,9 @@ describe('Execution', () => {
|
|||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
||||
workflowPage.getters.stopExecutionButton().should('exist');
|
||||
workflowPage.getters.stopExecutionButton().click();
|
||||
|
||||
|
@ -121,17 +124,17 @@ describe('Execution', () => {
|
|||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
||||
// Clear execution data
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
workflowPage.getters.clearExecutionDataButton().click();
|
||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||
|
||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
||||
workflowPage.getters.successToast().should('be.visible');
|
||||
});
|
||||
|
||||
it('should test webhook workflow', () => {
|
||||
cy.createFixtureWorkflow('Webhook_wait_set.json', `Webhook wait set ${uuid()}`);
|
||||
cy.createFixtureWorkflow('Webhook_wait_set.json');
|
||||
|
||||
// Check workflow buttons
|
||||
workflowPage.getters.executeWorkflowButton().should('be.visible');
|
||||
|
@ -194,17 +197,17 @@ describe('Execution', () => {
|
|||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
||||
// Clear execution data
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
workflowPage.getters.clearExecutionDataButton().click();
|
||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||
|
||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
||||
workflowPage.getters.successToast().should('be.visible');
|
||||
});
|
||||
|
||||
it('should test webhook workflow stop', () => {
|
||||
cy.createFixtureWorkflow('Webhook_wait_set.json', `Webhook wait set ${uuid()}`);
|
||||
cy.createFixtureWorkflow('Webhook_wait_set.json');
|
||||
|
||||
// Check workflow buttons
|
||||
workflowPage.getters.executeWorkflowButton().should('be.visible');
|
||||
|
@ -239,6 +242,9 @@ describe('Execution', () => {
|
|||
});
|
||||
});
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
||||
workflowPage.getters.stopExecutionButton().click();
|
||||
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
||||
workflowPage.getters
|
||||
|
@ -268,13 +274,13 @@ describe('Execution', () => {
|
|||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
||||
// Clear execution data
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
workflowPage.getters.clearExecutionDataButton().click();
|
||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||
|
||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
||||
workflowPage.getters.successToast().should('be.visible');
|
||||
});
|
||||
|
||||
describe('execution preview', () => {
|
||||
|
@ -286,13 +292,13 @@ describe('Execution', () => {
|
|||
executionsTab.actions.deleteExecutionInPreview();
|
||||
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 0);
|
||||
workflowPage.getters.successToast().contains('Execution deleted');
|
||||
successToast().contains('Execution deleted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('connections should be colored differently for pinned data', () => {
|
||||
beforeEach(() => {
|
||||
cy.createFixtureWorkflow('Schedule_pinned.json', `Schedule pinned ${uuid()}`);
|
||||
cy.createFixtureWorkflow('Schedule_pinned.json');
|
||||
workflowPage.actions.deselectAll();
|
||||
workflowPage.getters.zoomToFitButton().click();
|
||||
|
||||
|
@ -491,17 +497,14 @@ describe('Execution', () => {
|
|||
});
|
||||
|
||||
it('should send proper payload for node rerun', () => {
|
||||
cy.createFixtureWorkflow(
|
||||
'Multiple_trigger_node_rerun.json',
|
||||
`Multiple trigger node rerun ${uuid()}`,
|
||||
);
|
||||
cy.createFixtureWorkflow('Multiple_trigger_node_rerun.json', 'Multiple trigger node rerun');
|
||||
|
||||
workflowPage.getters.zoomToFitButton().click();
|
||||
workflowPage.getters.executeWorkflowButton().click();
|
||||
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
||||
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('do something with them')
|
||||
|
@ -510,22 +513,20 @@ describe('Execution', () => {
|
|||
|
||||
cy.wait('@workflowRun').then((interception) => {
|
||||
expect(interception.request.body).to.have.property('runData').that.is.an('object');
|
||||
const expectedKeys = ['When clicking "Test workflow"', 'fetch 5 random users'];
|
||||
const expectedKeys = ['When clicking ‘Test workflow’', 'fetch 5 random users'];
|
||||
|
||||
expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(expectedKeys.length);
|
||||
expect(interception.request.body.runData).to.include.all.keys(expectedKeys);
|
||||
const { runData } = interception.request.body as Record<string, object>;
|
||||
expect(Object.keys(runData)).to.have.lengthOf(expectedKeys.length);
|
||||
expect(runData).to.include.all.keys(expectedKeys);
|
||||
});
|
||||
});
|
||||
|
||||
it('should send proper payload for manual node run', () => {
|
||||
cy.createFixtureWorkflow(
|
||||
'Check_manual_node_run_for_pinned_and_rundata.json',
|
||||
`Check manual node run for pinned and rundata ${uuid()}`,
|
||||
);
|
||||
cy.createFixtureWorkflow('Check_manual_node_run_for_pinned_and_rundata.json');
|
||||
|
||||
workflowPage.getters.zoomToFitButton().click();
|
||||
|
||||
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('If')
|
||||
|
@ -534,18 +535,20 @@ describe('Execution', () => {
|
|||
|
||||
cy.wait('@workflowRun').then((interception) => {
|
||||
expect(interception.request.body).not.to.have.property('runData').that.is.an('object');
|
||||
expect(interception.request.body).to.have.property('pinData').that.is.an('object');
|
||||
expect(interception.request.body).to.have.property('workflowData').that.is.an('object');
|
||||
expect(interception.request.body.workflowData)
|
||||
.to.have.property('pinData')
|
||||
.that.is.an('object');
|
||||
const expectedPinnedDataKeys = ['Webhook'];
|
||||
|
||||
expect(Object.keys(interception.request.body.pinData)).to.have.lengthOf(
|
||||
expectedPinnedDataKeys.length,
|
||||
);
|
||||
expect(interception.request.body.pinData).to.include.all.keys(expectedPinnedDataKeys);
|
||||
const { pinData } = interception.request.body.workflowData as Record<string, object>;
|
||||
expect(Object.keys(pinData)).to.have.lengthOf(expectedPinnedDataKeys.length);
|
||||
expect(pinData).to.include.all.keys(expectedPinnedDataKeys);
|
||||
});
|
||||
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
||||
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('NoOp2')
|
||||
|
@ -554,29 +557,27 @@ describe('Execution', () => {
|
|||
|
||||
cy.wait('@workflowRun').then((interception) => {
|
||||
expect(interception.request.body).to.have.property('runData').that.is.an('object');
|
||||
expect(interception.request.body).to.have.property('pinData').that.is.an('object');
|
||||
expect(interception.request.body).to.have.property('workflowData').that.is.an('object');
|
||||
expect(interception.request.body.workflowData)
|
||||
.to.have.property('pinData')
|
||||
.that.is.an('object');
|
||||
const expectedPinnedDataKeys = ['Webhook'];
|
||||
const expectedRunDataKeys = ['If', 'Webhook'];
|
||||
|
||||
expect(Object.keys(interception.request.body.pinData)).to.have.lengthOf(
|
||||
expectedPinnedDataKeys.length,
|
||||
);
|
||||
expect(interception.request.body.pinData).to.include.all.keys(expectedPinnedDataKeys);
|
||||
const { pinData } = interception.request.body.workflowData as Record<string, object>;
|
||||
expect(Object.keys(pinData)).to.have.lengthOf(expectedPinnedDataKeys.length);
|
||||
expect(pinData).to.include.all.keys(expectedPinnedDataKeys);
|
||||
|
||||
expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(
|
||||
expectedRunDataKeys.length,
|
||||
);
|
||||
expect(interception.request.body.runData).to.include.all.keys(expectedRunDataKeys);
|
||||
const { runData } = interception.request.body as Record<string, object>;
|
||||
expect(Object.keys(runData)).to.have.lengthOf(expectedRunDataKeys.length);
|
||||
expect(runData).to.include.all.keys(expectedRunDataKeys);
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully execute partial executions with nodes attached to the second output', () => {
|
||||
cy.createFixtureWorkflow(
|
||||
'Test_Workflow_pairedItem_incomplete_manual_bug.json',
|
||||
'My test workflow',
|
||||
);
|
||||
cy.createFixtureWorkflow('Test_Workflow_pairedItem_incomplete_manual_bug.json');
|
||||
|
||||
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
|
||||
|
||||
workflowPage.getters.zoomToFitButton().click();
|
||||
workflowPage.getters.executeWorkflowButton().click();
|
||||
|
@ -590,16 +591,13 @@ describe('Execution', () => {
|
|||
cy.wait('@workflowRun');
|
||||
// Wait again for the websocket message to arrive and the UI to update.
|
||||
cy.wait(100);
|
||||
workflowPage.getters.errorToast({ timeout: 1 }).should('not.exist');
|
||||
errorToast({ timeout: 1 }).should('not.exist');
|
||||
});
|
||||
|
||||
it('should execute workflow partially up to the node that has issues', () => {
|
||||
cy.createFixtureWorkflow(
|
||||
'Test_workflow_partial_execution_with_missing_credentials.json',
|
||||
'My test workflow',
|
||||
);
|
||||
cy.createFixtureWorkflow('Test_workflow_partial_execution_with_missing_credentials.json');
|
||||
|
||||
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
|
||||
|
||||
workflowPage.getters.zoomToFitButton().click();
|
||||
workflowPage.getters.executeWorkflowButton().click();
|
||||
|
@ -617,6 +615,6 @@ describe('Execution', () => {
|
|||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
|
||||
workflowPage.getters.errorToast().should('contain', `Problem in node ‘Telegram‘`);
|
||||
errorToast().should('contain', 'Problem in node ‘Telegram‘');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { type ICredentialType } from 'n8n-workflow';
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
AI_TOOL_HTTP_NODE_NAME,
|
||||
GMAIL_NODE_NAME,
|
||||
HTTP_REQUEST_NODE_NAME,
|
||||
NEW_GOOGLE_ACCOUNT_NAME,
|
||||
|
@ -11,6 +14,7 @@ import {
|
|||
TRELLO_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
|
||||
import { successToast } from '../pages/notifications';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const credentialsPage = new CredentialsPage();
|
||||
|
@ -19,6 +23,7 @@ const workflowPage = new WorkflowPage();
|
|||
const nodeDetailsView = new NDV();
|
||||
|
||||
const NEW_CREDENTIAL_NAME = 'Something else';
|
||||
const NEW_CREDENTIAL_NAME2 = 'Something else entirely';
|
||||
|
||||
describe('Credentials', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -42,39 +47,6 @@ describe('Credentials', () => {
|
|||
credentialsPage.getters.credentialCards().should('have.length', 1);
|
||||
});
|
||||
|
||||
it.skip('should create a new credential using Add Credential button', () => {
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
|
||||
credentialsModal.getters.newCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeOption('Airtable API').click();
|
||||
|
||||
credentialsModal.getters.newCredentialTypeButton().click();
|
||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.connectionParameter('API Key').type('1234567890');
|
||||
|
||||
credentialsModal.actions.setName('Airtable Account');
|
||||
credentialsModal.actions.save();
|
||||
credentialsModal.actions.close();
|
||||
|
||||
credentialsPage.getters.credentialCards().should('have.length', 2);
|
||||
});
|
||||
|
||||
it.skip('should search credentials', () => {
|
||||
// Search by name
|
||||
credentialsPage.actions.search('Notion');
|
||||
credentialsPage.getters.credentialCards().should('have.length', 1);
|
||||
|
||||
// Search by Credential type
|
||||
credentialsPage.actions.search('Airtable API');
|
||||
credentialsPage.getters.credentialCards().should('have.length', 1);
|
||||
|
||||
// No results
|
||||
credentialsPage.actions.search('Google');
|
||||
credentialsPage.getters.credentialCards().should('have.length', 0);
|
||||
credentialsPage.getters.emptyList().should('be.visible');
|
||||
});
|
||||
|
||||
it('should sort credentials', () => {
|
||||
credentialsPage.actions.search('');
|
||||
credentialsPage.actions.sortBy('nameDesc');
|
||||
|
@ -185,7 +157,7 @@ describe('Credentials', () => {
|
|||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.deleteButton().click();
|
||||
cy.get('.el-message-box').find('button').contains('Yes').click();
|
||||
workflowPage.getters.successToast().contains('Credential deleted');
|
||||
successToast().contains('Credential deleted');
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
|
@ -211,6 +183,49 @@ describe('Credentials', () => {
|
|||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_CREDENTIAL_NAME);
|
||||
|
||||
// Reload page to make sure this also works when the credential hasn't been
|
||||
// just created.
|
||||
nodeDetailsView.actions.close();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.reload();
|
||||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.name().click();
|
||||
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME2);
|
||||
credentialsModal.getters.saveButton().click();
|
||||
credentialsModal.getters.closeButton().click();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_CREDENTIAL_NAME2);
|
||||
});
|
||||
|
||||
it('should edit credential for non-standard credential type', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(AGENT_NODE_NAME);
|
||||
workflowPage.actions.addNodeToCanvas(AI_TOOL_HTTP_NODE_NAME);
|
||||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
cy.getByTestId('parameter-input-authentication').click();
|
||||
cy.contains('Predefined Credential Type').click();
|
||||
cy.getByTestId('credential-select').click();
|
||||
cy.contains('Adalo API').click();
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.name().click();
|
||||
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME);
|
||||
credentialsModal.getters.saveButton().click();
|
||||
credentialsModal.getters.closeButton().click();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_CREDENTIAL_NAME);
|
||||
});
|
||||
|
||||
it('should setup generic authentication for HTTP node', () => {
|
||||
|
@ -242,7 +257,7 @@ describe('Credentials', () => {
|
|||
req.headers['cache-control'] = 'no-cache, no-store';
|
||||
|
||||
req.on('response', (res) => {
|
||||
const credentials = res.body || [];
|
||||
const credentials: ICredentialType[] = res.body || [];
|
||||
|
||||
const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api');
|
||||
|
||||
|
@ -254,8 +269,9 @@ describe('Credentials', () => {
|
|||
});
|
||||
|
||||
workflowPage.actions.visit(true);
|
||||
workflowPage.actions.addNodeToCanvas('Slack');
|
||||
workflowPage.actions.openNode('Slack');
|
||||
workflowPage.actions.addNodeToCanvas('Manual');
|
||||
workflowPage.actions.addNodeToCanvas('Slack', true, true, 'Get a channel');
|
||||
workflowPage.getters.nodeCredentialsSelect().should('exist');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import type { RouteHandler } from 'cypress/types/net-stubbing';
|
||||
import { WorkflowPage } from '../pages';
|
||||
import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
|
||||
import type { RouteHandler } from 'cypress/types/net-stubbing';
|
||||
import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const executionsTab = new WorkflowExecutionsTab();
|
||||
|
@ -10,7 +12,7 @@ const executionsRefreshInterval = 4000;
|
|||
describe('Current Workflow Executions', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`);
|
||||
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
|
||||
});
|
||||
|
||||
it('should render executions tab correctly', () => {
|
||||
|
@ -57,8 +59,8 @@ describe('Current Workflow Executions', () => {
|
|||
});
|
||||
|
||||
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
|
||||
const throttleResponse: RouteHandler = (req) => {
|
||||
return new Promise((resolve) => {
|
||||
const throttleResponse: RouteHandler = async (req) => {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(req.continue()), 2000);
|
||||
});
|
||||
};
|
||||
|
@ -71,6 +73,160 @@ describe('Current Workflow Executions', () => {
|
|||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
});
|
||||
|
||||
it('should error toast when server error message returned without stack trace', () => {
|
||||
executionsTab.actions.createManualExecutions(1);
|
||||
const message = 'Workflow did not finish, possible out-of-memory issue';
|
||||
cy.intercept('GET', '/rest/executions/*', {
|
||||
statusCode: 200,
|
||||
body: executionOutOfMemoryServerResponse,
|
||||
}).as('getExecution');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(['@getExecution']);
|
||||
|
||||
executionsTab.getters
|
||||
.workflowExecutionPreviewIframe()
|
||||
.should('be.visible')
|
||||
.its('0.contentDocument.body') // Access the body of the iframe document
|
||||
.should('not.be.empty') // Ensure the body is not empty
|
||||
|
||||
.then(cy.wrap)
|
||||
.find('.el-notification:has(.el-notification--error)')
|
||||
.should('be.visible')
|
||||
.filter(`:contains("${message}")`)
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('should show workflow data in executions tab after hard reload and modify name and tags', () => {
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
|
||||
workflowPage.getters.workflowTags().click();
|
||||
getVisibleSelect().find('li:contains("Manage tags")').click();
|
||||
cy.get('button:contains("Add new")').click();
|
||||
cy.getByTestId('tags-table').find('input').type('nutag').type('{enter}');
|
||||
cy.get('button:contains("Done")').click();
|
||||
|
||||
cy.reload();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.workflowTags().click();
|
||||
workflowPage.getters.tagsInDropdown().first().should('have.text', 'nutag').click();
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
|
||||
let newWorkflowName = 'Renamed workflow';
|
||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
newWorkflowName = 'New workflow';
|
||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
workflowPage.getters.workflowTags().click();
|
||||
workflowPage.getters.tagsDropdown().find('.el-tag__close').first().click();
|
||||
cy.get('body').click(0, 0);
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
});
|
||||
|
||||
it('should load items and auto scroll after filter change', () => {
|
||||
createMockExecutions();
|
||||
createMockExecutions();
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
|
||||
cy.wait(['@getExecutions']);
|
||||
|
||||
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
||||
|
||||
executionsTab.getters.executionListItems().eq(10).click();
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
||||
getVisibleSelect().find('li:contains("Error")').click();
|
||||
|
||||
executionsTab.getters.executionListItems().should('have.length', 5);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 4);
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
||||
getVisibleSelect().find('li:contains("Success")').click();
|
||||
|
||||
// check if the list is scrolled
|
||||
executionsTab.getters.executionListItems().eq(10).should('be.visible');
|
||||
executionsTab.getters.executionsList().then(($el) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = $el[0];
|
||||
expect(scrollTop).to.be.greaterThan(0);
|
||||
expect(scrollTop + clientHeight).to.be.lessThan(scrollHeight);
|
||||
|
||||
// scroll to the bottom
|
||||
$el[0].scrollTo(0, scrollHeight);
|
||||
executionsTab.getters.executionListItems().should('have.length', 18);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 18);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 0);
|
||||
});
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
|
||||
executionsTab.getters.executionListItems().eq(11).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
const createMockExecutions = () => {
|
||||
|
@ -82,3 +238,10 @@ const createMockExecutions = () => {
|
|||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
executionsTab.actions.createManualExecutions(4);
|
||||
};
|
||||
|
||||
const checkMainHeaderELements = () => {
|
||||
workflowPage.getters.workflowNameInputContainer().should('be.visible');
|
||||
workflowPage.getters.workflowTagsContainer().should('be.visible');
|
||||
workflowPage.getters.workflowMenu().should('be.visible');
|
||||
workflowPage.getters.saveButton().should('be.visible');
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { ICredentialType } from 'n8n-workflow';
|
||||
import { NodeCreator } from '../pages/features/node-creator';
|
||||
import CustomNodeFixture from '../fixtures/Custom_node.json';
|
||||
import { CredentialsModal, WorkflowPage } from '../pages';
|
||||
|
@ -5,6 +6,13 @@ import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_cred
|
|||
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
|
||||
import CustomCredential from '../fixtures/Custom_credential.json';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import {
|
||||
confirmCommunityNodeUninstall,
|
||||
confirmCommunityNodeUpdate,
|
||||
getCommunityCards,
|
||||
installFirstCommunityNode,
|
||||
visitCommunityNodesSettings,
|
||||
} from '../pages/settings-community-nodes';
|
||||
|
||||
const credentialsModal = new CredentialsModal();
|
||||
const nodeCreatorFeature = new NodeCreator();
|
||||
|
@ -13,7 +21,7 @@ const workflowPage = new WorkflowPage();
|
|||
// We separate-out the custom nodes because they require injecting nodes and credentials
|
||||
// so the /nodes and /credentials endpoints are intercepted and non-cached.
|
||||
// We want to keep the other tests as fast as possible so we don't want to break the cache in those.
|
||||
describe('Community Nodes', () => {
|
||||
describe('Community and custom nodes in canvas', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/types/nodes.json', { middleware: true }, (req) => {
|
||||
req.headers['cache-control'] = 'no-cache, no-store';
|
||||
|
@ -33,9 +41,9 @@ describe('Community Nodes', () => {
|
|||
req.headers['cache-control'] = 'no-cache, no-store';
|
||||
|
||||
req.on('response', (res) => {
|
||||
const credentials = res.body || [];
|
||||
const credentials: ICredentialType[] = res.body || [];
|
||||
|
||||
credentials.push(CustomCredential);
|
||||
credentials.push(CustomCredential as ICredentialType);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -94,3 +102,89 @@ describe('Community Nodes', () => {
|
|||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Community nodes', () => {
|
||||
const mockPackage = {
|
||||
createdAt: '2024-07-22T19:08:06.505Z',
|
||||
updatedAt: '2024-07-22T19:08:06.505Z',
|
||||
packageName: 'n8n-nodes-chatwork',
|
||||
installedVersion: '1.0.0',
|
||||
authorName: null,
|
||||
authorEmail: null,
|
||||
installedNodes: [
|
||||
{
|
||||
name: 'Chatwork',
|
||||
type: 'n8n-nodes-chatwork.chatwork',
|
||||
latestVersion: 1,
|
||||
},
|
||||
],
|
||||
updateAvailable: '1.1.2',
|
||||
};
|
||||
|
||||
it('can install, update and uninstall community nodes', () => {
|
||||
cy.intercept(
|
||||
{
|
||||
hostname: 'api.npms.io',
|
||||
pathname: '/v2/search',
|
||||
query: { q: 'keywords:n8n-community-node-package' },
|
||||
},
|
||||
{ body: {} },
|
||||
);
|
||||
cy.intercept(
|
||||
{ method: 'GET', pathname: '/rest/community-packages', times: 1 },
|
||||
{
|
||||
body: { data: [] },
|
||||
},
|
||||
).as('getEmptyPackages');
|
||||
visitCommunityNodesSettings();
|
||||
cy.wait('@getEmptyPackages');
|
||||
|
||||
// install a package
|
||||
cy.intercept(
|
||||
{ method: 'POST', pathname: '/rest/community-packages', times: 1 },
|
||||
{
|
||||
body: { data: mockPackage },
|
||||
},
|
||||
).as('installPackage');
|
||||
cy.intercept(
|
||||
{ method: 'GET', pathname: '/rest/community-packages', times: 1 },
|
||||
{
|
||||
body: { data: [mockPackage] },
|
||||
},
|
||||
).as('getPackages');
|
||||
installFirstCommunityNode('n8n-nodes-chatwork@1.0.0');
|
||||
cy.wait('@installPackage');
|
||||
cy.wait('@getPackages');
|
||||
getCommunityCards().should('have.length', 1);
|
||||
getCommunityCards().eq(0).should('include.text', 'v1.0.0');
|
||||
|
||||
// update the package
|
||||
cy.intercept(
|
||||
{ method: 'PATCH', pathname: '/rest/community-packages' },
|
||||
{
|
||||
body: { data: { ...mockPackage, installedVersion: '1.2.0', updateAvailable: undefined } },
|
||||
},
|
||||
).as('updatePackage');
|
||||
getCommunityCards().eq(0).find('button').click();
|
||||
confirmCommunityNodeUpdate();
|
||||
cy.wait('@updatePackage');
|
||||
getCommunityCards().should('have.length', 1);
|
||||
getCommunityCards().eq(0).should('not.include.text', 'v1.0.0');
|
||||
|
||||
// uninstall the package
|
||||
cy.intercept(
|
||||
{
|
||||
method: 'DELETE',
|
||||
pathname: '/rest/community-packages',
|
||||
query: { name: 'n8n-nodes-chatwork' },
|
||||
},
|
||||
{ statusCode: 204 },
|
||||
).as('uninstallPackage');
|
||||
getCommunityCards().getByTestId('action-toggle').click();
|
||||
cy.getByTestId('action-uninstall').click();
|
||||
confirmCommunityNodeUninstall();
|
||||
cy.wait('@uninstallPackage');
|
||||
|
||||
cy.getByTestId('action-box').should('exist');
|
||||
});
|
||||
});
|
||||
|
|
38
cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts
Normal file
38
cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts
Normal 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');
|
||||
});
|
||||
});
|
135
cypress/e2e/2111-ado-support-pinned-data-in-expressions.cy.ts
Normal file
135
cypress/e2e/2111-ado-support-pinned-data-in-expressions.cy.ts
Normal 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]',
|
||||
);
|
||||
});
|
||||
});
|
34
cypress/e2e/2230-ADO-ndv-reset-data-pagination.cy.ts
Normal file
34
cypress/e2e/2230-ADO-ndv-reset-data-pagination.cy.ts
Normal 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');
|
||||
});
|
||||
});
|
|
@ -4,7 +4,7 @@ const variablesPage = new VariablesPage();
|
|||
|
||||
describe('Variables', () => {
|
||||
it('should show the unlicensed action box when the feature is disabled', () => {
|
||||
cy.disableFeature('variables', false);
|
||||
cy.disableFeature('variables');
|
||||
cy.visit(variablesPage.url);
|
||||
|
||||
variablesPage.getters.unavailableResourcesList().should('be.visible');
|
||||
|
@ -18,14 +18,15 @@ describe('Variables', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', '/rest/variables').as('loadVariables');
|
||||
cy.intercept('GET', '/rest/login').as('login');
|
||||
|
||||
cy.visit(variablesPage.url);
|
||||
cy.wait(['@loadVariables', '@loadSettings']);
|
||||
cy.wait(['@loadVariables', '@loadSettings', '@login']);
|
||||
});
|
||||
|
||||
it('should show the licensed action box when the feature is enabled', () => {
|
||||
variablesPage.getters.emptyResourcesList().should('be.visible');
|
||||
variablesPage.getters.createVariableButton().should('be.visible');
|
||||
variablesPage.getters.emptyResourcesListNewVariableButton().should('be.visible');
|
||||
});
|
||||
|
||||
it('should create a new variable using empty state row', () => {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
@ -7,7 +6,7 @@ const ndv = new NDV();
|
|||
describe('NDV', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.renameWorkflow(uuid());
|
||||
workflowPage.actions.renameWithUniqueName();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
});
|
||||
|
||||
|
@ -113,6 +112,9 @@ describe('NDV', () => {
|
|||
workflowPage.actions.executeWorkflow();
|
||||
workflowPage.actions.openNode('Set3');
|
||||
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.switchOutputMode('Table');
|
||||
|
||||
ndv.getters
|
||||
.inputRunSelector()
|
||||
.should('exist')
|
||||
|
@ -124,9 +126,6 @@ describe('NDV', () => {
|
|||
.find('input')
|
||||
.should('include.value', '2 of 2 (6 items)');
|
||||
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.switchOutputMode('Table');
|
||||
|
||||
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
|
||||
ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
|
||||
|
@ -163,64 +162,6 @@ describe('NDV', () => {
|
|||
ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||
});
|
||||
|
||||
it('resolves expression with default item when input node is not parent, while still pairing items', () => {
|
||||
cy.fixture('Test_workflow_5.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
});
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.executeWorkflow();
|
||||
workflowPage.actions.openNode('Set2');
|
||||
|
||||
ndv.getters.inputPanel().contains('6 items').should('exist');
|
||||
ndv.getters
|
||||
.outputRunSelector()
|
||||
.find('input')
|
||||
.should('exist')
|
||||
.should('have.value', '2 of 2 (6 items)');
|
||||
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.switchOutputMode('Table');
|
||||
|
||||
ndv.getters.backToCanvas().realHover(); // reset to default hover
|
||||
ndv.getters.inputTableRow(1).should('have.text', '1111');
|
||||
|
||||
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||
ndv.getters.inputTableRow(1).realHover();
|
||||
cy.wait(100);
|
||||
ndv.getters.outputHoveringItem().should('not.exist');
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
|
||||
|
||||
ndv.actions.selectInputNode('Code1');
|
||||
ndv.getters.inputTableRow(1).realHover();
|
||||
ndv.getters.inputTableRow(1).should('have.text', '1000');
|
||||
|
||||
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||
ndv.getters.outputTableRow(1).should('have.text', '1000');
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
|
||||
|
||||
ndv.actions.selectInputNode('Code');
|
||||
|
||||
ndv.getters.inputTableRow(1).realHover();
|
||||
cy.wait(100);
|
||||
ndv.getters.inputTableRow(1).should('have.text', '6666');
|
||||
|
||||
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||
|
||||
ndv.getters.outputHoveringItem().should('not.exist');
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
|
||||
|
||||
ndv.actions.selectInputNode('When clicking');
|
||||
|
||||
ndv.getters.inputTableRow(1).realHover();
|
||||
ndv.getters
|
||||
.inputTableRow(1)
|
||||
.should('have.text', "This is an item, but it's empty.")
|
||||
.realHover();
|
||||
|
||||
ndv.getters.outputHoveringItem().should('have.length', 6);
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
|
||||
});
|
||||
|
||||
it('can pair items between input and output across branches and runs', () => {
|
||||
cy.fixture('Test_workflow_5.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { META_KEY } from '../constants';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { getPopper } from '../utils';
|
||||
import { Interception } from 'cypress/types/net-stubbing';
|
||||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
|
||||
|
@ -26,6 +24,9 @@ function checkStickiesStyle(
|
|||
describe('Canvas Actions', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.get('#collapse-change-button').should('be.visible').click();
|
||||
cy.get('#side-menu[class*=collapsed i]').should('be.visible');
|
||||
workflowPage.actions.zoomToFit();
|
||||
});
|
||||
|
||||
it('adds sticky to canvas with default text and position', () => {
|
||||
|
@ -34,15 +35,12 @@ describe('Canvas Actions', () => {
|
|||
addDefaultSticky();
|
||||
workflowPage.actions.deselectAll();
|
||||
workflowPage.actions.addStickyFromContextMenu();
|
||||
workflowPage.actions.hitAddStickyShortcut();
|
||||
workflowPage.actions.hitAddSticky();
|
||||
|
||||
workflowPage.getters.stickies().should('have.length', 3);
|
||||
|
||||
// Should not add a sticky for ctrl+shift+s
|
||||
cy.get('body')
|
||||
.type(META_KEY, { delay: 500, release: false })
|
||||
.type('{shift}', { release: false })
|
||||
.type('s');
|
||||
cy.get('body').type(`{${META_KEY}+shift+s}`);
|
||||
|
||||
workflowPage.getters.stickies().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
|
@ -82,32 +80,6 @@ describe('Canvas Actions', () => {
|
|||
workflowPage.getters.stickies().should('have.length', 0);
|
||||
});
|
||||
|
||||
it('change sticky color', () => {
|
||||
workflowPage.actions.addSticky();
|
||||
|
||||
workflowPage.getters.stickies().should('have.length', 1);
|
||||
|
||||
workflowPage.actions.toggleColorPalette();
|
||||
|
||||
getPopper().should('be.visible');
|
||||
|
||||
workflowPage.actions.pickColor(2);
|
||||
|
||||
workflowPage.actions.toggleColorPalette();
|
||||
|
||||
getPopper().should('not.be.visible');
|
||||
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
|
||||
cy.wait('@createWorkflow').then((interception: Interception) => {
|
||||
const { request } = interception;
|
||||
const color = request.body?.nodes[0]?.parameters?.color;
|
||||
expect(color).to.equal(2);
|
||||
});
|
||||
|
||||
workflowPage.getters.stickies().should('have.length', 1);
|
||||
});
|
||||
|
||||
it('edits sticky and updates content as markdown', () => {
|
||||
workflowPage.actions.addSticky();
|
||||
|
||||
|
@ -301,15 +273,6 @@ function stickyShouldBePositionedCorrectly(position: Position) {
|
|||
});
|
||||
}
|
||||
|
||||
function stickyShouldHaveCorrectSize(size: [number, number]) {
|
||||
const yOffset = 0;
|
||||
const xOffset = 0;
|
||||
workflowPage.getters.stickies().should(($el) => {
|
||||
expect($el).to.have.css('height', `${yOffset + size[0]}px`);
|
||||
expect($el).to.have.css('width', `${xOffset + size[1]}px`);
|
||||
});
|
||||
}
|
||||
|
||||
function moveSticky(target: Position) {
|
||||
cy.drag('[data-test-id="sticky"]', [target.left, target.top], { abs: true });
|
||||
stickyShouldBePositionedCorrectly(target);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
|
||||
import { getVisiblePopper, getVisibleSelect } from '../utils';
|
||||
import { getVisiblePopper } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
@ -37,21 +37,49 @@ describe('Resource Locator', () => {
|
|||
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);
|
||||
});
|
||||
|
||||
it('should show create credentials modal when clicking "add your credential"', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
|
||||
ndv.getters.resourceLocator('documentId').should('be.visible');
|
||||
ndv.getters.resourceLocatorInput('documentId').click();
|
||||
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);
|
||||
ndv.getters.resourceLocatorAddCredentials().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
});
|
||||
|
||||
it('should show appropriate error when credentials are not valid', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
|
||||
// Add oAuth credentials
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
cy.get('.el-message-box').find('button').contains('Close').click();
|
||||
|
||||
ndv.getters.resourceLocatorInput('documentId').click();
|
||||
ndv.getters.resourceLocatorErrorMessage().should('contain', INVALID_CREDENTIALS_MESSAGE);
|
||||
});
|
||||
|
||||
it('should show appropriate errors when search filter is required', () => {
|
||||
workflowPage.actions.addNodeToCanvas('Github', true, true, 'On Pull Request');
|
||||
ndv.getters.resourceLocator('owner').should('be.visible');
|
||||
ndv.getters.resourceLocatorInput('owner').click();
|
||||
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);
|
||||
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
|
||||
ndv.getters.resourceLocatorInput('owner').click();
|
||||
ndv.getters.resourceLocatorSearch('owner').type('owner');
|
||||
ndv.getters.resourceLocatorErrorMessage().should('contain', INVALID_CREDENTIALS_MESSAGE);
|
||||
});
|
||||
|
||||
it('should reset resource locator when dependent field is changed', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
|
||||
|
@ -75,7 +103,7 @@ describe('Resource Locator', () => {
|
|||
|
||||
ndv.actions.setInvalidExpression({ fieldName: 'fieldId' });
|
||||
|
||||
ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview
|
||||
ndv.getters.inputPanel().click(); // remove focus from input, hide expression preview
|
||||
|
||||
ndv.getters.resourceLocatorInput('rlc').click();
|
||||
|
||||
|
|
|
@ -6,86 +6,51 @@ import {
|
|||
getPublicApiUpgradeCTA,
|
||||
} from '../pages';
|
||||
import planData from '../fixtures/Plan_data_opt_in_trial.json';
|
||||
import { INSTANCE_OWNER } from '../constants';
|
||||
|
||||
const mainSidebar = new MainSidebar();
|
||||
const bannerStack = new BannerStack();
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
||||
describe('Cloud', { disableAutoLogin: true }, () => {
|
||||
describe('Cloud', () => {
|
||||
before(() => {
|
||||
const now = new Date();
|
||||
const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
|
||||
planData.expirationDate = fiveDaysFromNow.toJSON();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.overrideSettings({
|
||||
deployment: { type: 'cloud' },
|
||||
n8nMetadata: { userId: '1' },
|
||||
});
|
||||
cy.intercept('GET', '/rest/admin/cloud-plan', planData).as('getPlanData');
|
||||
cy.intercept('GET', '/rest/cloud/proxy/user/me', {}).as('getCloudUserInfo');
|
||||
cy.intercept('GET', new RegExp('/rest/projects*')).as('projects');
|
||||
cy.intercept('GET', new RegExp('/rest/roles')).as('roles');
|
||||
});
|
||||
|
||||
function visitWorkflowPage() {
|
||||
cy.visit(workflowPage.url);
|
||||
cy.wait('@getPlanData');
|
||||
cy.wait('@projects');
|
||||
cy.wait('@roles');
|
||||
}
|
||||
|
||||
describe('BannerStack', () => {
|
||||
it('should render trial banner for opt-in cloud user', () => {
|
||||
cy.intercept('GET', '/rest/admin/cloud-plan', {
|
||||
body: planData,
|
||||
}).as('getPlanData');
|
||||
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
req.on('response', (res) => {
|
||||
res.send({
|
||||
data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } },
|
||||
});
|
||||
});
|
||||
}).as('loadSettings');
|
||||
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
|
||||
cy.wait('@getPlanData');
|
||||
visitWorkflowPage();
|
||||
|
||||
bannerStack.getters.banner().should('be.visible');
|
||||
|
||||
mainSidebar.actions.signout();
|
||||
|
||||
bannerStack.getters.banner().should('not.be.visible');
|
||||
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
|
||||
bannerStack.getters.banner().should('be.visible');
|
||||
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
|
||||
it('should not render opt-in-trial banner for non cloud deployment', () => {
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
req.on('response', (res) => {
|
||||
res.send({
|
||||
data: { ...res.body.data, deployment: { type: 'default' } },
|
||||
});
|
||||
});
|
||||
}).as('loadSettings');
|
||||
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
|
||||
bannerStack.getters.banner().should('not.be.visible');
|
||||
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Admin Home', () => {
|
||||
it('Should show admin button', () => {
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
req.on('response', (res) => {
|
||||
res.send({
|
||||
data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } },
|
||||
});
|
||||
});
|
||||
}).as('loadSettings');
|
||||
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
visitWorkflowPage();
|
||||
|
||||
mainSidebar.getters.adminPanel().should('be.visible');
|
||||
});
|
||||
|
@ -93,25 +58,8 @@ describe('Cloud', { disableAutoLogin: true }, () => {
|
|||
|
||||
describe('Public API', () => {
|
||||
it('Should show upgrade CTA for Public API if user is trialing', () => {
|
||||
cy.intercept('GET', '/rest/admin/cloud-plan', {
|
||||
body: planData,
|
||||
}).as('getPlanData');
|
||||
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
req.on('response', (res) => {
|
||||
res.send({
|
||||
data: {
|
||||
...res.body.data,
|
||||
deployment: { type: 'cloud' },
|
||||
n8nMetadata: { userId: 1 },
|
||||
},
|
||||
});
|
||||
});
|
||||
}).as('loadSettings');
|
||||
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
|
||||
visitPublicApiPage();
|
||||
cy.wait(['@loadSettings', '@projects', '@roles', '@getPlanData']);
|
||||
|
||||
getPublicApiUpgradeCTA().should('be.visible');
|
||||
});
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { MainSidebar } from './../pages/sidebar/main-sidebar';
|
||||
import generateOTPToken from 'cypress-otp';
|
||||
import { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../constants';
|
||||
import { SigninPage } from '../pages';
|
||||
import { PersonalSettingsPage } from '../pages/settings-personal';
|
||||
import { MfaLoginPage } from '../pages/mfa-login';
|
||||
import generateOTPToken from 'cypress-otp';
|
||||
import { MainSidebar } from './../pages/sidebar/main-sidebar';
|
||||
|
||||
const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD';
|
||||
|
||||
|
@ -34,16 +34,15 @@ const signinPage = new SigninPage();
|
|||
const personalSettingsPage = new PersonalSettingsPage();
|
||||
const mainSidebar = new MainSidebar();
|
||||
|
||||
describe('Two-factor authentication', () => {
|
||||
describe('Two-factor authentication', { disableAutoLogin: true }, () => {
|
||||
beforeEach(() => {
|
||||
Cypress.session.clearAllSavedSessions();
|
||||
cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, {
|
||||
owner: user,
|
||||
members: [],
|
||||
admin,
|
||||
});
|
||||
cy.on('uncaught:exception', (err, runnable) => {
|
||||
expect(err.message).to.include('Not logged in');
|
||||
cy.on('uncaught:exception', (error) => {
|
||||
expect(error.message).to.include('Not logged in');
|
||||
return false;
|
||||
});
|
||||
cy.intercept('GET', '/rest/mfa/qr').as('getMfaQrCode');
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
HTTP_REQUEST_NODE_NAME,
|
||||
IF_NODE_NAME,
|
||||
INSTANCE_OWNER,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
} from '../constants';
|
||||
|
@ -19,9 +18,9 @@ describe('Debug', () => {
|
|||
it('should be able to debug executions', () => {
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
cy.intercept('GET', '/rest/executions/*').as('getExecution');
|
||||
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun');
|
||||
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
cy.signinAsOwner();
|
||||
|
||||
workflowPage.actions.visit();
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -1,36 +1,47 @@
|
|||
import { TemplatesPage } from '../pages/templates';
|
||||
import { WorkflowPage } from '../pages/workflow';
|
||||
import { WorkflowsPage } from '../pages/workflows';
|
||||
import { MainSidebar } from '../pages/sidebar/main-sidebar';
|
||||
import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json';
|
||||
import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json';
|
||||
|
||||
const templatesPage = new TemplatesPage();
|
||||
const workflowPage = new WorkflowPage();
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
const mainSidebar = new MainSidebar();
|
||||
|
||||
describe('Workflow templates', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', '**/rest/settings', (req) => {
|
||||
// Disable cache
|
||||
delete req.headers['if-none-match']
|
||||
req.reply((res) => {
|
||||
if (res.body.data) {
|
||||
// Disable custom templates host if it has been overridden by another intercept
|
||||
res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' };
|
||||
}
|
||||
const mockTemplateHost = (host: string) => {
|
||||
cy.overrideSettings({
|
||||
templates: { enabled: true, host },
|
||||
});
|
||||
}).as('settingsRequest');
|
||||
};
|
||||
|
||||
describe('For api.n8n.io', () => {
|
||||
beforeEach(() => {
|
||||
mockTemplateHost('https://api.n8n.io/api/');
|
||||
});
|
||||
|
||||
it('Opens website when clicking templates sidebar link', () => {
|
||||
cy.visit(workflowsPage.url);
|
||||
mainSidebar.getters.menuItem('Templates').should('be.visible');
|
||||
mainSidebar.getters.templates().should('be.visible');
|
||||
// Templates should be a link to the website
|
||||
mainSidebar.getters.templates().parent('a').should('have.attr', 'href').and('include', 'https://n8n.io/workflows');
|
||||
mainSidebar.getters
|
||||
.templates()
|
||||
.parent('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', 'https://n8n.io/workflows');
|
||||
// Link should contain instance address and n8n version
|
||||
mainSidebar.getters.templates().parent('a').then(($a) => {
|
||||
mainSidebar.getters
|
||||
.templates()
|
||||
.parent('a')
|
||||
.then(($a) => {
|
||||
const href = $a.attr('href');
|
||||
const params = new URLSearchParams(href);
|
||||
// Link should have all mandatory parameters expected on the website
|
||||
expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include(window.location.origin);
|
||||
expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include(
|
||||
window.location.origin,
|
||||
);
|
||||
expect(params.get('utm_n8n_version')).to.match(/[0-9]+\.[0-9]+\.[0-9]+/);
|
||||
expect(params.get('utm_awc')).to.match(/[0-9]+/);
|
||||
});
|
||||
|
@ -38,9 +49,198 @@ describe('Workflow templates', () => {
|
|||
});
|
||||
|
||||
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.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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import {
|
|||
CODE_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
IF_NODE_NAME,
|
||||
INSTANCE_OWNER,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
} from '../constants';
|
||||
import {
|
||||
|
@ -103,7 +102,7 @@ const switchBetweenEditorAndHistory = () => {
|
|||
|
||||
const switchBetweenEditorAndWorkflowlist = () => {
|
||||
cy.getByTestId('menu-item').first().click();
|
||||
cy.wait(['@getUsers', '@getWorkflows', '@getActiveWorkflows', '@getCredentials']);
|
||||
cy.wait(['@getUsers', '@getWorkflows', '@getActiveWorkflows', '@getProjects']);
|
||||
|
||||
cy.getByTestId('resources-list-item').first().click();
|
||||
|
||||
|
@ -125,15 +124,10 @@ describe('Editor actions should work', () => {
|
|||
beforeEach(() => {
|
||||
cy.enableFeature('debugInEditor');
|
||||
cy.enableFeature('workflowHistory');
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
cy.signinAsOwner();
|
||||
createNewWorkflowAndActivate();
|
||||
});
|
||||
|
||||
it('after saving a new workflow', () => {
|
||||
editWorkflowAndDeactivate();
|
||||
editWorkflowMoreAndActivate();
|
||||
});
|
||||
|
||||
it('after switching between Editor and Executions', () => {
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
|
||||
|
@ -148,7 +142,7 @@ describe('Editor actions should work', () => {
|
|||
it('after switching between Editor and Debug', () => {
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
cy.intercept('GET', '/rest/executions/*').as('getExecution');
|
||||
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun');
|
||||
|
||||
editWorkflowAndDeactivate();
|
||||
workflowPage.actions.executeWorkflow();
|
||||
|
@ -186,9 +180,9 @@ describe('Editor zoom should work after route changes', () => {
|
|||
beforeEach(() => {
|
||||
cy.enableFeature('debugInEditor');
|
||||
cy.enableFeature('workflowHistory');
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
cy.signinAsOwner();
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Lots_of_nodes.json', `Lots of nodes`);
|
||||
cy.createFixtureWorkflow('Lots_of_nodes.json', 'Lots of nodes');
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
});
|
||||
|
||||
|
@ -196,9 +190,9 @@ describe('Editor zoom should work after route changes', () => {
|
|||
cy.intercept('GET', '/rest/workflow-history/workflow/*/version/*').as('getVersion');
|
||||
cy.intercept('GET', '/rest/workflow-history/workflow/*').as('getHistory');
|
||||
cy.intercept('GET', '/rest/users').as('getUsers');
|
||||
cy.intercept('GET', '/rest/workflows').as('getWorkflows');
|
||||
cy.intercept('GET', '/rest/workflows?*').as('getWorkflows');
|
||||
cy.intercept('GET', '/rest/active-workflows').as('getActiveWorkflows');
|
||||
cy.intercept('GET', '/rest/credentials').as('getCredentials');
|
||||
cy.intercept('GET', '/rest/projects').as('getProjects');
|
||||
|
||||
switchBetweenEditorAndHistory();
|
||||
zoomInAndCheckNodes();
|
||||
|
|
|
@ -1,17 +1,4 @@
|
|||
import {
|
||||
AGENT_NODE_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { createMockNodeExecutionData, runMockWorkflowExcution } from '../utils';
|
||||
import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
|
||||
import {
|
||||
addLanguageModelNodeToParent,
|
||||
addMemoryNodeToParent,
|
||||
|
@ -42,6 +29,19 @@ import {
|
|||
getManualChatModalLogsTree,
|
||||
sendManualChatMessage,
|
||||
} from '../composables/modals/chat-modal';
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
} from './../constants';
|
||||
|
||||
describe('Langchain Integration', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -149,7 +149,7 @@ describe('Langchain Integration', () => {
|
|||
const outputMessage = 'Hi there! How can I assist you today?';
|
||||
|
||||
clickExecuteNode();
|
||||
runMockWorkflowExcution({
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => sendManualChatMessage(inputMessage),
|
||||
runData: [
|
||||
createMockNodeExecutionData(BASIC_LLM_CHAIN_NODE_NAME, {
|
||||
|
@ -189,7 +189,7 @@ describe('Langchain Integration', () => {
|
|||
const outputMessage = 'Hi there! How can I assist you today?';
|
||||
|
||||
clickExecuteNode();
|
||||
runMockWorkflowExcution({
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => sendManualChatMessage(inputMessage),
|
||||
runData: [
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
|
@ -230,7 +230,7 @@ describe('Langchain Integration', () => {
|
|||
const inputMessage = 'Hello!';
|
||||
const outputMessage = 'Hi there! How can I assist you today?';
|
||||
|
||||
runMockWorkflowExcution({
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => {
|
||||
sendManualChatMessage(inputMessage);
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,23 +1,32 @@
|
|||
import workflow from '../fixtures/Manual_wait_set.json';
|
||||
import { importWorkflow, vistDemoPage } from '../pages/demo';
|
||||
import { importWorkflow, visitDemoPage } from '../pages/demo';
|
||||
import { WorkflowPage } from '../pages/workflow';
|
||||
import { errorToast } from '../pages/notifications';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
||||
describe('Demo', () => {
|
||||
beforeEach(() => {
|
||||
cy.overrideSettings({ previewMode: true });
|
||||
cy.signout();
|
||||
});
|
||||
|
||||
it('can import template', () => {
|
||||
vistDemoPage();
|
||||
visitDemoPage();
|
||||
errorToast().should('not.exist');
|
||||
importWorkflow(workflow);
|
||||
workflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
});
|
||||
|
||||
it('can override theme to dark', () => {
|
||||
vistDemoPage('dark');
|
||||
visitDemoPage('dark');
|
||||
cy.get('body').should('have.attr', 'data-theme', 'dark');
|
||||
errorToast().should('not.exist');
|
||||
});
|
||||
|
||||
it('can override theme to light', () => {
|
||||
vistDemoPage('light');
|
||||
visitDemoPage('light');
|
||||
cy.get('body').should('have.attr', 'data-theme', 'light');
|
||||
errorToast().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ const ndv = new NDV();
|
|||
describe('Node IO Filter', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Node_IO_filter.json', `Node IO filter`);
|
||||
cy.createFixtureWorkflow('Node_IO_filter.json', 'Node IO filter');
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
workflowPage.actions.executeWorkflow();
|
||||
});
|
||||
|
@ -15,13 +15,13 @@ describe('Node IO Filter', () => {
|
|||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
ndv.actions.close();
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
cy.wait(500);
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
ndv.getters.outputPanel().findChildByTestId('ndv-search').should('exist');
|
||||
cy.document().trigger('keyup', { key: '/' });
|
||||
|
||||
const searchInput = ndv.getters.searchInput();
|
||||
|
||||
searchInput.filter(':focus').should('exist');
|
||||
searchInput.should('have.focus');
|
||||
ndv.getters.pagination().find('li').should('have.length', 3);
|
||||
ndv.getters.outputDataContainer().find('mark').should('not.exist');
|
||||
|
||||
|
@ -36,19 +36,18 @@ describe('Node IO Filter', () => {
|
|||
|
||||
it('should filter input/output data separately', () => {
|
||||
workflowPage.getters.canvasNodes().eq(1).dblclick();
|
||||
cy.wait(500);
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
ndv.getters.inputDataContainer().should('be.visible');
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.getters.outputPanel().findChildByTestId('ndv-search').should('exist');
|
||||
cy.document().trigger('keyup', { key: '/' });
|
||||
|
||||
ndv.getters.outputPanel().findChildByTestId('ndv-search').filter(':focus').should('not.exist');
|
||||
ndv.getters.outputPanel().findChildByTestId('ndv-search').should('not.have.focus');
|
||||
|
||||
let focusedInput = ndv.getters
|
||||
.inputPanel()
|
||||
.findChildByTestId('ndv-search')
|
||||
.filter(':focus')
|
||||
.should('exist');
|
||||
.should('have.focus');
|
||||
|
||||
const getInputPagination = () =>
|
||||
ndv.getters.inputPanel().findChildByTestId('ndv-data-pagination');
|
||||
|
@ -82,13 +81,9 @@ describe('Node IO Filter', () => {
|
|||
|
||||
ndv.getters.outputDataContainer().trigger('mouseover');
|
||||
cy.document().trigger('keyup', { key: '/' });
|
||||
ndv.getters.inputPanel().findChildByTestId('ndv-search').filter(':focus').should('not.exist');
|
||||
ndv.getters.inputPanel().findChildByTestId('ndv-search').should('not.have.focus');
|
||||
|
||||
focusedInput = ndv.getters
|
||||
.outputPanel()
|
||||
.findChildByTestId('ndv-search')
|
||||
.filter(':focus')
|
||||
.should('exist');
|
||||
focusedInput = ndv.getters.outputPanel().findChildByTestId('ndv-search').should('have.focus');
|
||||
|
||||
getInputPagination().find('li').should('have.length', 3);
|
||||
getInputCounter().contains('21 items').should('exist');
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
|
||||
import { WorkerViewPage } from '../pages';
|
||||
|
||||
const workerViewPage = new WorkerViewPage();
|
||||
|
@ -10,13 +9,13 @@ describe('Worker View (unlicensed)', () => {
|
|||
});
|
||||
|
||||
it('should not show up in the menu sidebar', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[0]);
|
||||
cy.signinAsMember(0);
|
||||
cy.visit(workerViewPage.url);
|
||||
workerViewPage.getters.menuItem().should('not.exist');
|
||||
});
|
||||
|
||||
it('should show action box', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[0]);
|
||||
cy.signinAsMember(0);
|
||||
cy.visit(workerViewPage.url);
|
||||
workerViewPage.getters.workerViewUnlicensed().should('exist');
|
||||
});
|
||||
|
@ -29,14 +28,14 @@ describe('Worker View (licensed)', () => {
|
|||
});
|
||||
|
||||
it('should show up in the menu sidebar', () => {
|
||||
cy.signin(INSTANCE_OWNER);
|
||||
cy.signinAsOwner();
|
||||
cy.enableQueueMode();
|
||||
cy.visit(workerViewPage.url);
|
||||
workerViewPage.getters.menuItem().should('exist');
|
||||
});
|
||||
|
||||
it('should show worker list view', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[0]);
|
||||
cy.signinAsMember(0);
|
||||
cy.visit(workerViewPage.url);
|
||||
workerViewPage.getters.workerViewLicensed().should('exist');
|
||||
});
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { WorkflowPage } from "../pages";
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
|
||||
const INVALID_NAMES = [
|
||||
'https://n8n.io',
|
||||
|
@ -27,14 +25,14 @@ const VALID_NAMES = [
|
|||
];
|
||||
|
||||
describe('Personal Settings', () => {
|
||||
it ('should allow to change first and last name', () => {
|
||||
it('should allow to change first and last name', () => {
|
||||
cy.visit('/settings/personal');
|
||||
VALID_NAMES.forEach((name) => {
|
||||
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name[0]);
|
||||
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name[1]);
|
||||
cy.getByTestId('save-settings-button').click();
|
||||
workflowPage.getters.successToast().should('contain', 'Personal details updated');
|
||||
workflowPage.getters.successToast().find('.el-notification__closeBtn').click();
|
||||
successToast().should('contain', 'Personal details updated');
|
||||
successToast().find('.el-notification__closeBtn').click();
|
||||
});
|
||||
});
|
||||
it('not allow malicious values for personal data', () => {
|
||||
|
@ -43,10 +41,8 @@ describe('Personal Settings', () => {
|
|||
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name);
|
||||
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name);
|
||||
cy.getByTestId('save-settings-button').click();
|
||||
workflowPage.getters
|
||||
.errorToast()
|
||||
.should('contain', 'Malicious firstName | Malicious lastName');
|
||||
workflowPage.getters.errorToast().find('.el-notification__closeBtn').click();
|
||||
errorToast().should('contain', 'Malicious firstName | Malicious lastName');
|
||||
errorToast().find('.el-notification__closeBtn').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,10 +8,19 @@ import { WorkflowPage } from '../pages/workflow';
|
|||
import * as formStep from '../composables/setup-template-form-step';
|
||||
import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button';
|
||||
import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal';
|
||||
import TestTemplate1 from '../fixtures/Test_Template_1.json';
|
||||
import TestTemplate2 from '../fixtures/Test_Template_2.json';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
||||
const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate;
|
||||
const testTemplate = {
|
||||
id: 1205,
|
||||
data: TestTemplate1,
|
||||
};
|
||||
const templateWithoutCredentials = {
|
||||
id: 1344,
|
||||
data: TestTemplate2,
|
||||
};
|
||||
|
||||
// NodeView uses beforeunload listener that will show a browser
|
||||
// native popup, which will block cypress from continuing / exiting.
|
||||
|
@ -29,19 +38,19 @@ Cypress.on('window:before:load', (win) => {
|
|||
|
||||
describe('Template credentials setup', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${testTemplate.id}`, {
|
||||
fixture: testTemplate.fixture,
|
||||
cy.intercept(
|
||||
'GET',
|
||||
`https://api.n8n.io/api/templates/workflows/${testTemplate.id}`,
|
||||
testTemplate.data,
|
||||
).as('getTemplatePreview');
|
||||
cy.intercept(
|
||||
'GET',
|
||||
`https://api.n8n.io/api/workflows/templates/${testTemplate.id}`,
|
||||
testTemplate.data.workflow,
|
||||
).as('getTemplate');
|
||||
cy.overrideSettings({
|
||||
templates: { enabled: true, host: 'https://api.n8n.io/api/' },
|
||||
});
|
||||
cy.intercept('GET', '**/rest/settings', (req) => {
|
||||
// Disable cache
|
||||
delete req.headers['if-none-match']
|
||||
req.reply((res) => {
|
||||
if (res.body.data) {
|
||||
// Disable custom templates host if it has been overridden by another intercept
|
||||
res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' };
|
||||
}
|
||||
});
|
||||
}).as('settingsRequest');
|
||||
});
|
||||
|
||||
it('can be opened from template collection page', () => {
|
||||
|
@ -50,7 +59,7 @@ describe('Template credentials setup', () => {
|
|||
clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');
|
||||
|
||||
templateCredentialsSetupPage.getters
|
||||
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
|
||||
.title("Set up 'Promote new Shopify products on Twitter and Telegram' template")
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
|
@ -58,7 +67,7 @@ describe('Template credentials setup', () => {
|
|||
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
|
||||
|
||||
templateCredentialsSetupPage.getters
|
||||
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
|
||||
.title("Set up 'Promote new Shopify products on Twitter and Telegram' template")
|
||||
.should('be.visible');
|
||||
|
||||
templateCredentialsSetupPage.getters
|
||||
|
@ -108,7 +117,7 @@ describe('Template credentials setup', () => {
|
|||
|
||||
// Focus the canvas so the copy to clipboard works
|
||||
workflowPage.getters.canvasNodes().eq(0).realClick();
|
||||
workflowPage.actions.selectAll();
|
||||
workflowPage.actions.hitSelectAll();
|
||||
workflowPage.actions.hitCopy();
|
||||
|
||||
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
|
||||
|
@ -117,6 +126,7 @@ describe('Template credentials setup', () => {
|
|||
const workflow = JSON.parse(workflowJSON);
|
||||
|
||||
expect(workflow.meta).to.haveOwnProperty('templateId', testTemplate.id.toString());
|
||||
expect(workflow.meta).not.to.haveOwnProperty('templateCredsSetupCompleted');
|
||||
workflow.nodes.forEach((node: any) => {
|
||||
expect(Object.keys(node.credentials ?? {})).to.have.lengthOf(1);
|
||||
});
|
||||
|
@ -124,11 +134,9 @@ describe('Template credentials setup', () => {
|
|||
});
|
||||
|
||||
it('should work with a template that has no credentials (ADO-1603)', () => {
|
||||
const templateWithoutCreds = templateCredentialsSetupPage.testData.templateWithoutCredentials;
|
||||
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${templateWithoutCreds.id}`, {
|
||||
fixture: templateWithoutCreds.fixture,
|
||||
});
|
||||
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(templateWithoutCreds.id);
|
||||
const { id, data } = templateWithoutCredentials;
|
||||
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${id}`, data);
|
||||
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(id);
|
||||
|
||||
const expectedAppNames = ['1. Email (IMAP)', '2. Nextcloud'];
|
||||
const expectedAppDescriptions = [
|
||||
|
@ -151,7 +159,7 @@ describe('Template credentials setup', () => {
|
|||
workflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
});
|
||||
|
||||
describe('Credential setup from workflow editor', () => {
|
||||
describe('Credential setup from workflow editor', { disableAutoLogin: true }, () => {
|
||||
beforeEach(() => {
|
||||
cy.resetDatabase();
|
||||
cy.signinAsOwner();
|
||||
|
@ -189,7 +197,7 @@ describe('Template credentials setup', () => {
|
|||
|
||||
// Focus the canvas so the copy to clipboard works
|
||||
workflowPage.getters.canvasNodes().eq(0).realClick();
|
||||
workflowPage.actions.selectAll();
|
||||
workflowPage.actions.hitSelectAll();
|
||||
workflowPage.actions.hitCopy();
|
||||
|
||||
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { INSTANCE_ADMIN, INSTANCE_OWNER } from '../constants';
|
||||
import { SettingsPage } from '../pages/settings';
|
||||
|
||||
const settingsPage = new SettingsPage();
|
||||
|
||||
describe('Admin user', { disableAutoLogin: true }, () => {
|
||||
it('should see same Settings sub menu items as instance owner', () => {
|
||||
cy.signin(INSTANCE_OWNER);
|
||||
cy.signinAsOwner();
|
||||
cy.visit(settingsPage.url);
|
||||
|
||||
let ownerMenuItems = 0;
|
||||
|
@ -15,7 +14,7 @@ describe('Admin user', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
cy.signout();
|
||||
cy.signin(INSTANCE_ADMIN);
|
||||
cy.signinAsAdmin();
|
||||
cy.visit(settingsPage.url);
|
||||
|
||||
settingsPage.getters.menuItems().should('have.length', ownerMenuItems);
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -1,4 +1,3 @@
|
|||
import { INSTANCE_OWNER } from '../constants';
|
||||
import { WorkflowsPage } from '../pages/workflows';
|
||||
import {
|
||||
closeVersionUpdatesPanel,
|
||||
|
@ -11,11 +10,7 @@ const workflowsPage = new WorkflowsPage();
|
|||
|
||||
describe('Versions', () => {
|
||||
it('should open updates panel', () => {
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
req.continue((res) => {
|
||||
if (res.body.hasOwnProperty('data')) {
|
||||
res.body.data = {
|
||||
...res.body.data,
|
||||
cy.overrideSettings({
|
||||
releaseChannel: 'stable',
|
||||
versionCli: '1.0.0',
|
||||
versionNotifications: {
|
||||
|
@ -23,40 +18,10 @@ describe('Versions', () => {
|
|||
endpoint: 'https://api.n8n.io/api/versions/',
|
||||
infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html',
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}).as('settings');
|
||||
|
||||
cy.intercept('GET', 'https://api.n8n.io/api/versions/1.0.0', [
|
||||
{
|
||||
name: '1.3.1',
|
||||
createdAt: '2023-08-18T11:53:12.857Z',
|
||||
hasSecurityIssue: null,
|
||||
hasSecurityFix: null,
|
||||
securityIssueFixVersion: null,
|
||||
hasBreakingChange: null,
|
||||
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131',
|
||||
nodes: [],
|
||||
description: 'Includes <strong>bug fixes</strong>',
|
||||
},
|
||||
{
|
||||
name: '1.0.5',
|
||||
createdAt: '2023-07-24T10:54:56.097Z',
|
||||
hasSecurityIssue: false,
|
||||
hasSecurityFix: null,
|
||||
securityIssueFixVersion: null,
|
||||
hasBreakingChange: true,
|
||||
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104',
|
||||
nodes: [],
|
||||
description: 'Includes <strong>core functionality</strong> and <strong>bug fixes</strong>',
|
||||
},
|
||||
]);
|
||||
|
||||
cy.signin(INSTANCE_OWNER);
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
cy.wait('@settings');
|
||||
cy.wait('@loadSettings');
|
||||
|
||||
getVersionUpdatesPanelOpenButton().should('contain', '2 updates');
|
||||
openVersionUpdatesPanel();
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
import { WorkflowPage } from '../pages';
|
||||
import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const messageBox = new MessageBoxClass();
|
||||
|
@ -29,9 +30,9 @@ describe('Import workflow', () => {
|
|||
|
||||
workflowPage.getters.canvasNodes().should('have.length', 4);
|
||||
|
||||
workflowPage.getters.errorToast().should('not.exist');
|
||||
errorToast().should('not.exist');
|
||||
|
||||
workflowPage.getters.successToast().should('not.exist');
|
||||
successToast().should('not.exist');
|
||||
});
|
||||
|
||||
it('clicking outside modal should not show error toast', () => {
|
||||
|
@ -42,7 +43,7 @@ describe('Import workflow', () => {
|
|||
|
||||
cy.get('body').click(0, 0);
|
||||
|
||||
workflowPage.getters.errorToast().should('not.exist');
|
||||
errorToast().should('not.exist');
|
||||
});
|
||||
|
||||
it('canceling modal should not show error toast', () => {
|
||||
|
@ -52,7 +53,7 @@ describe('Import workflow', () => {
|
|||
workflowPage.getters.workflowMenuItemImportFromURLItem().click();
|
||||
messageBox.getters.cancel().click();
|
||||
|
||||
workflowPage.getters.errorToast().should('not.exist');
|
||||
errorToast().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -64,7 +65,7 @@ describe('Import workflow', () => {
|
|||
workflowPage.getters.workflowMenuItemImportFromFile().click();
|
||||
workflowPage.getters
|
||||
.workflowImportInput()
|
||||
.selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true });
|
||||
.selectFile('fixtures/Test_workflow-actions_paste-data.json', { force: true });
|
||||
cy.waitForLoad(false);
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.getters.canvasNodes().should('have.length', 5);
|
||||
|
|
567
cypress/e2e/39-projects.cy.ts
Normal file
567
cypress/e2e/39-projects.cy.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -35,7 +35,7 @@ describe('Node Creator', () => {
|
|||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
|
||||
nodeCreatorFeature.getters.searchBar().find('input').type('manual');
|
||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 2);
|
||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 1);
|
||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('manual123');
|
||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
|
||||
nodeCreatorFeature.getters
|
||||
|
@ -159,7 +159,7 @@ describe('Node Creator', () => {
|
|||
|
||||
it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => {
|
||||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
nodeCreatorFeature.getters.getCreatorItem('Manually').click();
|
||||
nodeCreatorFeature.getters.getCreatorItem('Trigger manually').click();
|
||||
|
||||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
|
||||
|
@ -308,7 +308,7 @@ describe('Node Creator', () => {
|
|||
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
|
||||
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
|
||||
NDVModal.actions.close();
|
||||
WorkflowPage.actions.deleteNode('When clicking "Test workflow"');
|
||||
WorkflowPage.actions.deleteNode('When clicking ‘Test workflow’');
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click();
|
||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
|
||||
nodeCreatorFeature.getters.getCreatorItem('n8n').click();
|
||||
|
|
28
cypress/e2e/40-manual-partial-execution.cy.ts
Normal file
28
cypress/e2e/40-manual-partial-execution.cy.ts
Normal 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
|
||||
});
|
||||
});
|
176
cypress/e2e/41-editors.cy.ts
Normal file
176
cypress/e2e/41-editors.cy.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
143
cypress/e2e/42-nps-survey.cy.ts
Normal file
143
cypress/e2e/42-nps-survey.cy.ts
Normal 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');
|
||||
});
|
||||
});
|
46
cypress/e2e/43-oauth-flow.cy.ts
Normal file
46
cypress/e2e/43-oauth-flow.cy.ts
Normal 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');
|
||||
});
|
||||
});
|
26
cypress/e2e/44-routing.cy.ts
Normal file
26
cypress/e2e/44-routing.cy.ts
Normal 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');
|
||||
});
|
||||
});
|
|
@ -1,6 +1,4 @@
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../constants';
|
||||
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
import { NodeCreator } from '../pages/features/node-creator';
|
||||
import { clickCreateNewCredential } from '../composables/ndv';
|
||||
|
@ -12,7 +10,7 @@ const ndv = new NDV();
|
|||
describe('NDV', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.renameWorkflow(uuid());
|
||||
workflowPage.actions.renameWithUniqueName();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
});
|
||||
|
||||
|
@ -24,6 +22,14 @@ describe('NDV', () => {
|
|||
ndv.getters.container().should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should show input panel when node is not connected', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||
workflowPage.actions.deselectAll();
|
||||
workflowPage.actions.addNodeToCanvas('Set');
|
||||
workflowPage.getters.canvasNodes().last().dblclick();
|
||||
ndv.getters.container().should('be.visible').should('contain', 'Wire me up');
|
||||
});
|
||||
|
||||
it('should test webhook node', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Webhook');
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
|
@ -46,12 +52,13 @@ describe('NDV', () => {
|
|||
});
|
||||
|
||||
it('should change input and go back to canvas', () => {
|
||||
cy.createFixtureWorkflow('NDV-test-select-input.json', `NDV test select input ${uuid()}`);
|
||||
cy.createFixtureWorkflow('NDV-test-select-input.json', 'NDV test select input');
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.getters.canvasNodes().last().dblclick();
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.getters.inputSelect().click();
|
||||
ndv.getters.inputOption().last().click();
|
||||
ndv.getters.inputDataContainer().find('[class*=schema_]').should('exist');
|
||||
ndv.getters.inputDataContainer().should('be.visible');
|
||||
ndv.getters.inputDataContainer().should('contain', 'start');
|
||||
ndv.getters.backToCanvas().click();
|
||||
ndv.getters.container().should('not.be.visible');
|
||||
|
@ -59,7 +66,7 @@ describe('NDV', () => {
|
|||
});
|
||||
|
||||
it('should disconect Switch outputs if rules order was changed', () => {
|
||||
cy.createFixtureWorkflow('NDV-test-switch_reorder.json', `NDV test switch reorder`);
|
||||
cy.createFixtureWorkflow('NDV-test-switch_reorder.json', 'NDV test switch reorder');
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.executeWorkflow();
|
||||
|
@ -105,13 +112,26 @@ describe('NDV', () => {
|
|||
});
|
||||
|
||||
it('should show all validation errors when opening pasted node', () => {
|
||||
cy.fixture('Test_workflow_ndv_errors.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
cy.createFixtureWorkflow('Test_workflow_ndv_errors.json', 'Validation errors');
|
||||
workflowPage.getters.canvasNodes().should('have.have.length', 1);
|
||||
workflowPage.actions.openNode('Airtable');
|
||||
cy.get('.has-issues').should('have.length', 3);
|
||||
cy.get('[class*=hasIssues]').should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should render run errors correctly', () => {
|
||||
cy.createFixtureWorkflow('Test_workflow_ndv_run_error.json', 'Run error');
|
||||
workflowPage.actions.openNode('Error');
|
||||
ndv.actions.execute();
|
||||
ndv.getters
|
||||
.nodeRunErrorMessage()
|
||||
.should('have.text', 'Info for expression missing from previous node');
|
||||
ndv.getters
|
||||
.nodeRunErrorDescription()
|
||||
.should(
|
||||
'contains.text',
|
||||
"An expression here won't work because it uses .item and n8n can't figure out the matching item.",
|
||||
);
|
||||
});
|
||||
|
||||
it('should save workflow using keyboard shortcut from NDV', () => {
|
||||
|
@ -135,7 +155,7 @@ describe('NDV', () => {
|
|||
'prop2',
|
||||
];
|
||||
function setupSchemaWorkflow() {
|
||||
cy.createFixtureWorkflow('Test_workflow_schema_test.json', `NDV test schema view ${uuid()}`);
|
||||
cy.createFixtureWorkflow('Test_workflow_schema_test.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.execute();
|
||||
|
@ -209,7 +229,7 @@ describe('NDV', () => {
|
|||
it('should display large schema', () => {
|
||||
cy.createFixtureWorkflow(
|
||||
'Test_workflow_schema_test_pinned_data.json',
|
||||
`NDV test schema view ${uuid()}`,
|
||||
'NDV test schema view 2',
|
||||
);
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.openNode('Set');
|
||||
|
@ -231,6 +251,9 @@ describe('NDV', () => {
|
|||
workflowPage.actions.executeWorkflow();
|
||||
workflowPage.actions.openNode('Set3');
|
||||
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.switchOutputMode('Table');
|
||||
|
||||
ndv.getters
|
||||
.inputRunSelector()
|
||||
.should('exist')
|
||||
|
@ -242,9 +265,6 @@ describe('NDV', () => {
|
|||
.find('input')
|
||||
.should('include.value', '2 of 2 (6 items)');
|
||||
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.switchOutputMode('Table');
|
||||
|
||||
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
|
||||
ndv.getters.inputTbodyCell(1, 0).should('have.text', '1111');
|
||||
|
@ -284,7 +304,7 @@ describe('NDV', () => {
|
|||
it('should display parameter hints correctly', () => {
|
||||
workflowPage.actions.visit();
|
||||
|
||||
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`);
|
||||
cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow 1');
|
||||
workflowPage.actions.openNode('Set1');
|
||||
|
||||
ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions
|
||||
|
@ -312,43 +332,11 @@ describe('NDV', () => {
|
|||
}
|
||||
ndv.getters.parameterInput('name').click(); // remove focus from input, hide expression preview
|
||||
|
||||
ndv.actions.validateExpressionPreview('value', output || input);
|
||||
ndv.actions.validateExpressionPreview('value', output ?? input);
|
||||
ndv.getters.parameterInput('value').clear();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not retrieve remote options when required params throw errors', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
|
||||
|
||||
ndv.getters.parameterInput('remoteOptions').click();
|
||||
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
|
||||
|
||||
ndv.actions.setInvalidExpression({ fieldName: 'fieldId', delay: 200 });
|
||||
|
||||
ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview
|
||||
|
||||
ndv.getters.parameterInput('remoteOptions').click();
|
||||
|
||||
ndv.getters.parameterInputIssues('remoteOptions').realHover({ scrollBehavior: false });
|
||||
// Remote options dropdown should not be visible
|
||||
ndv.getters.parameterInput('remoteOptions').find('.el-select').should('not.exist');
|
||||
});
|
||||
|
||||
it('should retrieve remote options when non-required params throw errors', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
|
||||
|
||||
ndv.getters.parameterInput('remoteOptions').click();
|
||||
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
|
||||
ndv.getters.parameterInput('remoteOptions').click();
|
||||
|
||||
ndv.actions.setInvalidExpression({ fieldName: 'otherField', delay: 50 });
|
||||
|
||||
ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview
|
||||
|
||||
ndv.getters.parameterInput('remoteOptions').click();
|
||||
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should flag issues as soon as params are set', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Webhook');
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
|
@ -395,7 +383,11 @@ describe('NDV', () => {
|
|||
});
|
||||
|
||||
it('should not retrieve remote options when a parameter value changes', () => {
|
||||
cy.intercept('/rest/dynamic-node-parameters/options?**', cy.spy().as('fetchParameterOptions'));
|
||||
cy.intercept(
|
||||
'POST',
|
||||
'/rest/dynamic-node-parameters/options',
|
||||
cy.spy().as('fetchParameterOptions'),
|
||||
);
|
||||
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
|
||||
// Type something into the field
|
||||
ndv.actions.typeIntoParameterInput('otherField', 'test');
|
||||
|
@ -411,7 +403,7 @@ describe('NDV', () => {
|
|||
}
|
||||
|
||||
it('should traverse floating nodes with mouse', () => {
|
||||
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`);
|
||||
cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes');
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
getFloatingNodeByPosition('inputMain').should('not.exist');
|
||||
getFloatingNodeByPosition('outputMain').should('exist');
|
||||
|
@ -457,7 +449,7 @@ describe('NDV', () => {
|
|||
});
|
||||
|
||||
it('should traverse floating nodes with keyboard', () => {
|
||||
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`);
|
||||
cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes');
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
getFloatingNodeByPosition('inputMain').should('not.exist');
|
||||
getFloatingNodeByPosition('outputMain').should('exist');
|
||||
|
@ -548,21 +540,21 @@ describe('NDV', () => {
|
|||
});
|
||||
|
||||
it('should show node name and version in settings', () => {
|
||||
cy.createFixtureWorkflow('Test_workflow_ndv_version.json', `NDV test version ${uuid()}`);
|
||||
cy.createFixtureWorkflow('Test_workflow_ndv_version.json', 'NDV test version');
|
||||
|
||||
workflowPage.actions.openNode('Edit Fields (old)');
|
||||
ndv.actions.openSettings();
|
||||
ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.3)');
|
||||
ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.4)');
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.openNode('Edit Fields (latest)');
|
||||
ndv.actions.openSettings();
|
||||
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.3 (Latest)');
|
||||
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.4 (Latest)');
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.openNode('Edit Fields (no typeVersion)');
|
||||
ndv.actions.openSettings();
|
||||
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.3 (Latest)');
|
||||
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.4 (Latest)');
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.openNode('Function');
|
||||
|
@ -572,7 +564,7 @@ describe('NDV', () => {
|
|||
});
|
||||
|
||||
it('Should render xml and html tags as strings and can search', () => {
|
||||
cy.createFixtureWorkflow('Test_workflow_xml_output.json', `test`);
|
||||
cy.createFixtureWorkflow('Test_workflow_xml_output.json', 'test');
|
||||
|
||||
workflowPage.actions.executeWorkflow();
|
||||
|
||||
|
@ -605,8 +597,7 @@ describe('NDV', () => {
|
|||
ndv.getters.outputDisplayMode().find('label').eq(2).click({ force: true });
|
||||
ndv.getters
|
||||
.outputDataContainer()
|
||||
.findChildByTestId('run-data-schema-item')
|
||||
.find('> span')
|
||||
.findChildByTestId('run-data-schema-item-value')
|
||||
.should('include.text', '<?xml version="1.0" encoding="UTF-8"?>');
|
||||
});
|
||||
|
||||
|
@ -627,7 +618,7 @@ describe('NDV', () => {
|
|||
ndv.getters.backToCanvas().click();
|
||||
workflowPage.actions.executeWorkflow();
|
||||
// Manual tigger node should show success indicator
|
||||
workflowPage.actions.openNode('When clicking "Test workflow"');
|
||||
workflowPage.actions.openNode('When clicking ‘Test workflow’');
|
||||
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
||||
// Code node should show error
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
@ -673,6 +664,23 @@ describe('NDV', () => {
|
|||
ndv.getters.parameterInput('operation').find('input').should('have.value', 'Delete');
|
||||
});
|
||||
|
||||
it('Should show error state when remote options cannot be fetched', () => {
|
||||
cy.intercept('POST', '/rest/dynamic-node-parameters/options', { statusCode: 500 }).as(
|
||||
'parameterOptions',
|
||||
);
|
||||
|
||||
workflowPage.actions.addInitialNodeToCanvas(NOTION_NODE_NAME, {
|
||||
keepNdvOpen: true,
|
||||
action: 'Update a database page',
|
||||
});
|
||||
|
||||
ndv.actions.addItemToFixedCollection('propertiesUi');
|
||||
ndv.getters
|
||||
.parameterInput('key')
|
||||
.find('input')
|
||||
.should('have.value', 'Error fetching options from Notion');
|
||||
});
|
||||
|
||||
it('Should open appropriate node creator after clicking on connection hint link', () => {
|
||||
const nodeCreator = new NodeCreator();
|
||||
const hintMapper = {
|
||||
|
@ -685,7 +693,7 @@ describe('NDV', () => {
|
|||
};
|
||||
cy.createFixtureWorkflow(
|
||||
'open_node_creator_for_connection.json',
|
||||
`open_node_creator_for_connection ${uuid()}`,
|
||||
'open_node_creator_for_connection',
|
||||
);
|
||||
|
||||
Object.entries(hintMapper).forEach(([node, group]) => {
|
||||
|
@ -696,20 +704,59 @@ describe('NDV', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Stop listening for trigger event from NDV', () => {
|
||||
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
|
||||
workflowPage.actions.addInitialNodeToCanvas('Local File Trigger', {
|
||||
keepNdvOpen: true,
|
||||
action: 'On Changes To A Specific File',
|
||||
isTrigger: true,
|
||||
it('should allow selecting item for expressions', () => {
|
||||
workflowPage.actions.visit();
|
||||
|
||||
cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow 2');
|
||||
workflowPage.actions.openNode('Set');
|
||||
|
||||
ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions
|
||||
ndv.actions.typeIntoParameterInput('value', '{{', {
|
||||
parseSpecialCharSequences: false,
|
||||
});
|
||||
ndv.getters.triggerPanelExecuteButton().should('exist');
|
||||
ndv.getters.triggerPanelExecuteButton().realClick();
|
||||
ndv.getters.triggerPanelExecuteButton().should('contain', 'Stop Listening');
|
||||
ndv.getters.triggerPanelExecuteButton().realClick();
|
||||
cy.wait('@workflowRun').then(() => {
|
||||
ndv.getters.triggerPanelExecuteButton().should('contain', 'Test step');
|
||||
workflowPage.getters.successToast().should('exist');
|
||||
ndv.actions.typeIntoParameterInput('value', '$json.input[0].count');
|
||||
ndv.getters.inlineExpressionEditorOutput().should('have.text', '0');
|
||||
|
||||
ndv.actions.expressionSelectNextItem();
|
||||
ndv.getters.inlineExpressionEditorOutput().should('have.text', '1');
|
||||
ndv.getters.inlineExpressionEditorItemInput().should('have.value', '1');
|
||||
ndv.getters.inlineExpressionEditorItemNextButton().should('be.disabled');
|
||||
|
||||
ndv.actions.expressionSelectPrevItem();
|
||||
ndv.getters.inlineExpressionEditorOutput().should('have.text', '0');
|
||||
ndv.getters.inlineExpressionEditorItemInput().should('have.value', '0');
|
||||
ndv.getters.inlineExpressionEditorItemPrevButton().should('be.disabled');
|
||||
|
||||
ndv.actions.expressionSelectItem(1);
|
||||
ndv.getters.inlineExpressionEditorOutput().should('have.text', '1');
|
||||
});
|
||||
|
||||
it('should show data from the correct output in schema view', () => {
|
||||
cy.createFixtureWorkflow('Test_workflow_multiple_outputs.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.executeWorkflow();
|
||||
workflowPage.actions.openNode('Only Item 1');
|
||||
ndv.getters.inputPanel().should('be.visible');
|
||||
ndv.getters
|
||||
.inputPanel()
|
||||
.find('[data-test-id=run-data-schema-item]')
|
||||
.should('contain.text', 'onlyOnItem1');
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.openNode('Only Item 2');
|
||||
ndv.getters.inputPanel().should('be.visible');
|
||||
ndv.getters
|
||||
.inputPanel()
|
||||
.find('[data-test-id=run-data-schema-item]')
|
||||
.should('contain.text', 'onlyOnItem2');
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.openNode('Only Item 3');
|
||||
ndv.getters.inputPanel().should('be.visible');
|
||||
ndv.getters
|
||||
.inputPanel()
|
||||
.find('[data-test-id=run-data-schema-item]')
|
||||
.should('contain.text', 'onlyOnItem3');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { NDV } from '../pages/ndv';
|
||||
import { successToast } from '../pages/notifications';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
const ndv = new NDV();
|
||||
|
@ -28,13 +30,13 @@ describe('Code node', () => {
|
|||
it('should execute the placeholder successfully in both modes', () => {
|
||||
ndv.actions.execute();
|
||||
|
||||
WorkflowPage.getters.successToast().contains('Node executed successfully');
|
||||
successToast().contains('Node executed successfully');
|
||||
ndv.getters.parameterInput('mode').click();
|
||||
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
|
||||
|
||||
ndv.actions.execute();
|
||||
|
||||
WorkflowPage.getters.successToast().contains('Node executed successfully');
|
||||
successToast().contains('Node executed successfully');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -84,7 +86,7 @@ describe('Code node', () => {
|
|||
cy.getByTestId('ask-ai-cta-tooltip-no-prompt').should('exist');
|
||||
cy.getByTestId('ask-ai-prompt-input')
|
||||
// Type random 14 character string
|
||||
.type([...Array(14)].map(() => ((Math.random() * 36) | 0).toString(36)).join(''));
|
||||
.type(nanoid(14));
|
||||
|
||||
cy.getByTestId('ask-ai-cta').realHover();
|
||||
cy.getByTestId('ask-ai-cta-tooltip-prompt-too-short').should('exist');
|
||||
|
@ -92,14 +94,14 @@ describe('Code node', () => {
|
|||
cy.getByTestId('ask-ai-prompt-input')
|
||||
.clear()
|
||||
// Type random 15 character string
|
||||
.type([...Array(15)].map(() => ((Math.random() * 36) | 0).toString(36)).join(''));
|
||||
.type(nanoid(15));
|
||||
cy.getByTestId('ask-ai-cta').should('be.enabled');
|
||||
|
||||
cy.getByTestId('ask-ai-prompt-counter').should('contain.text', '15 / 600');
|
||||
});
|
||||
|
||||
it('should send correct schema and replace code', () => {
|
||||
const prompt = [...Array(20)].map(() => ((Math.random() * 36) | 0).toString(36)).join('');
|
||||
const prompt = nanoid(20);
|
||||
cy.get('#tab-ask-ai').click();
|
||||
ndv.actions.executePrevious();
|
||||
|
||||
|
@ -129,7 +131,7 @@ describe('Code node', () => {
|
|||
});
|
||||
|
||||
it('should show error based on status code', () => {
|
||||
const prompt = [...Array(20)].map(() => ((Math.random() * 36) | 0).toString(36)).join('');
|
||||
const prompt = nanoid(20);
|
||||
cy.get('#tab-ask-ai').click();
|
||||
ndv.actions.executePrevious();
|
||||
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import {
|
||||
CODE_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
META_KEY,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
INSTANCE_MEMBERS,
|
||||
INSTANCE_OWNER,
|
||||
NOTION_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { WorkflowExecutionsTab } from '../pages';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
|
||||
const NEW_WORKFLOW_NAME = 'Something else';
|
||||
const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow';
|
||||
|
@ -35,6 +34,20 @@ describe('Workflow Actions', () => {
|
|||
WorkflowPage.getters.isWorkflowSaved();
|
||||
});
|
||||
|
||||
it('should not save already saved workflow', () => {
|
||||
cy.intercept('PATCH', '/rest/workflows/*').as('saveWorkflow');
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.wait('@saveWorkflow');
|
||||
WorkflowPage.getters.isWorkflowSaved();
|
||||
// Try to save a few times
|
||||
WorkflowPage.actions.saveWorkflowUsingKeyboardShortcut();
|
||||
WorkflowPage.actions.saveWorkflowUsingKeyboardShortcut();
|
||||
// Should be saved only once
|
||||
cy.get('@saveWorkflow.all').should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should not be able to activate unsaved workflow', () => {
|
||||
WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled');
|
||||
});
|
||||
|
@ -53,6 +66,30 @@ describe('Workflow Actions', () => {
|
|||
WorkflowPage.getters.isWorkflowActivated();
|
||||
});
|
||||
|
||||
it('should not be be able to activate workflow when nodes have errors', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
successToast().should('exist');
|
||||
WorkflowPage.actions.clickWorkflowActivator();
|
||||
errorToast().should('exist');
|
||||
});
|
||||
|
||||
it('should be be able to activate workflow when nodes with errors are disabled', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
successToast().should('exist');
|
||||
// First, try to activate the workflow with errors
|
||||
WorkflowPage.actions.clickWorkflowActivator();
|
||||
errorToast().should('exist');
|
||||
// Now, disable the node with errors
|
||||
WorkflowPage.getters.canvasNodes().last().click();
|
||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
WorkflowPage.actions.activateWorkflow();
|
||||
WorkflowPage.getters.isWorkflowActivated();
|
||||
});
|
||||
|
||||
it('should save new workflow after renaming', () => {
|
||||
WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME);
|
||||
WorkflowPage.getters.isWorkflowSaved();
|
||||
|
@ -96,13 +133,13 @@ describe('Workflow Actions', () => {
|
|||
);
|
||||
cy.reload();
|
||||
cy.get('.el-loading-mask').should('exist');
|
||||
cy.get('body').type(META_KEY, { release: false }).type('s');
|
||||
cy.get('body').type(META_KEY, { release: false }).type('s');
|
||||
cy.get('body').type(META_KEY, { release: false }).type('s');
|
||||
WorkflowPage.actions.hitSaveWorkflow();
|
||||
WorkflowPage.actions.hitSaveWorkflow();
|
||||
WorkflowPage.actions.hitSaveWorkflow();
|
||||
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0));
|
||||
cy.waitForLoad();
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
cy.get('body').type(META_KEY, { release: false }).type('s');
|
||||
WorkflowPage.actions.hitSaveWorkflow();
|
||||
cy.wait('@saveWorkflow');
|
||||
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1));
|
||||
});
|
||||
|
@ -132,10 +169,11 @@ describe('Workflow Actions', () => {
|
|||
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
||||
|
||||
cy.get('#node-creator').should('not.exist');
|
||||
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('a');
|
||||
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
cy.get('.jtk-drag-selected').should('have.length', 2);
|
||||
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('c');
|
||||
WorkflowPage.getters.successToast().should('exist');
|
||||
WorkflowPage.actions.hitCopy();
|
||||
successToast().should('exist');
|
||||
});
|
||||
|
||||
it('should paste nodes (both current and old node versions)', () => {
|
||||
|
@ -147,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', () => {
|
||||
cy.visit(WorkflowPages.url);
|
||||
WorkflowPages.getters.workflowCards().then((cards) => {
|
||||
const totalWorkflows = cards.length;
|
||||
cy.intercept('GET', '/rest/workflows', (req) => {
|
||||
req.on('response', (res) => {
|
||||
const totalWorkflows = res.body.count ?? 0;
|
||||
|
||||
WorkflowPage.actions.visit();
|
||||
// Open settings dialog
|
||||
|
@ -186,8 +238,9 @@ describe('Workflow Actions', () => {
|
|||
// Save settings
|
||||
WorkflowPage.getters.workflowSettingsSaveButton().click();
|
||||
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', () => {
|
||||
|
@ -203,8 +256,8 @@ describe('Workflow Actions', () => {
|
|||
WorkflowPage.getters.workflowMenuItemDelete().click();
|
||||
cy.get('div[role=dialog][aria-modal=true]').should('be.visible');
|
||||
cy.get('button.btn--confirm').should('be.visible').click();
|
||||
WorkflowPage.getters.successToast().should('exist');
|
||||
cy.url().should('include', '/workflow/new');
|
||||
successToast().should('exist');
|
||||
cy.url().should('include', WorkflowPages.url);
|
||||
});
|
||||
|
||||
describe('duplicate workflow', () => {
|
||||
|
@ -232,7 +285,7 @@ describe('Workflow Actions', () => {
|
|||
.contains('Duplicate')
|
||||
.should('be.visible');
|
||||
WorkflowPage.getters.duplicateWorkflowModal().find('button').contains('Duplicate').click();
|
||||
WorkflowPage.getters.errorToast().should('not.exist');
|
||||
errorToast().should('not.exist');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -272,18 +325,43 @@ describe('Workflow Actions', () => {
|
|||
WorkflowPage.getters.canvasNodePlusEndpointByName(EDIT_FIELDS_SET_NODE_NAME).click();
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
});
|
||||
|
||||
it('should run workflow on button click', () => {
|
||||
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
WorkflowPage.getters.executeWorkflowButton().click();
|
||||
successToast().should('contain.text', 'Workflow executed successfully');
|
||||
});
|
||||
|
||||
it('should run workflow using keyboard shortcut', () => {
|
||||
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
WorkflowPage.actions.hitExecuteWorkflow();
|
||||
successToast().should('contain.text', 'Workflow executed successfully');
|
||||
});
|
||||
|
||||
it('should not run empty workflows', () => {
|
||||
// Clear the canvas
|
||||
WorkflowPage.actions.hitDeleteAllNodes();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 0);
|
||||
// Button should be disabled
|
||||
WorkflowPage.getters.executeWorkflowButton().should('be.disabled');
|
||||
// Keyboard shortcut should not work
|
||||
WorkflowPage.actions.hitExecuteWorkflow();
|
||||
successToast().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Menu entry Push To Git', () => {
|
||||
it('should not show up in the menu for members', () => {
|
||||
cy.signin(INSTANCE_MEMBERS[0]);
|
||||
cy.signinAsMember(0);
|
||||
cy.visit(WorkflowPages.url);
|
||||
WorkflowPage.actions.visit();
|
||||
WorkflowPage.getters.workflowMenuItemGitPush().should('not.exist');
|
||||
});
|
||||
|
||||
it('should show up for owners', () => {
|
||||
cy.signin(INSTANCE_OWNER);
|
||||
cy.signinAsOwner();
|
||||
cy.visit(WorkflowPages.url);
|
||||
WorkflowPage.actions.visit();
|
||||
WorkflowPage.getters.workflowMenuItemGitPush().should('exist');
|
||||
|
|
|
@ -8,28 +8,29 @@ describe('Expression editor modal', () => {
|
|||
beforeEach(() => {
|
||||
WorkflowPage.actions.visit();
|
||||
WorkflowPage.actions.addInitialNodeToCanvas('Schedule');
|
||||
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError');
|
||||
cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
|
||||
});
|
||||
|
||||
describe('Static data', () => {
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openExpressionEditorModal();
|
||||
});
|
||||
|
||||
it('should resolve primitive resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ 1 + 2');
|
||||
WorkflowPage.getters.expressionModalInput().click().type('{{ 1 + 2');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^3$/);
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ "ab" + "cd"');
|
||||
WorkflowPage.getters.expressionModalInput().click().type('{{ "ab" + "cd"');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^abcd$/);
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ true && false');
|
||||
WorkflowPage.getters.expressionModalInput().click().type('{{ true && false');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^false$/);
|
||||
});
|
||||
|
||||
|
@ -37,6 +38,7 @@ describe('Expression editor modal', () => {
|
|||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters
|
||||
.expressionModalInput()
|
||||
.click()
|
||||
.type('{{ { a : 1 }', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^\[Object: \{"a": 1\}\]$/);
|
||||
|
||||
|
@ -44,18 +46,19 @@ describe('Expression editor modal', () => {
|
|||
|
||||
WorkflowPage.getters
|
||||
.expressionModalInput()
|
||||
.click()
|
||||
.type('{{ { a : 1 }.a', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
|
||||
});
|
||||
|
||||
it('should resolve array resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3]');
|
||||
WorkflowPage.getters.expressionModalInput().click().type('{{ [1, 2, 3]');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3][0]');
|
||||
WorkflowPage.getters.expressionModalInput().click().type('{{ [1, 2, 3][0]');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
|
||||
});
|
||||
});
|
||||
|
@ -67,30 +70,34 @@ describe('Expression editor modal', () => {
|
|||
ndv.actions.close();
|
||||
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openExpressionEditorModal();
|
||||
});
|
||||
|
||||
it('should resolve $parameter[]', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]');
|
||||
WorkflowPage.getters.expressionModalInput().click().type('{{ $parameter["operation"]');
|
||||
WorkflowPage.getters.expressionModalOutput().should('have.text', 'getAll');
|
||||
});
|
||||
|
||||
it('should resolve input: $json,$input,$(nodeName)', () => {
|
||||
// Previous nodes have not run, input is empty
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $json.myStr');
|
||||
WorkflowPage.getters.expressionModalInput().click().type('{{ $json.myStr');
|
||||
WorkflowPage.getters
|
||||
.expressionModalOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $input.item.json.myStr');
|
||||
WorkflowPage.getters.expressionModalInput().click().type('{{ $input.item.json.myStr');
|
||||
WorkflowPage.getters
|
||||
.expressionModalOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type("{{ $('Schedule Trigger').item.json.myStr");
|
||||
WorkflowPage.getters
|
||||
.expressionModalInput()
|
||||
.click()
|
||||
.type("{{ $('Schedule Trigger').item.json.myStr");
|
||||
WorkflowPage.getters
|
||||
.expressionModalOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
|
@ -104,13 +111,16 @@ describe('Expression editor modal', () => {
|
|||
|
||||
// Previous nodes have run, input can be resolved
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $json.myStr');
|
||||
WorkflowPage.getters.expressionModalInput().click().type('{{ $json.myStr');
|
||||
WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday');
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $input.item.json.myStr');
|
||||
WorkflowPage.getters.expressionModalInput().click().type('{{ $input.item.json.myStr');
|
||||
WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday');
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type("{{ $('Schedule Trigger').item.json.myStr");
|
||||
WorkflowPage.getters
|
||||
.expressionModalInput()
|
||||
.click()
|
||||
.type("{{ $('Schedule Trigger').item.json.myStr");
|
||||
WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{
|
||||
"parameters": {},
|
||||
"id": "d0eda550-2526-42a1-aa19-dee411c8acf9",
|
||||
"name": "When clicking \"Test workflow\"",
|
||||
"name": "When clicking ‘Test workflow’",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
|
@ -91,7 +91,7 @@
|
|||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"When clicking \"Test workflow\"": {
|
||||
"When clicking ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{
|
||||
"parameters": {},
|
||||
"id": "369fe424-dd3b-4399-9de3-50bd4ce1f75b",
|
||||
"name": "When clicking \"Test workflow\"",
|
||||
"name": "When clicking ‘Test workflow’",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
|
@ -570,7 +570,7 @@
|
|||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"When clicking \"Test workflow\"": {
|
||||
"When clicking ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{
|
||||
"parameters": {},
|
||||
"id": "5ae8991f-08a2-4b27-b61c-85e3b8a83693",
|
||||
"name": "When clicking \"Test workflow\"",
|
||||
"name": "When clicking ‘Test workflow’",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
|
@ -14,7 +14,7 @@
|
|||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://random-data-api.com/api/v2/users?size=5",
|
||||
"url": "https://internal.users.n8n.cloud/webhook/random-data-api",
|
||||
"options": {}
|
||||
},
|
||||
"id": "22511d75-ab54-49e1-b8af-08b8b3372373",
|
||||
|
@ -28,7 +28,7 @@
|
|||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.first_name,\n firstnNameReversed: item.json.first_name_BUG.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();"
|
||||
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.firstname,\n firstnNameReversed: item.json.firstname.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();"
|
||||
},
|
||||
"id": "4b66b15a-1685-46c1-a5e3-ebf8cdb11d21",
|
||||
"name": "do something with them",
|
||||
|
@ -78,14 +78,14 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"When clicking \"Test workflow\"": [
|
||||
"When clicking ‘Test workflow’": [
|
||||
{
|
||||
"json": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Test workflow\"": {
|
||||
"When clicking ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
|
|
48
cypress/fixtures/NDV-debug-generate-data.json
Normal file
48
cypress/fixtures/NDV-debug-generate-data.json
Normal 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
Loading…
Reference in a new issue