diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..cffda42f08 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -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 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..c81220d1b4 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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"] + } + } +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..16103c2a4f --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -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 diff --git a/.dockerignore b/.dockerignore index fe841322cc..1b60244f21 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,6 +10,7 @@ packages/**/.turbo packages/**/*.test.* .git .github +!.github/scripts *.tsbuildinfo packages/cli/dist/**/e2e.* docker/compose diff --git a/.github/docker-compose.yml b/.github/docker-compose.yml index 3ae783222b..84a1b9c961 100644 --- a/.github/docker-compose.yml +++ b/.github/docker-compose.yml @@ -19,7 +19,7 @@ services: restart: always environment: - POSTGRES_DB=n8n - - POSTGRES_USER=root + - POSTGRES_USER=postgres - POSTGRES_PASSWORD=password ports: - 5432:5432 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index dfcc0615a7..0df76d2700 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,16 +1,26 @@ ## Summary -> 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. - - + ## 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)) - [ ] [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. \ No newline at end of file +- [ ] Tests included. +- [ ] PR Labeled with `release/backport` (if the PR is an urgent fix that needs to be backported) diff --git a/.github/pull_request_title_conventions.md b/.github/pull_request_title_conventions.md index f6f762048f..8808000e3b 100644 --- a/.github/pull_request_title_conventions.md +++ b/.github/pull_request_title_conventions.md @@ -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. diff --git a/.github/scripts/ensure-provenance-fields.mjs b/.github/scripts/ensure-provenance-fields.mjs new file mode 100644 index 0000000000..2fad319a62 --- /dev/null +++ b/.github/scripts/ensure-provenance-fields.mjs @@ -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'); +} diff --git a/scripts/trim-fe-packageJson.js b/.github/scripts/trim-fe-packageJson.js similarity index 79% rename from scripts/trim-fe-packageJson.js rename to .github/scripts/trim-fe-packageJson.js index 611cb2ec3c..791e483a2d 100644 --- a/scripts/trim-fe-packageJson.js +++ b/.github/scripts/trim-fe-packageJson.js @@ -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'); }; diff --git a/.github/workflows/check-documentation-urls.yml b/.github/workflows/check-documentation-urls.yml index a667b4088c..34db00aa9b 100644 --- a/.github/workflows/check-documentation-urls.yml +++ b/.github/workflows/check-documentation-urls.yml @@ -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 diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml index a66ae1232f..add6e858a3 100644 --- a/.github/workflows/check-pr-title.yml +++ b/.github/workflows/check-pr-title.yml @@ -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 diff --git a/.github/workflows/check-tests.yml b/.github/workflows/check-tests.yml index 30577bdc56..97f380974a 100644 --- a/.github/workflows/check-tests.yml +++ b/.github/workflows/check-tests.yml @@ -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 diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index ce860885c1..bfd43e9257 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -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}} diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index 41ee830bd5..5e828a7022 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -9,25 +9,23 @@ jobs: install-and-build: runs-on: ubuntu-latest - timeout-minutes: 30 - - strategy: - matrix: - node-version: [18.x, 20.x] + timeout-minutes: 10 steps: - uses: actions/checkout@v4.1.1 - run: corepack enable - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.1 + - uses: actions/setup-node@v4.0.2 with: - node-version: ${{ matrix.node-version }} + node-version: 20.x cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Setup build cache + uses: rharkor/caching-for-turbo@v1.5 + - name: Build run: pnpm build @@ -35,7 +33,7 @@ jobs: uses: actions/cache/save@v4.0.0 with: path: ./packages/**/dist - key: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint + key: ${{ github.sha }}-base:build unit-test: name: Unit tests @@ -43,46 +41,22 @@ jobs: needs: install-and-build strategy: matrix: - node-version: [18.x, 20.x] + node-version: [18.x, 20.x, 22.4] with: ref: ${{ inputs.branch }} nodeVersion: ${{ matrix.node-version }} - cacheKey: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint - collectCoverage: true + cacheKey: ${{ github.sha }}-base:build + collectCoverage: ${{ matrix.node-version == '20.x' }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} lint: - name: Lint changes - runs-on: ubuntu-latest + name: Lint + uses: ./.github/workflows/linting-reusable.yml needs: install-and-build - strategy: - matrix: - node-version: [18.x, 20.x] - steps: - - uses: actions/checkout@v4.1.1 - with: - repository: n8n-io/n8n - ref: ${{ inputs.branch }} - - - run: corepack enable - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.1 - with: - node-version: ${{ matrix.node-version }} - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Restore cached build artifacts - uses: actions/cache/restore@v4.0.0 - with: - path: ./packages/**/dist - key: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint - - - name: Lint - env: - CI_LINT_MASTER: true - run: pnpm lint + with: + ref: ${{ inputs.branch }} + cacheKey: ${{ github.sha }}-base:build notify-on-failure: name: Notify Slack on failure diff --git a/.github/workflows/ci-postgres-mysql.yml b/.github/workflows/ci-postgres-mysql.yml index 18c79ce435..d5abf1fa26 100644 --- a/.github/workflows/ci-postgres-mysql.yml +++ b/.github/workflows/ci-postgres-mysql.yml @@ -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: diff --git a/.github/workflows/ci-pull-requests.yml b/.github/workflows/ci-pull-requests.yml index 17bfb05891..78a5388950 100644 --- a/.github/workflows/ci-pull-requests.yml +++ b/.github/workflows/ci-pull-requests.yml @@ -3,7 +3,7 @@ name: Build, unit test and lint branch on: [pull_request] jobs: - install: + install-and-build: name: Install & Build runs-on: ubuntu-latest steps: @@ -13,57 +13,41 @@ jobs: ref: refs/pull/${{ github.event.pull_request.number }}/merge - run: corepack enable - - name: Use Node.js 18 - uses: actions/setup-node@v4.0.1 + - uses: actions/setup-node@v4.0.2 with: - node-version: 18.x + node-version: 20.x cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Setup build cache + uses: rharkor/caching-for-turbo@v1.5 + - name: Build run: pnpm build + - name: Run typecheck + run: pnpm typecheck + - name: Cache build artifacts uses: actions/cache/save@v4.0.0 with: path: ./packages/**/dist - key: ${{ github.sha }}-base:18-test-lint + key: ${{ github.sha }}-base:build unit-test: name: Unit tests uses: ./.github/workflows/units-tests-reusable.yml - needs: install + needs: install-and-build with: ref: refs/pull/${{ github.event.pull_request.number }}/merge - cacheKey: ${{ github.sha }}-base:18-test-lint + cacheKey: ${{ github.sha }}-base:build lint: - name: Lint changes - runs-on: ubuntu-latest - needs: install - steps: - - uses: actions/checkout@v4.1.1 - with: - repository: n8n-io/n8n - ref: refs/pull/${{ github.event.pull_request.number }}/merge - - - run: corepack enable - - name: Use Node.js 18 - uses: actions/setup-node@v4.0.1 - with: - node-version: 18.x - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Restore cached build artifacts - uses: actions/cache/restore@v4.0.0 - with: - path: ./packages/**/dist - key: ${{ github.sha }}-base:18-test-lint - - - name: Lint - run: pnpm lint + name: Lint + uses: ./.github/workflows/linting-reusable.yml + needs: install-and-build + with: + ref: refs/pull/${{ github.event.pull_request.number }}/merge + cacheKey: ${{ github.sha }}-base:build diff --git a/.github/workflows/docker-base-image.yml b/.github/workflows/docker-base-image.yml index 1913828f78..c32160e763 100644 --- a/.github/workflows/docker-base-image.yml +++ b/.github/workflows/docker-base-image.yml @@ -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: diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index b5cc8a38c6..beea3bca27 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -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 }} diff --git a/.github/workflows/linting-reusable.yml b/.github/workflows/linting-reusable.yml new file mode 100644 index 0000000000..2650622bd0 --- /dev/null +++ b/.github/workflows/linting-reusable.yml @@ -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 diff --git a/.github/workflows/notify-pr-status.yml b/.github/workflows/notify-pr-status.yml new file mode 100644 index 0000000000..1169b02af8 --- /dev/null +++ b/.github/workflows/notify-pr-status.yml @@ -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) }} }' diff --git a/.github/workflows/release-create-pr.yml b/.github/workflows/release-create-pr.yml index 612ef87920..a17fa1bf89 100644 --- a/.github/workflows/release-create-pr.yml +++ b/.github/workflows/release-create-pr.yml @@ -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 diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index f63166b6b5..239c18b512 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -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 diff --git a/.github/workflows/release-push-to-channel.yml b/.github/workflows/release-push-to-channel.yml index aca481cfb4..3eda6d4ebb 100644 --- a/.github/workflows/release-push-to-channel.yml +++ b/.github/workflows/release-push-to-channel.yml @@ -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' diff --git a/.github/workflows/test-workflows.yml b/.github/workflows/test-workflows.yml index 7856b42f4f..8ba22b590c 100644 --- a/.github/workflows/test-workflows.yml +++ b/.github/workflows/test-workflows.yml @@ -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' diff --git a/.github/workflows/units-tests-reusable.yml b/.github/workflows/units-tests-reusable.yml index 386612678c..fe270b1de0 100644 --- a/.github/workflows/units-tests-reusable.yml +++ b/.github/workflows/units-tests-reusable.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 2d016d557a..e36cf95823 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.npmrc b/.npmrc index 0d9bdb6234..42d2964a04 100644 --- a/.npmrc +++ b/.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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d5c8d94cf..986b85b068 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7480e60131..4a67bf00ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js new file mode 100644 index 0000000000..36fc0af7e2 --- /dev/null +++ b/cypress/.eslintrc.js @@ -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', + }, +}; diff --git a/cypress/.gitignore b/cypress/.gitignore new file mode 100644 index 0000000000..a1d14ddebb --- /dev/null +++ b/cypress/.gitignore @@ -0,0 +1,3 @@ +videos/ +screenshots/ +downloads/ diff --git a/cypress/augmentation.d.ts b/cypress/augmentation.d.ts new file mode 100644 index 0000000000..334bc0e9f4 --- /dev/null +++ b/cypress/augmentation.d.ts @@ -0,0 +1,4 @@ +declare module 'cypress-otp' { + // eslint-disable-next-line import/no-default-export + export default function generateOTPToken(secret: string): string; +} diff --git a/cypress/composables/becomeTemplateCreatorCta.ts b/cypress/composables/becomeTemplateCreatorCta.ts index 55fc985c74..ca35e611d9 100644 --- a/cypress/composables/becomeTemplateCreatorCta.ts +++ b/cypress/composables/becomeTemplateCreatorCta.ts @@ -10,7 +10,7 @@ export const getCloseBecomeTemplateCreatorCtaButton = () => //#region Actions export const interceptCtaRequestWithResponse = (becomeCreator: boolean) => { - return cy.intercept('GET', `/rest/cta/become-creator`, { + return cy.intercept('GET', '/rest/cta/become-creator', { body: becomeCreator, }); }; diff --git a/cypress/composables/modals/chat-modal.ts b/cypress/composables/modals/chat-modal.ts index 31e139c93e..254d811a18 100644 --- a/cypress/composables/modals/chat-modal.ts +++ b/cypress/composables/modals/chat-modal.ts @@ -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() { diff --git a/cypress/composables/modals/credential-modal.ts b/cypress/composables/modals/credential-modal.ts index bfcbf89251..8ce6a86049 100644 --- a/cypress/composables/modals/credential-modal.ts +++ b/cypress/composables/modals/credential-modal.ts @@ -42,7 +42,7 @@ export function closeCredentialModal() { getCredentialModalCloseButton().click(); } -export function setCredentialValues(values: Record, save = true) { +export function setCredentialValues(values: Record, save = true) { Object.entries(values).forEach(([key, value]) => { setCredentialConnectionParameterInputByName(key, value); }); diff --git a/cypress/composables/modals/save-changes-modal.ts b/cypress/composables/modals/save-changes-modal.ts new file mode 100644 index 0000000000..d44b09bd46 --- /dev/null +++ b/cypress/composables/modals/save-changes-modal.ts @@ -0,0 +1,3 @@ +export function getSaveChangesModal() { + return cy.get('.el-overlay').contains('Save changes before leaving?'); +} diff --git a/cypress/composables/ndv.ts b/cypress/composables/ndv.ts index e2fc03d7af..c3fab73f8c 100644 --- a/cypress/composables/ndv.ts +++ b/cypress/composables/ndv.ts @@ -2,7 +2,7 @@ * Getters */ -import { getVisibleSelect } from "../utils"; +import { getVisibleSelect } from '../utils'; export function getCredentialSelect(eq = 0) { return cy.getByTestId('node-credentials-select').eq(eq); @@ -75,7 +75,7 @@ export function setParameterInputByName(name: string, value: string) { } export function toggleParameterCheckboxInputByName(name: string) { - getParameterInputByName(name).find('input[type="checkbox"]').realClick() + getParameterInputByName(name).find('input[type="checkbox"]').realClick(); } export function setParameterSelectByContent(name: string, content: string) { diff --git a/cypress/composables/projects.ts b/cypress/composables/projects.ts new file mode 100644 index 0000000000..6fa2c6f502 --- /dev/null +++ b/cypress/composables/projects.ts @@ -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(); + }, +}; diff --git a/cypress/composables/setup-workflow-credentials-button.ts b/cypress/composables/setup-workflow-credentials-button.ts index 6b1b9b69d4..8285454d83 100644 --- a/cypress/composables/setup-workflow-credentials-button.ts +++ b/cypress/composables/setup-workflow-credentials-button.ts @@ -2,4 +2,4 @@ * Getters */ -export const getSetupWorkflowCredentialsButton = () => cy.get(`button:contains("Set up template")`); +export const getSetupWorkflowCredentialsButton = () => cy.get('button:contains("Set up template")'); diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index 1aa469b194..b3d6f20c28 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -51,7 +51,7 @@ export function getNodeByName(name: string) { export function disableNode(name: string) { const target = getNodeByName(name); target.rightclick(name ? 'center' : 'topLeft', { force: true }); - cy.getByTestId(`context-menu-item-toggle_activation`).click(); + cy.getByTestId('context-menu-item-toggle_activation').click(); } export function getConnectionBySourceAndTarget(source: string, target: string) { diff --git a/cypress/constants.ts b/cypress/constants.ts index 9711a7fc02..8439952ac7 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -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'; diff --git a/cypress.config.js b/cypress/cypress.config.js similarity index 71% rename from cypress.config.js rename to cypress/cypress.config.js index f01672c6f9..63913af7f8 100644 --- a/cypress.config.js +++ b/cypress/cypress.config.js @@ -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', }, }); diff --git a/cypress/e2e/1-workflows.cy.ts b/cypress/e2e/1-workflows.cy.ts index 25f4f3cb0a..d01f046d75 100644 --- a/cypress/e2e/1-workflows.cy.ts +++ b/cypress/e2e/1-workflows.cy.ts @@ -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'); diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts index 3190987541..6453443376 100644 --- a/cypress/e2e/10-undo-redo.cy.ts +++ b/cypress/e2e/10-undo-redo.cy.ts @@ -1,5 +1,9 @@ -import { CODE_NODE_NAME, SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from './../constants'; -import { SCHEDULE_TRIGGER_NODE_NAME } from '../constants'; +import { + SCHEDULE_TRIGGER_NODE_NAME, + CODE_NODE_NAME, + SET_NODE_NAME, + EDIT_FIELDS_SET_NODE_NAME, +} from '../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { MessageBox as MessageBoxClass } from '../pages/modals/message-box'; import { NDV } from '../pages/ndv'; @@ -16,24 +20,6 @@ describe('Undo/Redo', () => { WorkflowPage.actions.visit(); }); - it('should undo/redo adding nodes', () => { - WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); - WorkflowPage.actions.hitUndo(); - WorkflowPage.getters.canvasNodes().should('have.have.length', 0); - WorkflowPage.actions.hitRedo(); - WorkflowPage.getters.canvasNodes().should('have.have.length', 1); - }); - - it('should undo/redo adding connected nodes', () => { - WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); - WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - WorkflowPage.actions.hitUndo(); - WorkflowPage.getters.canvasNodes().should('have.have.length', 1); - WorkflowPage.actions.hitRedo(); - WorkflowPage.getters.canvasNodes().should('have.have.length', 2); - WorkflowPage.getters.nodeConnections().should('have.length', 1); - }); - it('should undo/redo adding node in the middle', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); @@ -43,28 +29,28 @@ describe('Undo/Redo', () => { SET_NODE_NAME, ); WorkflowPage.actions.zoomToFit(); - WorkflowPage.getters - .canvasNodeByName('Code') - .should('have.css', 'left', '860px') - .should('have.css', 'top', '220px'); + WorkflowPage.getters.canvasNodeByName('Code').then(($codeNode) => { + const cssLeft = parseInt($codeNode.css('left')); + const cssTop = parseInt($codeNode.css('top')); - WorkflowPage.actions.hitUndo(); - WorkflowPage.getters.canvasNodes().should('have.have.length', 2); - WorkflowPage.getters.nodeConnections().should('have.length', 1); - WorkflowPage.actions.hitUndo(); - WorkflowPage.getters.canvasNodes().should('have.have.length', 1); - WorkflowPage.getters.nodeConnections().should('have.length', 0); - WorkflowPage.actions.hitRedo(); - WorkflowPage.getters.canvasNodes().should('have.have.length', 2); - WorkflowPage.getters.nodeConnections().should('have.length', 1); - WorkflowPage.actions.hitRedo(); - WorkflowPage.getters.canvasNodes().should('have.have.length', 3); - WorkflowPage.getters.nodeConnections().should('have.length', 2); - // Last node should be added back to original position - WorkflowPage.getters - .canvasNodeByName('Code') - .should('have.css', 'left', '860px') - .should('have.css', 'top', '220px'); + WorkflowPage.actions.hitUndo(); + WorkflowPage.getters.canvasNodes().should('have.have.length', 2); + WorkflowPage.getters.nodeConnections().should('have.length', 1); + WorkflowPage.actions.hitUndo(); + WorkflowPage.getters.canvasNodes().should('have.have.length', 1); + WorkflowPage.getters.nodeConnections().should('have.length', 0); + WorkflowPage.actions.hitRedo(); + WorkflowPage.getters.canvasNodes().should('have.have.length', 2); + WorkflowPage.getters.nodeConnections().should('have.length', 1); + WorkflowPage.actions.hitRedo(); + WorkflowPage.getters.canvasNodes().should('have.have.length', 3); + WorkflowPage.getters.nodeConnections().should('have.length', 2); + // Last node should be added back to original position + WorkflowPage.getters + .canvasNodeByName('Code') + .should('have.css', 'left', cssLeft + 'px') + .should('have.css', 'top', cssTop + 'px'); + }); }); it('should undo/redo deleting node using context menu', () => { @@ -118,8 +104,7 @@ describe('Undo/Redo', () => { WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); cy.get('body').type('{esc}'); cy.get('body').type('{esc}'); - WorkflowPage.actions.selectAll(); - cy.get('body').type('{backspace}'); + WorkflowPage.actions.hitDeleteAllNodes(); WorkflowPage.getters.canvasNodes().should('have.have.length', 0); WorkflowPage.actions.hitUndo(); WorkflowPage.getters.canvasNodes().should('have.have.length', 2); @@ -132,22 +117,30 @@ describe('Undo/Redo', () => { it('should undo/redo moving nodes', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true }); - WorkflowPage.getters - .canvasNodeByName('Code') - .should('have.css', 'left', '740px') - .should('have.css', 'top', '320px'); + WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => { + const initialPosition = $node.position(); + cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true }); - WorkflowPage.actions.hitUndo(); - WorkflowPage.getters - .canvasNodeByName('Code') - .should('have.css', 'left', '640px') - .should('have.css', 'top', '220px'); - WorkflowPage.actions.hitRedo(); - WorkflowPage.getters - .canvasNodeByName('Code') - .should('have.css', 'left', '740px') - .should('have.css', 'top', '320px'); + WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => { + const cssLeft = parseInt($node.css('left')); + const cssTop = parseInt($node.css('top')); + expect(cssLeft).to.be.greaterThan(initialPosition.left); + expect(cssTop).to.be.greaterThan(initialPosition.top); + }); + + WorkflowPage.actions.hitUndo(); + WorkflowPage.getters + .canvasNodeByName(CODE_NODE_NAME) + .should('have.css', 'left', `${initialPosition.left}px`) + .should('have.css', 'top', `${initialPosition.top}px`); + WorkflowPage.actions.hitRedo(); + WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => { + const cssLeft = parseInt($node.css('left')); + const cssTop = parseInt($node.css('top')); + expect(cssLeft).to.be.greaterThan(initialPosition.left); + expect(cssTop).to.be.greaterThan(initialPosition.top); + }); + }); }); it('should undo/redo deleting a connection using context menu', () => { @@ -204,7 +197,7 @@ describe('Undo/Redo', () => { WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); cy.get('body').type('{esc}'); cy.get('body').type('{esc}'); - WorkflowPage.actions.selectAll(); + WorkflowPage.actions.hitSelectAll(); WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.getters.disabledNodes().should('have.length', 2); WorkflowPage.actions.hitUndo(); @@ -213,21 +206,6 @@ describe('Undo/Redo', () => { WorkflowPage.getters.disabledNodes().should('have.length', 2); }); - it('should undo/redo renaming node using NDV', () => { - WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); - WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - WorkflowPage.getters.canvasNodes().last().click(); - cy.get('body').type('{enter}'); - ndv.actions.rename(CODE_NODE_NEW_NAME); - cy.get('body').type('{esc}'); - WorkflowPage.actions.hitUndo(); - cy.get('body').type('{esc}'); - WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).should('exist'); - WorkflowPage.actions.hitRedo(); - cy.get('body').type('{esc}'); - WorkflowPage.getters.canvasNodeByName(CODE_NODE_NEW_NAME).should('exist'); - }); - it('should undo/redo renaming node using keyboard shortcut', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); @@ -289,8 +267,12 @@ describe('Undo/Redo', () => { WorkflowPage.getters .canvasNodes() .first() - .should('have.css', 'left', `${initialPosition.left + 120}px`) - .should('have.css', 'top', `${initialPosition.top + 140}px`); + .then(($node) => { + const cssLeft = parseInt($node.css('left')); + const cssTop = parseInt($node.css('top')); + expect(cssLeft).to.be.greaterThan(initialPosition.left); + expect(cssTop).to.be.greaterThan(initialPosition.top); + }); // Delete the set node WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click(); @@ -319,8 +301,12 @@ describe('Undo/Redo', () => { WorkflowPage.getters .canvasNodes() .first() - .should('have.css', 'left', `${initialPosition.left + 120}px`) - .should('have.css', 'top', `${initialPosition.top + 140}px`); + .then(($node) => { + const cssLeft = parseInt($node.css('left')); + const cssTop = parseInt($node.css('top')); + expect(cssLeft).to.be.greaterThan(initialPosition.left); + expect(cssTop).to.be.greaterThan(initialPosition.top); + }); // Third redo: Should delete the Set node WorkflowPage.actions.hitRedo(); WorkflowPage.getters.canvasNodes().should('have.length', 3); @@ -337,9 +323,6 @@ describe('Undo/Redo', () => { WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.nodeConnections().should('have.length', 1); cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1); - cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')) - .should('have.css', 'left', `637px`) - .should('have.css', 'top', `501px`); cy.fixture('Test_workflow_form_switch.json').then((data) => { cy.get('body').paste(JSON.stringify(data)); @@ -352,9 +335,6 @@ describe('Undo/Redo', () => { WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.nodeConnections().should('have.length', 1); cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1); - cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')) - .should('have.css', 'left', `637px`) - .should('have.css', 'top', `501px`); }); it('should not undo/redo when NDV or a modal is open', () => { diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts index 1d95b2db8e..143648ce1b 100644 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ b/cypress/e2e/11-inline-expression-editor.cy.ts @@ -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"); diff --git a/cypress/e2e/12-canvas-actions.cy.ts b/cypress/e2e/12-canvas-actions.cy.ts index 3c517b6c98..9b05cb84d4 100644 --- a/cypress/e2e/12-canvas-actions.cy.ts +++ b/cypress/e2e/12-canvas-actions.cy.ts @@ -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); diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index c1e06c107d..325e509e79 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -1,3 +1,5 @@ +import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; +import { NDV, WorkflowExecutionsTab } from '../pages'; import { MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME, @@ -7,8 +9,6 @@ import { SWITCH_NODE_NAME, MERGE_NODE_NAME, } from './../constants'; -import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; -import { NDV, WorkflowExecutionsTab } from '../pages'; const WorkflowPage = new WorkflowPageClass(); const ExecutionsTab = new WorkflowExecutionsTab(); @@ -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); }); diff --git a/cypress/e2e/13-pinning.cy.ts b/cypress/e2e/13-pinning.cy.ts index a9ccc78818..4558c44bca 100644 --- a/cypress/e2e/13-pinning.cy.ts +++ b/cypress/e2e/13-pinning.cy.ts @@ -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}}]'; diff --git a/cypress/e2e/1338-ADO-ndv-missing-input-panel.cy.ts b/cypress/e2e/1338-ADO-ndv-missing-input-panel.cy.ts index 046d4d809d..dfc635404d 100644 --- a/cypress/e2e/1338-ADO-ndv-missing-input-panel.cy.ts +++ b/cypress/e2e/1338-ADO-ndv-missing-input-panel.cy.ts @@ -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'); diff --git a/cypress/e2e/14-data-transformation-expressions.cy.ts b/cypress/e2e/14-data-transformation-expressions.cy.ts index 21c958d691..be9f08a678 100644 --- a/cypress/e2e/14-data-transformation-expressions.cy.ts +++ b/cypress/e2e/14-data-transformation-expressions.cy.ts @@ -1,5 +1,4 @@ import { WorkflowPage, NDV } from '../pages'; -import { getVisibleSelect } from '../utils'; const wf = new WorkflowPage(); const ndv = new NDV(); diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index bcfeb71ec7..2a33aee5c0 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -1,10 +1,10 @@ +import { WorkflowPage, NDV } from '../pages'; +import { getVisibleSelect } from '../utils'; import { MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME, SCHEDULE_TRIGGER_NODE_NAME, } from './../constants'; -import { WorkflowPage, NDV } from '../pages'; -import { getVisibleSelect } from '../utils'; const workflowPage = new WorkflowPage(); const ndv = new NDV(); @@ -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)'); }); }); diff --git a/cypress/e2e/15-scheduler-node.cy.ts b/cypress/e2e/15-scheduler-node.cy.ts index 0021455619..65f0904543 100644 --- a/cypress/e2e/15-scheduler-node.cy.ts +++ b/cypress/e2e/15-scheduler-node.cy.ts @@ -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'); - }); - }); - }); - }); }); diff --git a/cypress/e2e/16-webhook-node.cy.ts b/cypress/e2e/16-webhook-node.cy.ts index 560fc41056..791a704174 100644 --- a/cypress/e2e/16-webhook-node.cy.ts +++ b/cypress/e2e/16-webhook-node.cy.ts @@ -1,5 +1,5 @@ +import { nanoid } from 'nanoid'; import { WorkflowPage, NDV, CredentialsModal } from '../pages'; -import { v4 as uuid } from 'uuid'; import { cowBase64 } from '../support/binaryTestFiles'; import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants'; import { getVisibleSelect } from '../utils'; @@ -75,34 +75,34 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => { } }; -describe('Webhook Trigger node', async () => { +describe('Webhook Trigger node', () => { beforeEach(() => { workflowPage.actions.visit(); }); it('should listen for a GET request', () => { - simpleWebhookCall({ method: 'GET', webhookPath: uuid(), executeNow: true }); + simpleWebhookCall({ method: 'GET', webhookPath: nanoid(), executeNow: true }); }); it('should listen for a POST request', () => { - simpleWebhookCall({ method: 'POST', webhookPath: uuid(), executeNow: true }); + simpleWebhookCall({ method: 'POST', webhookPath: nanoid(), executeNow: true }); }); it('should listen for a DELETE request', () => { - simpleWebhookCall({ method: 'DELETE', webhookPath: uuid(), executeNow: true }); + simpleWebhookCall({ method: 'DELETE', webhookPath: nanoid(), executeNow: true }); }); it('should listen for a HEAD request', () => { - simpleWebhookCall({ method: 'HEAD', webhookPath: uuid(), executeNow: true }); + simpleWebhookCall({ method: 'HEAD', webhookPath: nanoid(), executeNow: true }); }); it('should listen for a PATCH request', () => { - simpleWebhookCall({ method: 'PATCH', webhookPath: uuid(), executeNow: true }); + simpleWebhookCall({ method: 'PATCH', webhookPath: nanoid(), executeNow: true }); }); it('should listen for a PUT request', () => { - simpleWebhookCall({ method: 'PUT', webhookPath: uuid(), executeNow: true }); + simpleWebhookCall({ method: 'PUT', webhookPath: nanoid(), executeNow: true }); }); it('should listen for a GET request and respond with Respond to Webhook node', () => { - const webhookPath = uuid(); + const webhookPath = nanoid(); simpleWebhookCall({ method: 'GET', webhookPath, @@ -121,14 +121,16 @@ describe('Webhook Trigger node', async () => { workflowPage.actions.executeWorkflow(); cy.wait(waitForWebhook); - cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { - expect(response.status).to.eq(200); - expect(response.body.MyValue).to.eq(1234); - }); + cy.request<{ MyValue: number }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then( + (response) => { + expect(response.status).to.eq(200); + expect(response.body.MyValue).to.eq(1234); + }, + ); }); it('should listen for a GET request and respond custom status code 201', () => { - const webhookPath = uuid(); + const webhookPath = nanoid(); simpleWebhookCall({ method: 'GET', webhookPath, @@ -145,7 +147,7 @@ describe('Webhook Trigger node', async () => { }); it('should listen for a GET request and respond with last node', () => { - const webhookPath = uuid(); + const webhookPath = nanoid(); simpleWebhookCall({ method: 'GET', webhookPath, @@ -161,14 +163,16 @@ describe('Webhook Trigger node', async () => { workflowPage.actions.executeWorkflow(); cy.wait(waitForWebhook); - cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { - expect(response.status).to.eq(200); - expect(response.body.MyValue).to.eq(1234); - }); + cy.request<{ MyValue: number }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then( + (response) => { + expect(response.status).to.eq(200); + expect(response.body.MyValue).to.eq(1234); + }, + ); }); it('should listen for a GET request and respond with last node binary data', () => { - const webhookPath = uuid(); + const webhookPath = nanoid(); simpleWebhookCall({ method: 'GET', webhookPath, @@ -200,14 +204,16 @@ describe('Webhook Trigger node', async () => { workflowPage.actions.executeWorkflow(); cy.wait(waitForWebhook); - cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { - expect(response.status).to.eq(200); - expect(Object.keys(response.body).includes('data')).to.be.true; - }); + cy.request<{ data: unknown }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then( + (response) => { + expect(response.status).to.eq(200); + expect(Object.keys(response.body).includes('data')).to.be.true; + }, + ); }); it('should listen for a GET request and respond with an empty body', () => { - const webhookPath = uuid(); + const webhookPath = nanoid(); simpleWebhookCall({ method: 'GET', webhookPath, @@ -217,14 +223,16 @@ describe('Webhook Trigger node', async () => { }); ndv.actions.execute(); cy.wait(waitForWebhook); - cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { - expect(response.status).to.eq(200); - expect(response.body.MyValue).to.be.undefined; - }); + cy.request<{ MyValue: unknown }>('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then( + (response) => { + expect(response.status).to.eq(200); + expect(response.body.MyValue).to.be.undefined; + }, + ); }); it('should listen for a GET request with Basic Authentication', () => { - const webhookPath = uuid(); + const webhookPath = nanoid(); simpleWebhookCall({ method: 'GET', webhookPath, @@ -267,7 +275,7 @@ describe('Webhook Trigger node', async () => { }); it('should listen for a GET request with Header Authentication', () => { - const webhookPath = uuid(); + const webhookPath = nanoid(); simpleWebhookCall({ method: 'GET', webhookPath, diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts index 71f41250ec..54c5e6efe2 100644 --- a/cypress/e2e/17-sharing.cy.ts +++ b/cypress/e2e/17-sharing.cy.ts @@ -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); + }); +}); diff --git a/cypress/e2e/17-workflow-tags.cy.ts b/cypress/e2e/17-workflow-tags.cy.ts index dea45d1a9a..88a2c9973d 100644 --- a/cypress/e2e/17-workflow-tags.cy.ts +++ b/cypress/e2e/17-workflow-tags.cy.ts @@ -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); }); }); diff --git a/cypress/e2e/18-user-management.cy.ts b/cypress/e2e/18-user-management.cy.ts index 0dc1264739..b53b0fdf53 100644 --- a/cypress/e2e/18-user-management.cy.ts +++ b/cypress/e2e/18-user-management.cy.ts @@ -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, diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index 1855bdb43b..804e81d4e6 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -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; + 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; + 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; + 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; + 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‘'); }); }); diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index 008758aef2..ffecd51959 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -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(); diff --git a/cypress/e2e/20-workflow-executions.cy.ts b/cypress/e2e/20-workflow-executions.cy.ts index 37036a7971..59f08c570b 100644 --- a/cypress/e2e/20-workflow-executions.cy.ts +++ b/cypress/e2e/20-workflow-executions.cy.ts @@ -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'); +}; diff --git a/cypress/e2e/21-community-nodes.cy.ts b/cypress/e2e/21-community-nodes.cy.ts index 39f572ba5c..bf88d3d24c 100644 --- a/cypress/e2e/21-community-nodes.cy.ts +++ b/cypress/e2e/21-community-nodes.cy.ts @@ -1,3 +1,4 @@ +import type { ICredentialType } from 'n8n-workflow'; import { NodeCreator } from '../pages/features/node-creator'; import CustomNodeFixture from '../fixtures/Custom_node.json'; import { CredentialsModal, WorkflowPage } from '../pages'; @@ -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'); + }); +}); diff --git a/cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts b/cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts new file mode 100644 index 0000000000..e26a7acb82 --- /dev/null +++ b/cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts @@ -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'); + }); +}); diff --git a/cypress/e2e/2111-ado-support-pinned-data-in-expressions.cy.ts b/cypress/e2e/2111-ado-support-pinned-data-in-expressions.cy.ts new file mode 100644 index 0000000000..6d2da55b32 --- /dev/null +++ b/cypress/e2e/2111-ado-support-pinned-data-in-expressions.cy.ts @@ -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]', + ); + }); +}); diff --git a/cypress/e2e/2230-ADO-ndv-reset-data-pagination.cy.ts b/cypress/e2e/2230-ADO-ndv-reset-data-pagination.cy.ts new file mode 100644 index 0000000000..995423bc3a --- /dev/null +++ b/cypress/e2e/2230-ADO-ndv-reset-data-pagination.cy.ts @@ -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'); + }); +}); diff --git a/cypress/e2e/23-variables.cy.ts b/cypress/e2e/23-variables.cy.ts index ce6a49fb99..c481f25128 100644 --- a/cypress/e2e/23-variables.cy.ts +++ b/cypress/e2e/23-variables.cy.ts @@ -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', () => { diff --git a/cypress/e2e/24-ndv-paired-item.cy.ts b/cypress/e2e/24-ndv-paired-item.cy.ts index 1b2b4f1efe..8324144343 100644 --- a/cypress/e2e/24-ndv-paired-item.cy.ts +++ b/cypress/e2e/24-ndv-paired-item.cy.ts @@ -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)); diff --git a/cypress/e2e/25-stickies.cy.ts b/cypress/e2e/25-stickies.cy.ts index 4cbad810f9..14c176f17b 100644 --- a/cypress/e2e/25-stickies.cy.ts +++ b/cypress/e2e/25-stickies.cy.ts @@ -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); diff --git a/cypress/e2e/26-resource-locator.cy.ts b/cypress/e2e/26-resource-locator.cy.ts index 9cea4e25a3..6e431690ad 100644 --- a/cypress/e2e/26-resource-locator.cy.ts +++ b/cypress/e2e/26-resource-locator.cy.ts @@ -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(); diff --git a/cypress/e2e/27-cloud.cy.ts b/cypress/e2e/27-cloud.cy.ts index 965bc5bccf..dd0d3b06ba 100644 --- a/cypress/e2e/27-cloud.cy.ts +++ b/cypress/e2e/27-cloud.cy.ts @@ -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'); }); diff --git a/cypress/e2e/27-two-factor-authentication.cy.ts b/cypress/e2e/27-two-factor-authentication.cy.ts index 91f6ca57a2..21319dd79b 100644 --- a/cypress/e2e/27-two-factor-authentication.cy.ts +++ b/cypress/e2e/27-two-factor-authentication.cy.ts @@ -1,9 +1,9 @@ -import { MainSidebar } from './../pages/sidebar/main-sidebar'; +import generateOTPToken from 'cypress-otp'; import { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../constants'; import { SigninPage } from '../pages'; import { PersonalSettingsPage } from '../pages/settings-personal'; import { MfaLoginPage } from '../pages/mfa-login'; -import generateOTPToken from 'cypress-otp'; +import { MainSidebar } from './../pages/sidebar/main-sidebar'; const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD'; @@ -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'); diff --git a/cypress/e2e/28-debug.cy.ts b/cypress/e2e/28-debug.cy.ts index 955d33ce28..5d2bd76cac 100644 --- a/cypress/e2e/28-debug.cy.ts +++ b/cypress/e2e/28-debug.cy.ts @@ -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(); diff --git a/cypress/e2e/29-sql-editor.cy.ts b/cypress/e2e/29-sql-editor.cy.ts deleted file mode 100644 index 86299d5f67..0000000000 --- a/cypress/e2e/29-sql-editor.cy.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/cypress/e2e/29-templates.cy.ts b/cypress/e2e/29-templates.cy.ts index 34762b12fc..5cc6657416 100644 --- a/cypress/e2e/29-templates.cy.ts +++ b/cypress/e2e/29-templates.cy.ts @@ -1,46 +1,246 @@ import { TemplatesPage } from '../pages/templates'; +import { WorkflowPage } from '../pages/workflow'; import { WorkflowsPage } from '../pages/workflows'; import { MainSidebar } from '../pages/sidebar/main-sidebar'; +import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json'; +import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json'; const templatesPage = new TemplatesPage(); +const workflowPage = new WorkflowPage(); const workflowsPage = new WorkflowsPage(); const mainSidebar = new MainSidebar(); describe('Workflow templates', () => { - beforeEach(() => { - cy.intercept('GET', '**/rest/settings', (req) => { - // Disable cache - delete req.headers['if-none-match'] - req.reply((res) => { - if (res.body.data) { - // Disable custom templates host if it has been overridden by another intercept - res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' }; - } - }); - }).as('settingsRequest'); - }); - - it('Opens website when clicking templates sidebar link', () => { - cy.visit(workflowsPage.url); - mainSidebar.getters.menuItem('Templates').should('be.visible'); - // Templates should be a link to the website - mainSidebar.getters.templates().parent('a').should('have.attr', 'href').and('include', 'https://n8n.io/workflows'); - // Link should contain instance address and n8n version - mainSidebar.getters.templates().parent('a').then(($a) => { - const href = $a.attr('href'); - const params = new URLSearchParams(href); - // Link should have all mandatory parameters expected on the website - expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include(window.location.origin); - expect(params.get('utm_n8n_version')).to.match(/[0-9]+\.[0-9]+\.[0-9]+/); - expect(params.get('utm_awc')).to.match(/[0-9]+/); + const mockTemplateHost = (host: string) => { + cy.overrideSettings({ + templates: { enabled: true, host }, + }); + }; + + describe('For api.n8n.io', () => { + beforeEach(() => { + mockTemplateHost('https://api.n8n.io/api/'); + }); + + it('Opens website when clicking templates sidebar link', () => { + cy.visit(workflowsPage.url); + mainSidebar.getters.templates().should('be.visible'); + // Templates should be a link to the website + mainSidebar.getters + .templates() + .parent('a') + .should('have.attr', 'href') + .and('include', 'https://n8n.io/workflows'); + // Link should contain instance address and n8n version + mainSidebar.getters + .templates() + .parent('a') + .then(($a) => { + const href = $a.attr('href'); + const params = new URLSearchParams(href); + // Link should have all mandatory parameters expected on the website + expect(decodeURIComponent(`${params.get('utm_instance')}`)).to.include( + window.location.origin, + ); + expect(params.get('utm_n8n_version')).to.match(/[0-9]+\.[0-9]+\.[0-9]+/); + expect(params.get('utm_awc')).to.match(/[0-9]+/); + }); + mainSidebar.getters.templates().parent('a').should('have.attr', 'target', '_blank'); + }); + + it('Redirects to website when visiting templates page directly', () => { + cy.intercept( + { + hostname: 'n8n.io', + pathname: '/workflows', + }, + 'Mock Template Page', + ).as('templatesPage'); + + cy.visit(templatesPage.url); + + cy.wait('@templatesPage'); }); - mainSidebar.getters.templates().parent('a').should('have.attr', 'target', '_blank'); }); - it('Redirects to website when visiting templates page directly', () => { - cy.visit(templatesPage.url); - cy.origin('https://n8n.io', () => { - cy.url().should('include', 'https://n8n.io/workflows'); - }) + describe('For a custom template host', () => { + const hostname = 'random.domain'; + const categories = [ + { id: 1, name: 'Engineering' }, + { id: 2, name: 'Finance' }, + { id: 3, name: 'Sales' }, + ]; + const collections = [ + { + id: 1, + name: 'Test Collection', + workflows: [{ id: 1 }], + nodes: [], + }, + ]; + + beforeEach(() => { + cy.intercept({ hostname, pathname: '/api/health' }, { status: 'OK' }); + cy.intercept({ hostname, pathname: '/api/templates/categories' }, { categories }); + cy.intercept( + { hostname, pathname: '/api/templates/collections', query: { category: '**' } }, + (req) => { + req.reply({ collections: req.query['category[]'] === '3' ? [] : collections }); + }, + ); + cy.intercept( + { hostname, pathname: '/api/templates/search', query: { category: '**' } }, + (req) => { + const fixture = + req.query.category === 'Sales' + ? 'templates_search/sales_templates_search_response.json' + : 'templates_search/all_templates_search_response.json'; + req.reply({ statusCode: 200, fixture }); + }, + ); + + cy.intercept( + { hostname, pathname: '/api/workflows/templates/1' }, + { + statusCode: 200, + body: { + id: 1, + name: OnboardingWorkflow.name, + workflow: OnboardingWorkflow, + }, + }, + ).as('getTemplate'); + + cy.intercept( + { hostname, pathname: '/api/templates/workflows/1' }, + { + statusCode: 200, + body: WorkflowTemplate, + }, + ).as('getTemplatePreview'); + + mockTemplateHost(`https://${hostname}/api`); + }); + + it('can open onboarding flow', () => { + templatesPage.actions.openOnboardingFlow(); + cy.url().should('match', /.*\/workflow\/.*?onboardingId=1$/); + + workflowPage.actions.shouldHaveWorkflowName('Demo: ' + OnboardingWorkflow.name); + workflowPage.getters.canvasNodes().should('have.length', 4); + workflowPage.getters.stickies().should('have.length', 1); + workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon'); + }); + + it('can import template', () => { + templatesPage.actions.importTemplate(); + cy.url().should('include', '/workflow/new?templateId=1'); + + workflowPage.getters.canvasNodes().should('have.length', 4); + workflowPage.getters.stickies().should('have.length', 1); + workflowPage.actions.shouldHaveWorkflowName(OnboardingWorkflow.name); + }); + + it('should save template id with the workflow', () => { + templatesPage.actions.importTemplate(); + + cy.visit(templatesPage.url); + cy.get('.el-skeleton.n8n-loading').should('not.exist'); + templatesPage.getters.firstTemplateCard().should('exist'); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + templatesPage.getters.firstTemplateCard().click(); + cy.url().should('include', '/templates/1'); + cy.wait('@getTemplatePreview'); + + templatesPage.getters.useTemplateButton().click(); + cy.url().should('include', '/workflow/new'); + workflowPage.actions.saveWorkflowOnButtonClick(); + + workflowPage.actions.hitSelectAll(); + workflowPage.actions.hitCopy(); + + cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); + // Check workflow JSON by copying it to clipboard + cy.readClipboard().then((workflowJSON) => { + expect(workflowJSON).to.contain('"templateId": "1"'); + }); + }); + + it('can open template with images and hides workflow screenshots', () => { + cy.visit(`${templatesPage.url}/1`); + cy.wait('@getTemplatePreview'); + + templatesPage.getters.description().find('img').should('have.length', 1); + }); + + it('renders search elements correctly', () => { + cy.visit(templatesPage.url); + templatesPage.getters.searchInput().should('exist'); + templatesPage.getters.allCategoriesFilter().should('exist'); + templatesPage.getters.categoryFilters().should('have.length.greaterThan', 1); + templatesPage.getters.templateCards().should('have.length.greaterThan', 0); + }); + + it('can filter templates by category', () => { + cy.visit(templatesPage.url); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + templatesPage.getters.categoryFilter('sales').should('exist'); + let initialTemplateCount = 0; + let initialCollectionCount = 0; + + templatesPage.getters.templateCountLabel().then(($el) => { + initialTemplateCount = parseInt($el.text().replace(/\D/g, ''), 10); + templatesPage.getters.collectionCountLabel().then(($el1) => { + initialCollectionCount = parseInt($el1.text().replace(/\D/g, ''), 10); + + templatesPage.getters.categoryFilter('sales').click(); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + + // Should have less templates and collections after selecting a category + templatesPage.getters.templateCountLabel().should(($el2) => { + expect(parseInt($el2.text().replace(/\D/g, ''), 10)).to.be.lessThan( + initialTemplateCount, + ); + }); + templatesPage.getters.collectionCountLabel().should(($el2) => { + expect(parseInt($el2.text().replace(/\D/g, ''), 10)).to.be.lessThan( + initialCollectionCount, + ); + }); + }); + }); + }); + + it('should preserve search query in URL', () => { + cy.visit(templatesPage.url); + templatesPage.getters.templatesLoadingContainer().should('not.exist'); + templatesPage.getters.categoryFilter('sales').should('exist'); + templatesPage.getters.categoryFilter('sales').click(); + templatesPage.getters.searchInput().type('auto'); + + cy.url().should('include', '?categories='); + cy.url().should('include', '&search='); + + cy.reload(); + + // Should preserve search query in URL + cy.url().should('include', '?categories='); + cy.url().should('include', '&search='); + + // Sales category should still be selected + templatesPage.getters + .categoryFilter('sales') + .find('label') + .should('have.class', 'is-checked'); + // Search input should still have the search query + templatesPage.getters.searchInput().should('have.value', 'auto'); + // Sales checkbox should be pushed to the top + templatesPage.getters + .categoryFilters() + .eq(1) + .then(($el) => { + expect($el.text()).to.equal('Sales'); + }); + }); }); }); diff --git a/cypress/e2e/30-editor-after-route-changes.cy.ts b/cypress/e2e/30-editor-after-route-changes.cy.ts index 727078e735..6598780676 100644 --- a/cypress/e2e/30-editor-after-route-changes.cy.ts +++ b/cypress/e2e/30-editor-after-route-changes.cy.ts @@ -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(); diff --git a/cypress/e2e/30-langchain.cy.ts b/cypress/e2e/30-langchain.cy.ts index 9536b3cf60..c1409a34f3 100644 --- a/cypress/e2e/30-langchain.cy.ts +++ b/cypress/e2e/30-langchain.cy.ts @@ -1,17 +1,4 @@ -import { - AGENT_NODE_NAME, - MANUAL_CHAT_TRIGGER_NODE_NAME, - AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, - MANUAL_TRIGGER_NODE_NAME, - AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME, - AI_TOOL_CALCULATOR_NODE_NAME, - AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME, - AI_TOOL_CODE_NODE_NAME, - AI_TOOL_WIKIPEDIA_NODE_NAME, - BASIC_LLM_CHAIN_NODE_NAME, - EDIT_FIELDS_SET_NODE_NAME, -} from './../constants'; -import { createMockNodeExecutionData, runMockWorkflowExcution } from '../utils'; +import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils'; import { addLanguageModelNodeToParent, addMemoryNodeToParent, @@ -42,6 +29,19 @@ import { getManualChatModalLogsTree, sendManualChatMessage, } from '../composables/modals/chat-modal'; +import { + AGENT_NODE_NAME, + MANUAL_CHAT_TRIGGER_NODE_NAME, + AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, + MANUAL_TRIGGER_NODE_NAME, + AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME, + AI_TOOL_CALCULATOR_NODE_NAME, + AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME, + AI_TOOL_CODE_NODE_NAME, + AI_TOOL_WIKIPEDIA_NODE_NAME, + BASIC_LLM_CHAIN_NODE_NAME, + EDIT_FIELDS_SET_NODE_NAME, +} from './../constants'; describe('Langchain Integration', () => { beforeEach(() => { @@ -149,7 +149,7 @@ describe('Langchain Integration', () => { const outputMessage = 'Hi there! How can I assist you today?'; clickExecuteNode(); - runMockWorkflowExcution({ + runMockWorkflowExecution({ trigger: () => sendManualChatMessage(inputMessage), runData: [ createMockNodeExecutionData(BASIC_LLM_CHAIN_NODE_NAME, { @@ -189,7 +189,7 @@ describe('Langchain Integration', () => { const outputMessage = 'Hi there! How can I assist you today?'; clickExecuteNode(); - runMockWorkflowExcution({ + runMockWorkflowExecution({ trigger: () => sendManualChatMessage(inputMessage), runData: [ createMockNodeExecutionData(AGENT_NODE_NAME, { @@ -230,7 +230,7 @@ describe('Langchain Integration', () => { const inputMessage = 'Hello!'; const outputMessage = 'Hi there! How can I assist you today?'; - runMockWorkflowExcution({ + runMockWorkflowExecution({ trigger: () => { sendManualChatMessage(inputMessage); }, diff --git a/cypress/e2e/30-workflow-filters.cy.ts b/cypress/e2e/30-workflow-filters.cy.ts deleted file mode 100644 index 634f95ba06..0000000000 --- a/cypress/e2e/30-workflow-filters.cy.ts +++ /dev/null @@ -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(); - }); - }); -}); diff --git a/cypress/e2e/31-demo.cy.ts b/cypress/e2e/31-demo.cy.ts index d9397ace4e..eed3198c83 100644 --- a/cypress/e2e/31-demo.cy.ts +++ b/cypress/e2e/31-demo.cy.ts @@ -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'); }); }); diff --git a/cypress/e2e/32-node-io-filter.cy.ts b/cypress/e2e/32-node-io-filter.cy.ts index 3f1ffdf005..6613fa1ff2 100644 --- a/cypress/e2e/32-node-io-filter.cy.ts +++ b/cypress/e2e/32-node-io-filter.cy.ts @@ -6,7 +6,7 @@ const ndv = new NDV(); describe('Node IO Filter', () => { beforeEach(() => { workflowPage.actions.visit(); - cy.createFixtureWorkflow('Node_IO_filter.json', `Node IO filter`); + cy.createFixtureWorkflow('Node_IO_filter.json', 'Node IO filter'); workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.executeWorkflow(); }); @@ -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'); diff --git a/cypress/e2e/32-worker-view.cy.ts b/cypress/e2e/32-worker-view.cy.ts index ba3edbe4c9..de9afc2891 100644 --- a/cypress/e2e/32-worker-view.cy.ts +++ b/cypress/e2e/32-worker-view.cy.ts @@ -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'); }); diff --git a/cypress/e2e/33-settings-personal.cy.ts b/cypress/e2e/33-settings-personal.cy.ts index 9257bee22d..73dc7476b8 100644 --- a/cypress/e2e/33-settings-personal.cy.ts +++ b/cypress/e2e/33-settings-personal.cy.ts @@ -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(); }); }); }); diff --git a/cypress/e2e/34-template-credentials-setup.cy.ts b/cypress/e2e/34-template-credentials-setup.cy.ts index 7de435c4fa..c5d9f2643f 100644 --- a/cypress/e2e/34-template-credentials-setup.cy.ts +++ b/cypress/e2e/34-template-credentials-setup.cy.ts @@ -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'); diff --git a/cypress/e2e/35-admin-user-smoke-test.cy.ts b/cypress/e2e/35-admin-user-smoke-test.cy.ts index 05e70aa339..c8585118e7 100644 --- a/cypress/e2e/35-admin-user-smoke-test.cy.ts +++ b/cypress/e2e/35-admin-user-smoke-test.cy.ts @@ -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); diff --git a/cypress/e2e/36-suggested-templates.cy.ts b/cypress/e2e/36-suggested-templates.cy.ts deleted file mode 100644 index b788796e45..0000000000 --- a/cypress/e2e/36-suggested-templates.cy.ts +++ /dev/null @@ -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; -}; - -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'); - }); -}); diff --git a/cypress/e2e/36-versions.cy.ts b/cypress/e2e/36-versions.cy.ts index 2d93223ebb..1d4fc51808 100644 --- a/cypress/e2e/36-versions.cy.ts +++ b/cypress/e2e/36-versions.cy.ts @@ -1,4 +1,3 @@ -import { INSTANCE_OWNER } from '../constants'; import { WorkflowsPage } from '../pages/workflows'; import { closeVersionUpdatesPanel, @@ -11,52 +10,18 @@ const workflowsPage = new WorkflowsPage(); describe('Versions', () => { it('should open updates panel', () => { - cy.intercept('GET', '/rest/settings', (req) => { - req.continue((res) => { - if (res.body.hasOwnProperty('data')) { - res.body.data = { - ...res.body.data, - releaseChannel: 'stable', - versionCli: '1.0.0', - versionNotifications: { - enabled: true, - endpoint: 'https://api.n8n.io/api/versions/', - infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html', - }, - }; - } - }); - }).as('settings'); - - cy.intercept('GET', 'https://api.n8n.io/api/versions/1.0.0', [ - { - name: '1.3.1', - createdAt: '2023-08-18T11:53:12.857Z', - hasSecurityIssue: null, - hasSecurityFix: null, - securityIssueFixVersion: null, - hasBreakingChange: null, - documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131', - nodes: [], - description: 'Includes bug fixes', + cy.overrideSettings({ + releaseChannel: 'stable', + versionCli: '1.0.0', + versionNotifications: { + enabled: true, + endpoint: 'https://api.n8n.io/api/versions/', + infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html', }, - { - name: '1.0.5', - createdAt: '2023-07-24T10:54:56.097Z', - hasSecurityIssue: false, - hasSecurityFix: null, - securityIssueFixVersion: null, - hasBreakingChange: true, - documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104', - nodes: [], - description: 'Includes core functionality and bug fixes', - }, - ]); - - cy.signin(INSTANCE_OWNER); + }); cy.visit(workflowsPage.url); - cy.wait('@settings'); + cy.wait('@loadSettings'); getVersionUpdatesPanelOpenButton().should('contain', '2 updates'); openVersionUpdatesPanel(); diff --git a/cypress/e2e/38-custom-template-repository.cy.ts b/cypress/e2e/38-custom-template-repository.cy.ts deleted file mode 100644 index 067bf19b15..0000000000 --- a/cypress/e2e/38-custom-template-repository.cy.ts +++ /dev/null @@ -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'); - }); - }); -}); diff --git a/cypress/e2e/39-import-workflow.cy.ts b/cypress/e2e/39-import-workflow.cy.ts index 831228fba3..f92790eb3b 100644 --- a/cypress/e2e/39-import-workflow.cy.ts +++ b/cypress/e2e/39-import-workflow.cy.ts @@ -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); diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts new file mode 100644 index 0000000000..94a6384233 --- /dev/null +++ b/cypress/e2e/39-projects.cy.ts @@ -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); + }); + }); +}); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 6955c95463..bb47ef4765 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -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(); diff --git a/cypress/e2e/40-manual-partial-execution.cy.ts b/cypress/e2e/40-manual-partial-execution.cy.ts new file mode 100644 index 0000000000..5fe31b56ad --- /dev/null +++ b/cypress/e2e/40-manual-partial-execution.cy.ts @@ -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 + }); +}); diff --git a/cypress/e2e/41-editors.cy.ts b/cypress/e2e/41-editors.cy.ts new file mode 100644 index 0000000000..f7ad8129b3 --- /dev/null +++ b/cypress/e2e/41-editors.cy.ts @@ -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 = '

Test'; + const TEST_ELEMENT_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'); + }); + }); +}); diff --git a/cypress/e2e/42-nps-survey.cy.ts b/cypress/e2e/42-nps-survey.cy.ts new file mode 100644 index 0000000000..e06fe43ba8 --- /dev/null +++ b/cypress/e2e/42-nps-survey.cy.ts @@ -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'); + }); +}); diff --git a/cypress/e2e/43-oauth-flow.cy.ts b/cypress/e2e/43-oauth-flow.cy.ts new file mode 100644 index 0000000000..300a202540 --- /dev/null +++ b/cypress/e2e/43-oauth-flow.cy.ts @@ -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'); + }); +}); diff --git a/cypress/e2e/44-routing.cy.ts b/cypress/e2e/44-routing.cy.ts new file mode 100644 index 0000000000..67a092235b --- /dev/null +++ b/cypress/e2e/44-routing.cy.ts @@ -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'); + }); +}); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 6103dbbc03..24296ddca8 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -1,6 +1,4 @@ -import { v4 as uuid } from 'uuid'; -import { getVisibleSelect } from '../utils'; -import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../constants'; +import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants'; import { NDV, WorkflowPage } from '../pages'; import { NodeCreator } from '../pages/features/node-creator'; import { clickCreateNewCredential } from '../composables/ndv'; @@ -12,7 +10,7 @@ const ndv = new NDV(); describe('NDV', () => { beforeEach(() => { workflowPage.actions.visit(); - workflowPage.actions.renameWorkflow(uuid()); + workflowPage.actions.renameWithUniqueName(); workflowPage.actions.saveWorkflowOnButtonClick(); }); @@ -24,6 +22,14 @@ describe('NDV', () => { ndv.getters.container().should('not.be.visible'); }); + it('should show input panel when node is not connected', () => { + workflowPage.actions.addInitialNodeToCanvas('Manual'); + workflowPage.actions.deselectAll(); + workflowPage.actions.addNodeToCanvas('Set'); + workflowPage.getters.canvasNodes().last().dblclick(); + ndv.getters.container().should('be.visible').should('contain', 'Wire me up'); + }); + it('should test webhook node', () => { workflowPage.actions.addInitialNodeToCanvas('Webhook'); workflowPage.getters.canvasNodes().first().dblclick(); @@ -46,12 +52,13 @@ describe('NDV', () => { }); it('should change input and go back to canvas', () => { - cy.createFixtureWorkflow('NDV-test-select-input.json', `NDV test select input ${uuid()}`); + cy.createFixtureWorkflow('NDV-test-select-input.json', 'NDV test select input'); workflowPage.actions.zoomToFit(); workflowPage.getters.canvasNodes().last().dblclick(); + ndv.actions.switchInputMode('Table'); ndv.getters.inputSelect().click(); ndv.getters.inputOption().last().click(); - ndv.getters.inputDataContainer().find('[class*=schema_]').should('exist'); + ndv.getters.inputDataContainer().should('be.visible'); ndv.getters.inputDataContainer().should('contain', 'start'); ndv.getters.backToCanvas().click(); ndv.getters.container().should('not.be.visible'); @@ -59,7 +66,7 @@ describe('NDV', () => { }); it('should disconect Switch outputs if rules order was changed', () => { - cy.createFixtureWorkflow('NDV-test-switch_reorder.json', `NDV test switch reorder`); + cy.createFixtureWorkflow('NDV-test-switch_reorder.json', 'NDV test switch reorder'); workflowPage.actions.zoomToFit(); workflowPage.actions.executeWorkflow(); @@ -105,13 +112,26 @@ describe('NDV', () => { }); it('should show all validation errors when opening pasted node', () => { - cy.fixture('Test_workflow_ndv_errors.json').then((data) => { - cy.get('body').paste(JSON.stringify(data)); - workflowPage.getters.canvasNodes().should('have.have.length', 1); - workflowPage.actions.openNode('Airtable'); - cy.get('.has-issues').should('have.length', 3); - cy.get('[class*=hasIssues]').should('have.length', 1); - }); + cy.createFixtureWorkflow('Test_workflow_ndv_errors.json', 'Validation errors'); + workflowPage.getters.canvasNodes().should('have.have.length', 1); + workflowPage.actions.openNode('Airtable'); + cy.get('.has-issues').should('have.length', 3); + cy.get('[class*=hasIssues]').should('have.length', 1); + }); + + it('should render run errors correctly', () => { + cy.createFixtureWorkflow('Test_workflow_ndv_run_error.json', 'Run error'); + workflowPage.actions.openNode('Error'); + ndv.actions.execute(); + ndv.getters + .nodeRunErrorMessage() + .should('have.text', 'Info for expression missing from previous node'); + ndv.getters + .nodeRunErrorDescription() + .should( + 'contains.text', + "An expression here won't work because it uses .item and n8n can't figure out the matching item.", + ); }); it('should save workflow using keyboard shortcut from NDV', () => { @@ -135,7 +155,7 @@ describe('NDV', () => { 'prop2', ]; function setupSchemaWorkflow() { - cy.createFixtureWorkflow('Test_workflow_schema_test.json', `NDV test schema view ${uuid()}`); + cy.createFixtureWorkflow('Test_workflow_schema_test.json'); workflowPage.actions.zoomToFit(); workflowPage.actions.openNode('Set'); ndv.actions.execute(); @@ -209,7 +229,7 @@ describe('NDV', () => { it('should display large schema', () => { cy.createFixtureWorkflow( 'Test_workflow_schema_test_pinned_data.json', - `NDV test schema view ${uuid()}`, + 'NDV test schema view 2', ); workflowPage.actions.zoomToFit(); workflowPage.actions.openNode('Set'); @@ -231,6 +251,9 @@ describe('NDV', () => { workflowPage.actions.executeWorkflow(); workflowPage.actions.openNode('Set3'); + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + ndv.getters .inputRunSelector() .should('exist') @@ -242,9 +265,6 @@ describe('NDV', () => { .find('input') .should('include.value', '2 of 2 (6 items)'); - ndv.actions.switchInputMode('Table'); - ndv.actions.switchOutputMode('Table'); - ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 2 (6 items)'); ndv.getters.inputTbodyCell(1, 0).should('have.text', '1111'); @@ -284,7 +304,7 @@ describe('NDV', () => { it('should display parameter hints correctly', () => { workflowPage.actions.visit(); - cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`); + cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow 1'); workflowPage.actions.openNode('Set1'); ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions @@ -312,43 +332,11 @@ describe('NDV', () => { } ndv.getters.parameterInput('name').click(); // remove focus from input, hide expression preview - ndv.actions.validateExpressionPreview('value', output || input); + ndv.actions.validateExpressionPreview('value', output ?? input); ndv.getters.parameterInput('value').clear(); }); }); - it('should not retrieve remote options when required params throw errors', () => { - workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' }); - - ndv.getters.parameterInput('remoteOptions').click(); - getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3); - - ndv.actions.setInvalidExpression({ fieldName: 'fieldId', delay: 200 }); - - ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview - - ndv.getters.parameterInput('remoteOptions').click(); - - ndv.getters.parameterInputIssues('remoteOptions').realHover({ scrollBehavior: false }); - // Remote options dropdown should not be visible - ndv.getters.parameterInput('remoteOptions').find('.el-select').should('not.exist'); - }); - - it('should retrieve remote options when non-required params throw errors', () => { - workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' }); - - ndv.getters.parameterInput('remoteOptions').click(); - getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3); - ndv.getters.parameterInput('remoteOptions').click(); - - ndv.actions.setInvalidExpression({ fieldName: 'otherField', delay: 50 }); - - ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview - - ndv.getters.parameterInput('remoteOptions').click(); - getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3); - }); - it('should flag issues as soon as params are set', () => { workflowPage.actions.addInitialNodeToCanvas('Webhook'); workflowPage.getters.canvasNodes().first().dblclick(); @@ -395,7 +383,11 @@ describe('NDV', () => { }); it('should not retrieve remote options when a parameter value changes', () => { - cy.intercept('/rest/dynamic-node-parameters/options?**', cy.spy().as('fetchParameterOptions')); + cy.intercept( + 'POST', + '/rest/dynamic-node-parameters/options', + cy.spy().as('fetchParameterOptions'), + ); workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' }); // Type something into the field ndv.actions.typeIntoParameterInput('otherField', 'test'); @@ -411,7 +403,7 @@ describe('NDV', () => { } it('should traverse floating nodes with mouse', () => { - cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`); + cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes'); workflowPage.getters.canvasNodes().first().dblclick(); getFloatingNodeByPosition('inputMain').should('not.exist'); getFloatingNodeByPosition('outputMain').should('exist'); @@ -457,7 +449,7 @@ describe('NDV', () => { }); it('should traverse floating nodes with keyboard', () => { - cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`); + cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes'); workflowPage.getters.canvasNodes().first().dblclick(); getFloatingNodeByPosition('inputMain').should('not.exist'); getFloatingNodeByPosition('outputMain').should('exist'); @@ -548,21 +540,21 @@ describe('NDV', () => { }); it('should show node name and version in settings', () => { - cy.createFixtureWorkflow('Test_workflow_ndv_version.json', `NDV test version ${uuid()}`); + cy.createFixtureWorkflow('Test_workflow_ndv_version.json', 'NDV test version'); workflowPage.actions.openNode('Edit Fields (old)'); ndv.actions.openSettings(); - ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.3)'); + ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.4)'); ndv.actions.close(); workflowPage.actions.openNode('Edit Fields (latest)'); ndv.actions.openSettings(); - ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.3 (Latest)'); + ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.4 (Latest)'); ndv.actions.close(); workflowPage.actions.openNode('Edit Fields (no typeVersion)'); ndv.actions.openSettings(); - ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.3 (Latest)'); + ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.4 (Latest)'); ndv.actions.close(); workflowPage.actions.openNode('Function'); @@ -572,7 +564,7 @@ describe('NDV', () => { }); it('Should render xml and html tags as strings and can search', () => { - cy.createFixtureWorkflow('Test_workflow_xml_output.json', `test`); + cy.createFixtureWorkflow('Test_workflow_xml_output.json', 'test'); workflowPage.actions.executeWorkflow(); @@ -605,8 +597,7 @@ describe('NDV', () => { ndv.getters.outputDisplayMode().find('label').eq(2).click({ force: true }); ndv.getters .outputDataContainer() - .findChildByTestId('run-data-schema-item') - .find('> span') + .findChildByTestId('run-data-schema-item-value') .should('include.text', ''); }); @@ -627,7 +618,7 @@ describe('NDV', () => { ndv.getters.backToCanvas().click(); workflowPage.actions.executeWorkflow(); // Manual tigger node should show success indicator - workflowPage.actions.openNode('When clicking "Test workflow"'); + workflowPage.actions.openNode('When clicking ‘Test workflow’'); ndv.getters.nodeRunSuccessIndicator().should('exist'); // Code node should show error ndv.getters.backToCanvas().click(); @@ -673,6 +664,23 @@ describe('NDV', () => { ndv.getters.parameterInput('operation').find('input').should('have.value', 'Delete'); }); + it('Should show error state when remote options cannot be fetched', () => { + cy.intercept('POST', '/rest/dynamic-node-parameters/options', { statusCode: 500 }).as( + 'parameterOptions', + ); + + workflowPage.actions.addInitialNodeToCanvas(NOTION_NODE_NAME, { + keepNdvOpen: true, + action: 'Update a database page', + }); + + ndv.actions.addItemToFixedCollection('propertiesUi'); + ndv.getters + .parameterInput('key') + .find('input') + .should('have.value', 'Error fetching options from Notion'); + }); + it('Should open appropriate node creator after clicking on connection hint link', () => { const nodeCreator = new NodeCreator(); const hintMapper = { @@ -685,7 +693,7 @@ describe('NDV', () => { }; cy.createFixtureWorkflow( 'open_node_creator_for_connection.json', - `open_node_creator_for_connection ${uuid()}`, + 'open_node_creator_for_connection', ); Object.entries(hintMapper).forEach(([node, group]) => { @@ -696,20 +704,59 @@ describe('NDV', () => { }); }); - it('Stop listening for trigger event from NDV', () => { - cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); - workflowPage.actions.addInitialNodeToCanvas('Local File Trigger', { - keepNdvOpen: true, - action: 'On Changes To A Specific File', - isTrigger: true, - }); - ndv.getters.triggerPanelExecuteButton().should('exist'); - ndv.getters.triggerPanelExecuteButton().realClick(); - ndv.getters.triggerPanelExecuteButton().should('contain', 'Stop Listening'); - ndv.getters.triggerPanelExecuteButton().realClick(); - cy.wait('@workflowRun').then(() => { - ndv.getters.triggerPanelExecuteButton().should('contain', 'Test step'); - workflowPage.getters.successToast().should('exist'); + it('should allow selecting item for expressions', () => { + workflowPage.actions.visit(); + + cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow 2'); + workflowPage.actions.openNode('Set'); + + ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions + ndv.actions.typeIntoParameterInput('value', '{{', { + parseSpecialCharSequences: false, }); + ndv.actions.typeIntoParameterInput('value', '$json.input[0].count'); + ndv.getters.inlineExpressionEditorOutput().should('have.text', '0'); + + ndv.actions.expressionSelectNextItem(); + ndv.getters.inlineExpressionEditorOutput().should('have.text', '1'); + ndv.getters.inlineExpressionEditorItemInput().should('have.value', '1'); + ndv.getters.inlineExpressionEditorItemNextButton().should('be.disabled'); + + ndv.actions.expressionSelectPrevItem(); + ndv.getters.inlineExpressionEditorOutput().should('have.text', '0'); + ndv.getters.inlineExpressionEditorItemInput().should('have.value', '0'); + ndv.getters.inlineExpressionEditorItemPrevButton().should('be.disabled'); + + ndv.actions.expressionSelectItem(1); + ndv.getters.inlineExpressionEditorOutput().should('have.text', '1'); + }); + + it('should show data from the correct output in schema view', () => { + cy.createFixtureWorkflow('Test_workflow_multiple_outputs.json'); + workflowPage.actions.zoomToFit(); + + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Only Item 1'); + ndv.getters.inputPanel().should('be.visible'); + ndv.getters + .inputPanel() + .find('[data-test-id=run-data-schema-item]') + .should('contain.text', 'onlyOnItem1'); + ndv.actions.close(); + + workflowPage.actions.openNode('Only Item 2'); + ndv.getters.inputPanel().should('be.visible'); + ndv.getters + .inputPanel() + .find('[data-test-id=run-data-schema-item]') + .should('contain.text', 'onlyOnItem2'); + ndv.actions.close(); + + workflowPage.actions.openNode('Only Item 3'); + ndv.getters.inputPanel().should('be.visible'); + ndv.getters + .inputPanel() + .find('[data-test-id=run-data-schema-item]') + .should('contain.text', 'onlyOnItem3'); }); }); diff --git a/cypress/e2e/6-code-node.cy.ts b/cypress/e2e/6-code-node.cy.ts index 0964cff41e..74e775453b 100644 --- a/cypress/e2e/6-code-node.cy.ts +++ b/cypress/e2e/6-code-node.cy.ts @@ -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(); diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index 794e2ee605..7c7c3be554 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -1,16 +1,15 @@ import { CODE_NODE_NAME, MANUAL_TRIGGER_NODE_NAME, - META_KEY, SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME, - INSTANCE_MEMBERS, - INSTANCE_OWNER, + NOTION_NODE_NAME, } from '../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; import { getVisibleSelect } from '../utils'; import { WorkflowExecutionsTab } from '../pages'; +import { errorToast, successToast } from '../pages/notifications'; const NEW_WORKFLOW_NAME = 'Something else'; const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow'; @@ -35,6 +34,20 @@ describe('Workflow Actions', () => { WorkflowPage.getters.isWorkflowSaved(); }); + it('should not save already saved workflow', () => { + cy.intercept('PATCH', '/rest/workflows/*').as('saveWorkflow'); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + cy.wait('@saveWorkflow'); + WorkflowPage.getters.isWorkflowSaved(); + // Try to save a few times + WorkflowPage.actions.saveWorkflowUsingKeyboardShortcut(); + WorkflowPage.actions.saveWorkflowUsingKeyboardShortcut(); + // Should be saved only once + cy.get('@saveWorkflow.all').should('have.length', 1); + }); + it('should not be able to activate unsaved workflow', () => { WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled'); }); @@ -53,6 +66,30 @@ describe('Workflow Actions', () => { WorkflowPage.getters.isWorkflowActivated(); }); + it('should not be be able to activate workflow when nodes have errors', () => { + WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + WorkflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + successToast().should('exist'); + WorkflowPage.actions.clickWorkflowActivator(); + errorToast().should('exist'); + }); + + it('should be be able to activate workflow when nodes with errors are disabled', () => { + WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + WorkflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + successToast().should('exist'); + // First, try to activate the workflow with errors + WorkflowPage.actions.clickWorkflowActivator(); + errorToast().should('exist'); + // Now, disable the node with errors + WorkflowPage.getters.canvasNodes().last().click(); + WorkflowPage.actions.hitDisableNodeShortcut(); + WorkflowPage.actions.activateWorkflow(); + WorkflowPage.getters.isWorkflowActivated(); + }); + it('should save new workflow after renaming', () => { WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME); WorkflowPage.getters.isWorkflowSaved(); @@ -96,13 +133,13 @@ describe('Workflow Actions', () => { ); cy.reload(); cy.get('.el-loading-mask').should('exist'); - cy.get('body').type(META_KEY, { release: false }).type('s'); - cy.get('body').type(META_KEY, { release: false }).type('s'); - cy.get('body').type(META_KEY, { release: false }).type('s'); + WorkflowPage.actions.hitSaveWorkflow(); + WorkflowPage.actions.hitSaveWorkflow(); + WorkflowPage.actions.hitSaveWorkflow(); cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0)); cy.waitForLoad(); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); - cy.get('body').type(META_KEY, { release: false }).type('s'); + WorkflowPage.actions.hitSaveWorkflow(); cy.wait('@saveWorkflow'); cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1)); }); @@ -132,10 +169,11 @@ describe('Workflow Actions', () => { WorkflowPage.getters.canvasNodes().should('have.have.length', 2); cy.get('#node-creator').should('not.exist'); - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('a'); + + WorkflowPage.actions.hitSelectAll(); cy.get('.jtk-drag-selected').should('have.length', 2); - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('c'); - WorkflowPage.getters.successToast().should('exist'); + WorkflowPage.actions.hitCopy(); + successToast().should('exist'); }); it('should paste nodes (both current and old node versions)', () => { @@ -147,47 +185,62 @@ describe('Workflow Actions', () => { }); }); + it('should allow importing nodes without names', () => { + cy.fixture('Test_workflow-actions_import_nodes_empty_name.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + WorkflowPage.actions.zoomToFit(); + WorkflowPage.getters.canvasNodes().should('have.length', 3); + WorkflowPage.getters.nodeConnections().should('have.length', 2); + // Check if all nodes have names + WorkflowPage.getters.canvasNodes().each((node) => { + cy.wrap(node).should('have.attr', 'data-name'); + }); + }); + }); + it('should update workflow settings', () => { cy.visit(WorkflowPages.url); - WorkflowPages.getters.workflowCards().then((cards) => { - const totalWorkflows = cards.length; + cy.intercept('GET', '/rest/workflows', (req) => { + req.on('response', (res) => { + const totalWorkflows = res.body.count ?? 0; - WorkflowPage.actions.visit(); - // Open settings dialog - WorkflowPage.actions.saveWorkflowOnButtonClick(); - WorkflowPage.getters.workflowMenu().should('be.visible'); - WorkflowPage.getters.workflowMenu().click(); - WorkflowPage.getters.workflowMenuItemSettings().should('be.visible'); - WorkflowPage.getters.workflowMenuItemSettings().click(); - // Change all settings - // totalWorkflows + 1 (current workflow) + 1 (no workflow option) - WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().click(); - getVisibleSelect() - .find('li') - .should('have.length', totalWorkflows + 2); - getVisibleSelect().find('li').last().click({ force: true }); - WorkflowPage.getters.workflowSettingsTimezoneSelect().click(); - getVisibleSelect().find('li').should('exist'); - getVisibleSelect().find('li').eq(1).click({ force: true }); - WorkflowPage.getters.workflowSettingsSaveFiledExecutionsSelect().click(); - getVisibleSelect().find('li').should('have.length', 3); - getVisibleSelect().find('li').last().click({ force: true }); - WorkflowPage.getters.workflowSettingsSaveSuccessExecutionsSelect().click(); - getVisibleSelect().find('li').should('have.length', 3); - getVisibleSelect().find('li').last().click({ force: true }); - WorkflowPage.getters.workflowSettingsSaveManualExecutionsSelect().click(); - getVisibleSelect().find('li').should('have.length', 3); - getVisibleSelect().find('li').last().click({ force: true }); - WorkflowPage.getters.workflowSettingsSaveExecutionProgressSelect().click(); - getVisibleSelect().find('li').should('have.length', 3); - getVisibleSelect().find('li').last().click({ force: true }); - WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click(); - WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1'); - // Save settings - WorkflowPage.getters.workflowSettingsSaveButton().click(); - WorkflowPage.getters.workflowSettingsModal().should('not.exist'); - WorkflowPage.getters.successToast().should('exist'); - }); + WorkflowPage.actions.visit(); + // Open settings dialog + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.getters.workflowMenu().should('be.visible'); + WorkflowPage.getters.workflowMenu().click(); + WorkflowPage.getters.workflowMenuItemSettings().should('be.visible'); + WorkflowPage.getters.workflowMenuItemSettings().click(); + // Change all settings + // totalWorkflows + 1 (current workflow) + 1 (no workflow option) + WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', totalWorkflows + 2); + getVisibleSelect().find('li').last().click({ force: true }); + WorkflowPage.getters.workflowSettingsTimezoneSelect().click(); + getVisibleSelect().find('li').should('exist'); + getVisibleSelect().find('li').eq(1).click({ force: true }); + WorkflowPage.getters.workflowSettingsSaveFiledExecutionsSelect().click(); + getVisibleSelect().find('li').should('have.length', 3); + getVisibleSelect().find('li').last().click({ force: true }); + WorkflowPage.getters.workflowSettingsSaveSuccessExecutionsSelect().click(); + getVisibleSelect().find('li').should('have.length', 3); + getVisibleSelect().find('li').last().click({ force: true }); + WorkflowPage.getters.workflowSettingsSaveManualExecutionsSelect().click(); + getVisibleSelect().find('li').should('have.length', 3); + getVisibleSelect().find('li').last().click({ force: true }); + WorkflowPage.getters.workflowSettingsSaveExecutionProgressSelect().click(); + getVisibleSelect().find('li').should('have.length', 3); + getVisibleSelect().find('li').last().click({ force: true }); + WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click(); + WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1'); + // Save settings + WorkflowPage.getters.workflowSettingsSaveButton().click(); + WorkflowPage.getters.workflowSettingsModal().should('not.exist'); + successToast().should('exist'); + }); + }).as('loadWorkflows'); }); it('should not be able to delete unsaved workflow', () => { @@ -203,8 +256,8 @@ describe('Workflow Actions', () => { WorkflowPage.getters.workflowMenuItemDelete().click(); cy.get('div[role=dialog][aria-modal=true]').should('be.visible'); cy.get('button.btn--confirm').should('be.visible').click(); - WorkflowPage.getters.successToast().should('exist'); - cy.url().should('include', '/workflow/new'); + successToast().should('exist'); + cy.url().should('include', WorkflowPages.url); }); describe('duplicate workflow', () => { @@ -232,7 +285,7 @@ describe('Workflow Actions', () => { .contains('Duplicate') .should('be.visible'); WorkflowPage.getters.duplicateWorkflowModal().find('button').contains('Duplicate').click(); - WorkflowPage.getters.errorToast().should('not.exist'); + errorToast().should('not.exist'); } beforeEach(() => { @@ -272,18 +325,43 @@ describe('Workflow Actions', () => { WorkflowPage.getters.canvasNodePlusEndpointByName(EDIT_FIELDS_SET_NODE_NAME).click(); WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); }); + + it('should run workflow on button click', () => { + WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.getters.executeWorkflowButton().click(); + successToast().should('contain.text', 'Workflow executed successfully'); + }); + + it('should run workflow using keyboard shortcut', () => { + WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.actions.hitExecuteWorkflow(); + successToast().should('contain.text', 'Workflow executed successfully'); + }); + + it('should not run empty workflows', () => { + // Clear the canvas + WorkflowPage.actions.hitDeleteAllNodes(); + WorkflowPage.getters.canvasNodes().should('have.length', 0); + // Button should be disabled + WorkflowPage.getters.executeWorkflowButton().should('be.disabled'); + // Keyboard shortcut should not work + WorkflowPage.actions.hitExecuteWorkflow(); + successToast().should('not.exist'); + }); }); describe('Menu entry Push To Git', () => { it('should not show up in the menu for members', () => { - cy.signin(INSTANCE_MEMBERS[0]); + cy.signinAsMember(0); cy.visit(WorkflowPages.url); WorkflowPage.actions.visit(); WorkflowPage.getters.workflowMenuItemGitPush().should('not.exist'); }); it('should show up for owners', () => { - cy.signin(INSTANCE_OWNER); + cy.signinAsOwner(); cy.visit(WorkflowPages.url); WorkflowPage.actions.visit(); WorkflowPage.getters.workflowMenuItemGitPush().should('exist'); diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts index 66758cabd3..840e3d4fc6 100644 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ b/cypress/e2e/9-expression-editor-modal.cy.ts @@ -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'); }); }); diff --git a/cypress/fixtures/Floating_Nodes.json b/cypress/fixtures/Floating_Nodes.json index 2ffc1b3fde..6624c53ac6 100644 --- a/cypress/fixtures/Floating_Nodes.json +++ b/cypress/fixtures/Floating_Nodes.json @@ -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": [ [ { diff --git a/cypress/fixtures/Lots_of_nodes.json b/cypress/fixtures/Lots_of_nodes.json index 7b3ad507c8..11152fb496 100644 --- a/cypress/fixtures/Lots_of_nodes.json +++ b/cypress/fixtures/Lots_of_nodes.json @@ -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": [ [ { diff --git a/cypress/fixtures/Multiple_trigger_node_rerun.json b/cypress/fixtures/Multiple_trigger_node_rerun.json index 39d231a894..c5b34aaa26 100644 --- a/cypress/fixtures/Multiple_trigger_node_rerun.json +++ b/cypress/fixtures/Multiple_trigger_node_rerun.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "5ae8991f-08a2-4b27-b61c-85e3b8a83693", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -14,7 +14,7 @@ }, { "parameters": { - "url": "https://random-data-api.com/api/v2/users?size=5", + "url": "https://internal.users.n8n.cloud/webhook/random-data-api", "options": {} }, "id": "22511d75-ab54-49e1-b8af-08b8b3372373", @@ -28,7 +28,7 @@ }, { "parameters": { - "jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.first_name,\n firstnNameReversed: item.json.first_name_BUG.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();" + "jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.firstname,\n firstnNameReversed: item.json.firstname.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();" }, "id": "4b66b15a-1685-46c1-a5e3-ebf8cdb11d21", "name": "do something with them", @@ -78,14 +78,14 @@ } } ], - "When clicking \"Test workflow\"": [ + "When clicking ‘Test workflow’": [ { "json": {} } ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { @@ -130,4 +130,4 @@ }, "id": "PymcwIrbqgNh3O0K", "tags": [] -} \ No newline at end of file +} diff --git a/cypress/fixtures/NDV-debug-generate-data.json b/cypress/fixtures/NDV-debug-generate-data.json new file mode 100644 index 0000000000..25f771f9ec --- /dev/null +++ b/cypress/fixtures/NDV-debug-generate-data.json @@ -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": {} +} diff --git a/cypress/fixtures/NDV-test-switch_reorder.json b/cypress/fixtures/NDV-test-switch_reorder.json index cf970434f3..8e06d9dc02 100644 --- a/cypress/fixtures/NDV-test-switch_reorder.json +++ b/cypress/fixtures/NDV-test-switch_reorder.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "b3f0815d-b733-413f-ab3f-74e48277bd3a", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -160,7 +160,7 @@ ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Node_IO_filter.json b/cypress/fixtures/Node_IO_filter.json index 61be5d58d8..885c76a2b9 100644 --- a/cypress/fixtures/Node_IO_filter.json +++ b/cypress/fixtures/Node_IO_filter.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "46770685-44d1-4aad-9107-1d790cf26b50", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -74,7 +74,7 @@ } ], "pinData": { - "When clicking \"Test workflow\"": [ + "When clicking ‘Test workflow’": [ { "json": { "id": "654cfa05fa51480dcb543b1a", @@ -599,7 +599,7 @@ ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Suggested_Templates.json b/cypress/fixtures/Suggested_Templates.json deleted file mode 100644 index 3f69c4b1a9..0000000000 --- a/cypress/fixtures/Suggested_Templates.json +++ /dev/null @@ -1,655 +0,0 @@ -{ - "sections": [ - { - "name": "Lead enrichment", - "description": "Explore curated lead enrichment workflows or start fresh with a blank canvas", - "workflows": [ - { - "title": "Score new leads with AI from Facebook Lead Ads with AI and get notifications for high scores on Slack", - "description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.", - "preview": { - "nodes": [ - { - "parameters": { - "operation": "create", - "base": { - "__rl": true, - "mode": "list", - "value": "" - }, - "table": { - "__rl": true, - "mode": "list", - "value": "" - }, - "columns": { - "mappingMode": "defineBelow", - "value": {}, - "matchingColumns": [], - "schema": [] - }, - "options": {} - }, - "id": "b09d4f4d-19fa-43de-8148-2d430a04956f", - "name": "Airtable", - "type": "n8n-nodes-base.airtable", - "typeVersion": 2, - "position": [ - 1800, - 740 - ] - }, - { - "parameters": {}, - "id": "551313bb-1e01-4133-9956-e6f09968f2ce", - "name": "When clicking \"Test workflow\"", - "type": "n8n-nodes-base.manualTrigger", - "typeVersion": 1, - "position": [ - 920, - 740 - ] - }, - { - "parameters": { - "options": {} - }, - "id": "b4c089ee-2adb-435e-8d48-47012c981a11", - "name": "Get image", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [ - 1140, - 740 - ] - }, - { - "parameters": { - "operation": "extractHtmlContent", - "options": {} - }, - "id": "04ca2f61-b930-4fbc-b467-3470c0d93d64", - "name": "Extract Information", - "type": "n8n-nodes-base.html", - "typeVersion": 1, - "position": [ - 1360, - 740 - ] - }, - { - "parameters": { - "options": {} - }, - "id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce", - "name": "Set Information", - "type": "n8n-nodes-base.set", - "typeVersion": 3.2, - "position": [ - 1580, - 740 - ] - } - ], - "connections": { - "When clicking \"Test workflow\"": { - "main": [ - [ - { - "node": "Get image", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get image": { - "main": [ - [ - { - "node": "Extract Information", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract Information": { - "main": [ - [ - { - "node": "Set Information", - "type": "main", - "index": 0 - } - ] - ] - }, - "Set Information": { - "main": [ - [ - { - "node": "Airtable", - "type": "main", - "index": 0 - } - ] - ] - } - } - }, - "nodes": [ - { - "id": 24, - "icon": "fa:code-branch", - "defaults": { - "color": "#00bbcc" - }, - "iconData": { - "icon": "code-branch", - "type": "icon" - }, - "displayName": "Merge" - } - ] - }, - { - "title": "Verify the email address every time a contact is created in HubSpot", - "description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.", - "preview": { - "nodes": [ - { - "parameters": { - "operation": "create", - "base": { - "__rl": true, - "mode": "list", - "value": "" - }, - "table": { - "__rl": true, - "mode": "list", - "value": "" - }, - "columns": { - "mappingMode": "defineBelow", - "value": {}, - "matchingColumns": [], - "schema": [] - }, - "options": {} - }, - "id": "b09d4f4d-19fa-43de-8148-2d430a04956f", - "name": "Airtable", - "type": "n8n-nodes-base.airtable", - "typeVersion": 2, - "position": [ - 1800, - 740 - ] - }, - { - "parameters": {}, - "id": "551313bb-1e01-4133-9956-e6f09968f2ce", - "name": "When clicking \"Test workflow\"", - "type": "n8n-nodes-base.manualTrigger", - "typeVersion": 1, - "position": [ - 920, - 740 - ] - }, - { - "parameters": { - "options": {} - }, - "id": "b4c089ee-2adb-435e-8d48-47012c981a11", - "name": "Get image", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [ - 1140, - 740 - ] - }, - { - "parameters": { - "operation": "extractHtmlContent", - "options": {} - }, - "id": "04ca2f61-b930-4fbc-b467-3470c0d93d64", - "name": "Extract Information", - "type": "n8n-nodes-base.html", - "typeVersion": 1, - "position": [ - 1360, - 740 - ] - }, - { - "parameters": { - "options": {} - }, - "id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce", - "name": "Set Information", - "type": "n8n-nodes-base.set", - "typeVersion": 3.2, - "position": [ - 1580, - 740 - ] - } - ], - "connections": { - "When clicking \"Test workflow\"": { - "main": [ - [ - { - "node": "Get image", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get image": { - "main": [ - [ - { - "node": "Extract Information", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract Information": { - "main": [ - [ - { - "node": "Set Information", - "type": "main", - "index": 0 - } - ] - ] - }, - "Set Information": { - "main": [ - [ - { - "node": "Airtable", - "type": "main", - "index": 0 - } - ] - ] - } - } - }, - "nodes": [ - { - "id": 14, - "icon": "fa:code", - "name": "n8n-nodes-base.function", - "defaults": { - "name": "Function", - "color": "#FF9922" - }, - "iconData": { - "icon": "code", - "type": "icon" - }, - "categories": [ - { - "id": 5, - "name": "Development" - }, - { - "id": 9, - "name": "Core Nodes" - } - ], - "displayName": "Function", - "typeVersion": 1 - }, - { - "id": 24, - "icon": "fa:code-branch", - "name": "n8n-nodes-base.merge", - "defaults": { - "name": "Merge", - "color": "#00bbcc" - }, - "iconData": { - "icon": "code-branch", - "type": "icon" - }, - "categories": [ - { - "id": 9, - "name": "Core Nodes" - } - ], - "displayName": "Merge", - "typeVersion": 2 - } - ] - }, - { - "title": "Enrich leads from HubSpot with company information via OpenAi", - "description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.", - "preview": { - "nodes": [ - { - "parameters": { - "operation": "create", - "base": { - "__rl": true, - "mode": "list", - "value": "" - }, - "table": { - "__rl": true, - "mode": "list", - "value": "" - }, - "columns": { - "mappingMode": "defineBelow", - "value": {}, - "matchingColumns": [], - "schema": [] - }, - "options": {} - }, - "id": "b09d4f4d-19fa-43de-8148-2d430a04956f", - "name": "Airtable", - "type": "n8n-nodes-base.airtable", - "typeVersion": 2, - "position": [ - 1800, - 740 - ] - }, - { - "parameters": {}, - "id": "551313bb-1e01-4133-9956-e6f09968f2ce", - "name": "When clicking \"Test workflow\"", - "type": "n8n-nodes-base.manualTrigger", - "typeVersion": 1, - "position": [ - 920, - 740 - ] - }, - { - "parameters": { - "options": {} - }, - "id": "b4c089ee-2adb-435e-8d48-47012c981a11", - "name": "Get image", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [ - 1140, - 740 - ] - }, - { - "parameters": { - "operation": "extractHtmlContent", - "options": {} - }, - "id": "04ca2f61-b930-4fbc-b467-3470c0d93d64", - "name": "Extract Information", - "type": "n8n-nodes-base.html", - "typeVersion": 1, - "position": [ - 1360, - 740 - ] - }, - { - "parameters": { - "options": {} - }, - "id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce", - "name": "Set Information", - "type": "n8n-nodes-base.set", - "typeVersion": 3.2, - "position": [ - 1580, - 740 - ] - } - ], - "connections": { - "When clicking \"Test workflow\"": { - "main": [ - [ - { - "node": "Get image", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get image": { - "main": [ - [ - { - "node": "Extract Information", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract Information": { - "main": [ - [ - { - "node": "Set Information", - "type": "main", - "index": 0 - } - ] - ] - }, - "Set Information": { - "main": [ - [ - { - "node": "Airtable", - "type": "main", - "index": 0 - } - ] - ] - } - } - }, - "nodes": [ - { - "id": 14, - "icon": "fa:code", - "defaults": { - "name": "Function", - "color": "#FF9922" - }, - "iconData": { - "icon": "code", - "type": "icon" - }, - "displayName": "Function" - } - ] - }, - { - "title": "Score new lead submissions from Facebook Lead Ads with AI and notify me on Slack when it is a high score lead", - "description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.", - "preview": { - "nodes": [ - { - "parameters": { - "operation": "create", - "base": { - "__rl": true, - "mode": "list", - "value": "" - }, - "table": { - "__rl": true, - "mode": "list", - "value": "" - }, - "columns": { - "mappingMode": "defineBelow", - "value": {}, - "matchingColumns": [], - "schema": [] - }, - "options": {} - }, - "id": "b09d4f4d-19fa-43de-8148-2d430a04956f", - "name": "Airtable", - "type": "n8n-nodes-base.airtable", - "typeVersion": 2, - "position": [ - 1800, - 740 - ] - }, - { - "parameters": {}, - "id": "551313bb-1e01-4133-9956-e6f09968f2ce", - "name": "When clicking \"Test workflow\"", - "type": "n8n-nodes-base.manualTrigger", - "typeVersion": 1, - "position": [ - 920, - 740 - ] - }, - { - "parameters": { - "options": {} - }, - "id": "b4c089ee-2adb-435e-8d48-47012c981a11", - "name": "Get image", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [ - 1140, - 740 - ] - }, - { - "parameters": { - "operation": "extractHtmlContent", - "options": {} - }, - "id": "04ca2f61-b930-4fbc-b467-3470c0d93d64", - "name": "Extract Information", - "type": "n8n-nodes-base.html", - "typeVersion": 1, - "position": [ - 1360, - 740 - ] - }, - { - "parameters": { - "options": {} - }, - "id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce", - "name": "Set Information", - "type": "n8n-nodes-base.set", - "typeVersion": 3.2, - "position": [ - 1580, - 740 - ] - } - ], - "connections": { - "When clicking \"Test workflow\"": { - "main": [ - [ - { - "node": "Get image", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get image": { - "main": [ - [ - { - "node": "Extract Information", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract Information": { - "main": [ - [ - { - "node": "Set Information", - "type": "main", - "index": 0 - } - ] - ] - }, - "Set Information": { - "main": [ - [ - { - "node": "Airtable", - "type": "main", - "index": 0 - } - ] - ] - } - } - }, - "nodes": [ - { - "id": 14, - "icon": "fa:code", - "defaults": { - "name": "Function", - "color": "#FF9922" - }, - "iconData": { - "icon": "code", - "type": "icon" - }, - "displayName": "Function" - }, - { - "id": 24, - "icon": "fa:code-branch", - "defaults": { - "name": "Merge", - "color": "#00bbcc" - }, - "iconData": { - "icon": "code-branch", - "type": "icon" - }, - "displayName": "Merge" - } - ] - } - ] - } - ] -} diff --git a/cypress/fixtures/Test_Workflow_pairedItem_incomplete_manual_bug.json b/cypress/fixtures/Test_Workflow_pairedItem_incomplete_manual_bug.json index f740bc1df6..60875681ab 100644 --- a/cypress/fixtures/Test_Workflow_pairedItem_incomplete_manual_bug.json +++ b/cypress/fixtures/Test_Workflow_pairedItem_incomplete_manual_bug.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "f26332f3-c61a-4843-94bd-64a73ad161ff", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -105,7 +105,7 @@ ], "pinData": {}, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow-actions_import_nodes_empty_name.json b/cypress/fixtures/Test_workflow-actions_import_nodes_empty_name.json new file mode 100644 index 0000000000..cb7b4dcf20 --- /dev/null +++ b/cypress/fixtures/Test_workflow-actions_import_nodes_empty_name.json @@ -0,0 +1,69 @@ +{ + "meta": { + "instanceId": "4a31be0d29cfa6246ba62b359030d712af57b98c5dfe6a7ee8beee0a46c5b5a4" + }, + "nodes": [ + { + "parameters": { + "operation": "get" + }, + "id": "5b084875-bd5e-4731-9591-18d2c8996945", + "name": "", + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [ + 900, + 460 + ] + }, + { + "parameters": {}, + "id": "449ab540-d9d7-480d-b131-05e9989a69cd", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 460, + 460 + ] + }, + { + "parameters": { + "operation": "get" + }, + "id": "3a791321-6f0c-4f92-91e5-20e1be0d4964", + "name": "Gmail", + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [ + 680, + 460 + ] + } + ], + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Gmail", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gmail": { + "main": [ + [ + { + "node": "Gmail1", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/cypress/fixtures/Test_workflow_4_executions_view.json b/cypress/fixtures/Test_workflow_4_executions_view.json index a0be9eae35..2af7a1df29 100644 --- a/cypress/fixtures/Test_workflow_4_executions_view.json +++ b/cypress/fixtures/Test_workflow_4_executions_view.json @@ -65,5 +65,19 @@ ] ] } - } + }, + "tags": [ + { + "name": "some-tag-1", + "createdAt": "2022-11-10T13:43:34.001Z", + "updatedAt": "2022-11-10T13:43:34.001Z", + "id": "6" + }, + { + "name": "some-tag-2", + "createdAt": "2022-11-10T13:43:39.778Z", + "updatedAt": "2022-11-10T13:43:39.778Z", + "id": "7" + } + ] } diff --git a/cypress/fixtures/Test_workflow_5.json b/cypress/fixtures/Test_workflow_5.json index 5771e197d9..f3bf74634b 100644 --- a/cypress/fixtures/Test_workflow_5.json +++ b/cypress/fixtures/Test_workflow_5.json @@ -40,7 +40,7 @@ { "parameters": {}, "id": "ef63cdc5-50bc-4525-9873-7e7f7589a60e", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -199,7 +199,7 @@ ] ] }, - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_filter.json b/cypress/fixtures/Test_workflow_filter.json index e5aad93388..cf3ab886f9 100644 --- a/cypress/fixtures/Test_workflow_filter.json +++ b/cypress/fixtures/Test_workflow_filter.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "f332a7d1-31b4-4e78-b31e-9e8db945bf3f", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -99,7 +99,7 @@ ], "pinData": {}, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_multiple_outputs.json b/cypress/fixtures/Test_workflow_multiple_outputs.json new file mode 100644 index 0000000000..b80ff5dd33 --- /dev/null +++ b/cypress/fixtures/Test_workflow_multiple_outputs.json @@ -0,0 +1,223 @@ +{ + "name": "Multiple outputs", + "nodes": [ + { + "parameters": {}, + "id": "64b27674-3da6-46ce-9008-e173182efa48", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "position": [ + 16, + -32 + ], + "typeVersion": 1 + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "leftValue": "={{ $json.code }}", + "rightValue": 1, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Item1" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "a659050f-0867-471d-8914-d499b6ad7b31", + "leftValue": "={{ $json.code }}", + "rightValue": 2, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Item2" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "109fc001-53af-48f1-b79c-5e9afc8b94bd", + "leftValue": "={{ $json.code }}", + "rightValue": 3, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Item3" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "position": [ + 192, + -32 + ], + "id": "3863cc7a-8f45-46fc-a60c-36aad5b12877", + "name": "Switch", + "typeVersion": 3 + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f71bac89-8852-41b2-98dd-cb689f011dcb", + "name": "", + "value": "", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "position": [ + 480, + -192 + ], + "id": "85940094-4656-4cdf-a871-1b3b46421de3", + "name": "Only Item 1", + "typeVersion": 3.4 + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.set", + "position": [ + 480, + -32 + ], + "id": "a7f4e2b5-8cc9-4881-aa06-38601988740e", + "name": "Only Item 2", + "typeVersion": 3.4 + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.set", + "position": [ + 480, + 128 + ], + "id": "7e44ad56-415a-4991-a70e-fea86c430031", + "name": "Only Item 3", + "typeVersion": 3.4 + } + ], + "pinData": { + "When clicking ‘Test workflow’": [ + { + "json": { + "name": "First item", + "onlyOnItem1": true, + "code": 1 + } + }, + { + "json": { + "name": "Second item", + "onlyOnItem2": true, + "code": 2 + } + }, + { + "json": { + "name": "Third item", + "onlyOnItem3": true, + "code": 3 + } + } + ] + }, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + [ + { + "node": "Only Item 1", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Only Item 2", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Only Item 3", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "1e2a7b45-7730-42d6-989e-f3fa80de303e", + "meta": { + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "V2ld4YU11fsHgr1z", + "tags": [] +} diff --git a/cypress/fixtures/Test_workflow_ndv_run_error.json b/cypress/fixtures/Test_workflow_ndv_run_error.json new file mode 100644 index 0000000000..a42347dccf --- /dev/null +++ b/cypress/fixtures/Test_workflow_ndv_run_error.json @@ -0,0 +1,162 @@ +{ + "name": "My workflow 52", + "nodes": [ + { + "parameters": { + "jsCode": "\nreturn [\n {\n \"field\": \"the same\"\n }\n];" + }, + "id": "38c14c4a-7af1-4b04-be76-f8e474c95569", + "name": "Break pairedItem chain", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 240, + 1020 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "78c4964a-c4e8-47e5-81f3-89ba778feb8b", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [ + 40, + 1020 + ] + }, + { + "parameters": {}, + "id": "4f4c6527-d565-448a-96bd-8f5414caf8cc", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -180, + 1020 + ] + }, + { + "parameters": { + "fields": { + "values": [ + { + "stringValue": "={{ $('Edit Fields').item.json.name }}" + } + ] + }, + "options": {} + }, + "id": "44f4e5da-bfe9-4dc3-8d1f-f38e9f364754", + "name": "Error", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [ + 460, + 1020 + ] + } + ], + "pinData": { + "Edit Fields": [ + { + "json": { + "id": "23423532", + "name": "Jay Gatsby", + "email": "gatsby@west-egg.com", + "notes": "Keeps asking about a green light??", + "country": "US", + "created": "1925-04-10" + } + }, + { + "json": { + "id": "23423533", + "name": "José Arcadio Buendía", + "email": "jab@macondo.co", + "notes": "Lots of people named after him. Very confusing", + "country": "CO", + "created": "1967-05-05" + } + }, + { + "json": { + "id": "23423534", + "name": "Max Sendak", + "email": "info@in-and-out-of-weeks.org", + "notes": "Keeps rolling his terrible eyes", + "country": "US", + "created": "1963-04-09" + } + }, + { + "json": { + "id": "23423535", + "name": "Zaphod Beeblebrox", + "email": "captain@heartofgold.com", + "notes": "Felt like I was talking to more than one person", + "country": null, + "created": "1979-10-12" + } + }, + { + "json": { + "id": "23423536", + "name": "Edmund Pevensie", + "email": "edmund@narnia.gov", + "notes": "Passionate sailor", + "country": "UK", + "created": "1950-10-16" + } + } + ] + }, + "connections": { + "Break pairedItem chain": { + "main": [ + [ + { + "node": "Error", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "Break pairedItem chain", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "ca53267f-4eb4-481d-9e09-ecb97f6b09e2", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "6fr8GiRyMlZCiDQW", + "tags": [] + } diff --git a/cypress/fixtures/Test_workflow_ndv_version.json b/cypress/fixtures/Test_workflow_ndv_version.json index 7f3ba16f43..80de6e0fdc 100644 --- a/cypress/fixtures/Test_workflow_ndv_version.json +++ b/cypress/fixtures/Test_workflow_ndv_version.json @@ -3,7 +3,7 @@ "nodes": [ { "id": "2acca986-10a6-451e-b20a-86e95b50e627", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [460, 460] @@ -32,7 +32,7 @@ "id": "273f60c9-08e7-457e-b01d-31e16c565171", "name": "Edit Fields (latest)", "type": "n8n-nodes-base.set", - "typeVersion": 3.3, + "typeVersion": 3.4, "position": [640, 460] } ], diff --git a/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json b/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json index 2a9e75e11b..c02f01e59c 100644 --- a/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json +++ b/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json @@ -7,7 +7,7 @@ { "parameters": {}, "id": "09e4325e-ede1-40cf-a1ba-58612bbc7f1b", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -77,7 +77,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_pinned_data_in_expressions.json b/cypress/fixtures/Test_workflow_pinned_data_in_expressions.json new file mode 100644 index 0000000000..099672810e --- /dev/null +++ b/cypress/fixtures/Test_workflow_pinned_data_in_expressions.json @@ -0,0 +1,112 @@ +{ + "meta": { + "instanceId": "5bd32b91ed2a88e542012920460f736c3687a32fbb953718f6952d182231c0ff" + }, + "nodes": [ + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "a482f1fd-4815-4da4-a733-7beafb43c500", + "name": "static", + "value": "={{ $('PinnedSet').first().json.firstName }}\n{{ $('PinnedSet').itemMatching(0).json.firstName }}\n{{ $('PinnedSet').itemMatching(1).json.firstName }}\n{{ $('PinnedSet').last().json.firstName }}\n{{ $('PinnedSet').all()[0].json.firstName }}\n{{ $('PinnedSet').all()[1].json.firstName }}\n\n{{ $input.first().json.firstName }}\n{{ $input.last().json.firstName }}\n\n{{ $items()[0].json.firstName }}", + "type": "string" + }, + { + "id": "2c973f2a-7ca0-41bc-903c-7174bee251b0", + "name": "variable", + "value": "={{ $runIndex }},{{ $itemIndex }}\n{{ $node['PinnedSet'].json.firstName }}\n\n{{ $('PinnedSet').item.json.firstName }}\n\n{{ $input.item.json.firstName }}\n\n{{ $json.firstName }}\n{{ $data.firstName }}", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "ac55ee16-4598-48bf-ace3-a48fed1d4ff3", + "name": "NotPinnedWithExpressions", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 1600, + 640 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "3058c300-b377-41b7-9c90-a01372f9b581", + "name": "firstName", + "value": "Joe", + "type": "string" + }, + { + "id": "bb871662-c23c-4234-ac0c-b78c279bbf34", + "name": "lastName", + "value": "Smith", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "300a3888-cc2f-4e61-8578-b0adbcf33450", + "name": "PinnedSet", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 1340, + 640 + ] + }, + { + "parameters": {}, + "id": "426ff39a-3408-48b4-899f-60db732675f8", + "name": "Start", + "type": "n8n-nodes-base.manualTrigger", + "position": [ + 1100, + 640 + ], + "typeVersion": 1 + } + ], + "connections": { + "PinnedSet": { + "main": [ + [ + { + "node": "NotPinnedWithExpressions", + "type": "main", + "index": 0 + } + ] + ] + }, + "Start": { + "main": [ + [ + { + "node": "PinnedSet", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "PinnedSet": [ + { + "firstName": "Joe", + "lastName": "Smith" + }, + { + "firstName": "Joan", + "lastName": "Summers" + } + ] + } +} diff --git a/cypress/fixtures/Test_workflow_schema_test.json b/cypress/fixtures/Test_workflow_schema_test.json index 8c83c4f20e..0252fb893e 100644 --- a/cypress/fixtures/Test_workflow_schema_test.json +++ b/cypress/fixtures/Test_workflow_schema_test.json @@ -47,7 +47,7 @@ { "parameters": {}, "id": "58512a93-dabf-4584-817f-27c608c1bdd5", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -69,7 +69,7 @@ ] ] }, - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_schema_test_pinned_data.json b/cypress/fixtures/Test_workflow_schema_test_pinned_data.json index 8bd5ef783d..73f6b62cff 100644 --- a/cypress/fixtures/Test_workflow_schema_test_pinned_data.json +++ b/cypress/fixtures/Test_workflow_schema_test_pinned_data.json @@ -47,7 +47,7 @@ { "parameters": {}, "id": "3dc7cf26-ff25-4437-b9fd-0e8b127ebec9", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -552,7 +552,7 @@ ] ] }, - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_webhook_with_pin_data.json b/cypress/fixtures/Test_workflow_webhook_with_pin_data.json index fb632bcf36..503b723e5b 100644 --- a/cypress/fixtures/Test_workflow_webhook_with_pin_data.json +++ b/cypress/fixtures/Test_workflow_webhook_with_pin_data.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "0a60e507-7f34-41c0-a0f9-697d852033b6", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -93,7 +93,7 @@ ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_xml_output.json b/cypress/fixtures/Test_workflow_xml_output.json index 17449bc56d..871156fab2 100644 --- a/cypress/fixtures/Test_workflow_xml_output.json +++ b/cypress/fixtures/Test_workflow_xml_output.json @@ -6,7 +6,7 @@ { "parameters": {}, "id": "8108d313-8b03-4aa4-963d-cd1c0fe8f85c", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -37,7 +37,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Webhook_set_pinned.json b/cypress/fixtures/Webhook_set_pinned.json new file mode 100644 index 0000000000..12401db243 --- /dev/null +++ b/cypress/fixtures/Webhook_set_pinned.json @@ -0,0 +1,67 @@ +{ + "nodes": [ + { + "parameters": { + "options": {} + }, + "id": "bd816131-d8ad-4b4c-90d6-59fdab2e6307", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [ + 720, + 460 + ] + }, + { + "parameters": { + "httpMethod": "POST", + "path": "23fc3930-b8f9-41d9-89db-b647291a2201", + "options": {} + }, + "id": "82fe0f6c-854a-4eb9-b311-d7b43025c047", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [ + 460, + 460 + ], + "webhookId": "23fc3930-b8f9-41d9-89db-b647291a2201" + } + ], + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "Webhook": [ + { + "headers": { + "host": "localhost:5678", + "content-length": "37", + "accept": "*/*", + "content-type": "application/json", + "accept-encoding": "gzip" + }, + "params": {}, + "query": {}, + "body": { + "here": "be", + "dragons": true + }, + "webhookUrl": "http://localhost:5678/webhook-test/23fc3930-b8f9-41d9-89db-b647291a2201", + "executionMode": "test" + } + ] + } +} diff --git a/cypress/fixtures/expression_with_paired_item_in_multi_input_node.json b/cypress/fixtures/expression_with_paired_item_in_multi_input_node.json index ffb7005f4f..d2b16c5656 100644 --- a/cypress/fixtures/expression_with_paired_item_in_multi_input_node.json +++ b/cypress/fixtures/expression_with_paired_item_in_multi_input_node.json @@ -6,7 +6,7 @@ { "parameters": {}, "id": "bcb6abdf-d34b-4ea7-a8ed-58155b708c43", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -90,7 +90,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/manual-partial-execution.json b/cypress/fixtures/manual-partial-execution.json new file mode 100644 index 0000000000..9e43cd525b --- /dev/null +++ b/cypress/fixtures/manual-partial-execution.json @@ -0,0 +1,107 @@ +{ + "meta": { + "templateCredsSetupCompleted": true + }, + "nodes": [ + { + "parameters": { + "options": {} + }, + "id": "f4467143-fdb9-46fa-8020-6417cc5eea7d", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 1140, + 260 + ] + }, + { + "parameters": { + "path": "30ff316d-405f-4288-a0ac-e713546c9d4e", + "options": {} + }, + "id": "4760aafb-5d56-4633-99d3-7a97c576a216", + "name": "Webhook1", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + 680, + 340 + ], + "webhookId": "30ff316d-405f-4288-a0ac-e713546c9d4e" + }, + { + "parameters": { + "articleId": "123", + "additionalFields": {} + }, + "id": "8c811eca-8978-44d9-b8f7-ef2c7725784c", + "name": "Hacker News", + "type": "n8n-nodes-base.hackerNews", + "typeVersion": 1, + "position": [ + 920, + 260 + ] + }, + { + "parameters": { + "path": "4a3398e4-1388-4e10-9d21-add90b804955", + "options": {} + }, + "id": "1c2c2d06-45c9-4712-9fa0-c655bef8d0e5", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + 680, + 180 + ], + "webhookId": "4a3398e4-1388-4e10-9d21-add90b804955" + } + ], + "connections": { + "Webhook1": { + "main": [ + [ + { + "node": "Hacker News", + "type": "main", + "index": 0 + } + ] + ] + }, + "Hacker News": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook": { + "main": [ + [ + { + "node": "Hacker News", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "Webhook": [ + { + "name": "First item", + "code": 1 + } + ] + } +} diff --git a/cypress/fixtures/open_node_creator_for_connection.json b/cypress/fixtures/open_node_creator_for_connection.json index 78827d4083..3f693e538c 100644 --- a/cypress/fixtures/open_node_creator_for_connection.json +++ b/cypress/fixtures/open_node_creator_for_connection.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "25ff0c17-7064-4e14-aec6-45c71d63201b", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -107,4 +107,4 @@ }, "id": "L3tgfoW660UOSuX6", "tags": [] -} \ No newline at end of file +} diff --git a/cypress/fixtures/responses/execution-out-of-memory-server-response.json b/cypress/fixtures/responses/execution-out-of-memory-server-response.json new file mode 100644 index 0000000000..377e8170d0 --- /dev/null +++ b/cypress/fixtures/responses/execution-out-of-memory-server-response.json @@ -0,0 +1,81 @@ +{ + "data": { + "id": "29932", + "finished": false, + "mode": "manual", + "retryOf": null, + "retrySuccessId": null, + "status": "crashed", + "startedAt": "2024-05-27T09:54:23.848Z", + "stoppedAt": "2024-05-27T09:54:23.868Z", + "deletedAt": null, + "workflowId": "fGNYufZFmt9XRsNy", + "waitTill": null, + "data": "[{\"resultData\":\"1\"},{\"runData\":\"2\",\"error\":\"3\"},{\"When clicking \\\"Execute Workflow\\\"\":\"4\",\"Code\":\"5\"},{\"message\":\"6\",\"timestamp\":1716803796412,\"name\":\"7\",\"context\":\"8\"},[\"9\"],[\"10\"],\"Workflow did not finish, possible out-of-memory issue\",\"WorkflowOperationError\",{},{\"startTime\":0,\"executionTime\":0,\"source\":\"11\",\"executionStatus\":\"12\"},{\"startTime\":1716803663,\"executionTime\":19,\"source\":\"13\",\"executionStatus\":\"14\",\"data\":\"15\"},[null],\"unknown\",[null],\"success\",{\"main\":\"16\"},[\"17\"],[\"18\"],{\"json\":\"19\"},{\"isArtificialRecoveredEventItem\":true}]", + "workflowData": { + "id": "fGNYufZFmt9XRsNy", + "name": "My workflow 24", + "nodes": [ + { + "parameters": { + "notice": "" + }, + "id": "b4ef3357-de92-41c4-bc0d-526a736f463d", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 240, + 640 + ] + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "language": "javaScript", + "jsCode": "\nif ($input.all().length == 1) {\n return {\n \"blob\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pharetra ut ligula in commodo. Donec fermentum lacus ut faucibus egestas. Aliquam vitae vestibulum urna, a rutrum turpis. Ut consectetur sodales lorem quis mattis. Nullam rhoncus sapien ex, id ullamcorper neque viverra vel. Mauris vitae ex vitae augue pulvinar eleifend. Mauris ultricies neque eget sapien blandit tincidunt. Suspendisse cursus interdum bibendum. Morbi sodales posuere lobortis. Curabitur faucibus mollis lectus, ut sodales magna ultrices vel. Curabitur ligula sapien, porttitor eu dolor quis, vehicula placerat odio. Pellentesque iaculis rutrum nisi, nec cursus turpis ultricies sed. Sed convallis finibus tellus, et elementum lacus efficitur ut. Duis id ante malesuada ante tristique vulputate. Vivamus porta mauris id lorem semper tristique a venenatis dui. Fusce a sapien eget eros blandit pretium quis eu nisi. Donec sit amet odio maximus, eleifend mi sed, imperdiet ante. Sed sollicitudin nulla nec dolor gravida vulputate. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus dapibus suscipit ex tincidunt convallis. Fusce ut dignissim augue, vitae vehicula felis. Nullam ut ipsum vitae mi lacinia pharetra. Aenean venenatis imperdiet ipsum. Pellentesque malesuada ornare metus ac aliquet. Curabitur blandit tellus quis scelerisque dapibus. Nunc suscipit dignissim maximus. Etiam vehicula sapien et sapien lacinia cursus. Curabitur porta, risus nec finibus ultrices, nibh ante semper tellus, ut consequat velit nibh nec arcu. Duis eleifend eget quam in imperdiet. Donec bibendum erat tristique, dapibus quam ac, mattis justo. Praesent fermentum, erat in lobortis faucibus, lacus est posuere nisi, at feugiat erat risus eu erat. Etiam pellentesque, arcu nec ultrices fermentum, augue purus imperdiet quam, sit amet tempor lacus leo venenatis metus. Sed a commodo ex. Fusce eget vulputate sem. Duis tincidunt ultricies orci eu dictum. Integer placerat tellus ac sapien malesuada, vel malesuada ligula ornare. Cras massa nibh, tristique sed felis et, elementum tincidunt lectus. Nulla ullamcorper libero in dui laoreet, eget cursus mauris pulvinar. Curabitur porttitor tristique libero, in molestie ligula ullamcorper vitae. Phasellus ornare lacus a tellus hendrerit molestie. Suspendisse a tempor lectus. Morbi sapien arcu, imperdiet vel lacinia sit amet, fringilla non ligula. Phasellus rhoncus, dui a laoreet ultrices, eros ipsum varius massa, at ultricies enim neque ut arcu. Pellentesque mattis neque quis purus tincidunt mattis. Donec euismod ante id nulla accumsan, vitae interdum augue mollis. Quisque id vestibulum velit, sed fringilla magna. Sed sollicitudin justo eu odio porttitor, ac tincidunt purus interdum. Phasellus lobortis laoreet neque a auctor. Fusce id orci leo. Phasellus congue nec erat et aliquam. Nulla in nulla eu lacus tempus elementum. Fusce scelerisque eleifend ante at suscipit. Nunc nunc felis, vehicula eu porta at, dapibus quis sem. Vivamus vel facilisis justo. Vestibulum feugiat pretium turpis eu tempor. Nunc fermentum urna ac sem semper, vel scelerisque metus sodales. Suspendisse sit amet nisi justo. Nunc accumsan lacus pulvinar ipsum sollicitudin, id commodo mauris suscipit. Nam efficitur ultrices dolor, aliquet facilisis diam vehicula quis. Proin posuere at lorem vel dignissim. Vivamus in felis augue. Fusce vel nisl ex. Duis quis felis eu neque posuere tempus at nec mi. Mauris malesuada viverra arcu placerat ullamcorper. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Sed non varius felis, vitae bibendum nisl. Sed vitae hendrerit metus, efficitur venenatis dui. Integer ac aliquam arcu. Fusce nulla dui, posuere et vehicula at, lacinia et quam. Vivamus vestibulum placerat sem nec ultrices. Praesent eget est quis ex consequat congue. Pellentesque fringilla sem vitae nisi eleifend, sit amet mollis velit feugiat. Cras quis tincidunt nunc. Etiam elementum scelerisque nulla ac tempus. Praesent in nibh convallis, eleifend orci eget, semper lectus. Integer mollis, magna et rutrum consectetur, tortor urna eleifend mauris, a porttitor enim diam non risus. Maecenas ligula sapien, gravida condimentum quam vitae, dignissim varius ligula. Phasellus ultricies est ut maximus tempus. Praesent fringilla augue justo, vel vestibulum odio dictum luctus. Donec malesuada augue id enim finibus, a ullamcorper nisl posuere. Donec dignissim lacus ut justo rutrum, ut feugiat erat cursus. Pellentesque quam metus, efficitur ut leo quis, lobortis tempus diam. Suspendisse libero augue, auctor nec luctus at, venenatis vel urna. Morbi at aliquam sem. Vestibulum vitae porttitor nisi. In bibendum id sapien consectetur ornare. Vestibulum fermentum risus tellus, quis ornare ex pharetra sed. In imperdiet fermentum odio nec consequat. Aliquam ut diam sagittis, viverra sem at, mattis lorem. Pellentesque id sagittis erat. Proin vestibulum ligula vestibulum, molestie diam vel, auctor lorem. Pellentesque ullamcorper elementum nunc non mollis. Ut non sem ex. Duis imperdiet ligula finibus, dignissim magna quis, placerat velit. Ut in odio molestie, eleifend lectus non, vehicula augue. Nulla in turpis sit amet augue feugiat placerat ac non ligula. Fusce mauris nisl, imperdiet mattis neque quis, pellentesque consectetur ipsum. Donec lacinia tellus eget urna condimentum ornare vitae a urna. Nam at lacinia odio. Nunc in dignissim tortor, id scelerisque nisi. Suspendisse potenti. Etiam rhoncus urna id tortor rutrum commodo. Nunc ultrices mi semper neque pulvinar faucibus. Aliquam sit amet leo diam. Integer eget gravida sem, laoreet scelerisque arcu. Vivamus mattis cursus egestas. Nullam sagittis condimentum diam vitae porttitor. Integer at placerat mauris. Aliquam tempus eu nisi id suscipit. Nulla facilisi. Vivamus urna est, condimentum eu finibus et, porta non ligula. Vivamus eu sem id nunc sagittis dapibus sit amet at nisl. Vivamus aliquam, elit et ultricies congue, magna mi luctus tortor, vitae interdum orci mauris quis nunc. Vestibulum a felis at odio ornare aliquet sed id metus. Suspendisse mattis magna in tincidunt eleifend. Donec rhoncus velit purus, vitae blandit nunc maximus eu. Vivamus mattis mattis mauris, nec pretium tellus aliquam nec. Donec felis nulla, convallis tristique nibh et, facilisis tempus sem. Quisque nec erat felis. Mauris congue mi in egestas sollicitudin. Etiam ut lacus ac tortor lacinia consectetur. Maecenas facilisis justo id dui auctor, ac dapibus nunc convallis. Aliquam id dignissim lorem, vel sagittis metus. In ornare lorem vitae elit luctus volutpat. Donec scelerisque velit a gravida porta. Sed egestas justo felis, nec molestie erat venenatis in. Nulla sed felis vehicula, maximus turpis ac, porttitor neque. Nullam rhoncus consequat urna quis imperdiet. Etiam auctor ligula id nibh placerat, vitae laoreet urna fermentum. Praesent nec vestibulum odio, sed efficitur velit. In hac habitasse platea dictumst. Fusce pulvinar mi diam, ac scelerisque libero pulvinar nec. Vestibulum commodo quam leo. Proin molestie nisi eget lorem vehicula porttitor. Proin ut sem condimentum, convallis risus sed, vehicula dolor. Praesent varius mi ac aliquet cursus. Fusce nec mattis libero. Nullam pretium arcu in ante pellentesque, eu consectetur ex consequat. In orci ligula, bibendum eu pellentesque at, mattis ac felis. Nunc porta tortor id ornare interdum. Duis ac est lacinia, porta ante sed, molestie lorem. Curabitur efficitur non ligula sit amet rhoncus. Mauris nec vestibulum tellus, in accumsan libero. In tincidunt non est id vehicula. Vivamus quis tortor volutpat, dictum est placerat, rutrum tellus. Donec eros purus, congue in quam ut, sollicitudin imperdiet felis. Fusce et metus eget arcu dictum vestibulum. Donec in lacus nulla. Proin ac sem non lectus tempus efficitur vitae in turpis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam sodales porttitor lectus eleifend sodales. Aliquam erat volutpat. In pretium, odio nec interdum cursus, augue ligula euismod ligula, quis dapibus ex elit non libero. Nunc sodales enim erat, nec ultricies ipsum ullamcorper nec. Suspendisse ac purus vitae augue gravida varius. Pellentesque sed lorem eu nulla iaculis placerat. Pellentesque cursus et dolor sodales viverra. Phasellus commodo rhoncus libero eu varius. Aenean feugiat risus et elit commodo aliquam. Suspendisse vel interdum lectus, sit amet finibus tortor. Etiam eu neque nec dui pharetra vulputate. Ut rutrum sed sem a feugiat. Fusce luctus sit amet augue a facilisis. Curabitur faucibus neque non elementum iaculis. Vestibulum euismod nisl eget tellus convallis venenatis. Sed erat nibh, tincidunt vitae sodales eget, malesuada ut ante. Aenean vitae orci ut turpis sodales tincidunt. Aliquam semper hendrerit massa at pulvinar. Duis egestas eleifend massa, faucibus interdum erat consectetur sed. Nullam a vulputate diam, mollis sagittis nunc. Praesent in malesuada velit. Proin suscipit, erat ac aliquet malesuada, nulla turpis ornare urna, at viverra felis odio in dolor. Donec non fermentum nibh. Donec pharetra risus non sapien fringilla hendrerit. Quisque sed urna ante. Vestibulum lorem lacus, tempor nec ipsum ut, maximus lobortis purus. Donec pulvinar aliquet velit, eget accumsan nulla rhoncus vitae. Proin molestie at purus id laoreet. Fusce euismod viverra enim, a mattis erat elementum vitae. Sed vestibulum augue purus, ac tempor ligula fringilla nec. Etiam vel lorem lorem. Aenean vitae quam lorem. Ut egestas velit ut aliquam luctus. Sed eget nibh justo. Quisque id viverra sapien. Vestibulum molestie enim id nisi laoreet aliquam. Proin vestibulum nunc quis justo ullamcorper posuere. Ut eleifend, est id congue sagittis, magna lectus tincidunt metus, in ultricies augue purus non lectus. Vivamus vitae iaculis nisi. Curabitur in dolor imperdiet, aliquet est vel, cursus leo. Ut sagittis neque vitae arcu placerat fermentum. Mauris non sagittis ante. Ut aliquam accumsan dolor et rutrum. Integer lobortis eros vel nisl iaculis pulvinar. Morbi at sodales est. Cras et ullamcorper felis, non mattis dui. Maecenas laoreet tempus odio, eu finibus dolor rutrum ornare. Duis iaculis, est vitae mollis fringilla, nulla sapien vestibulum augue, at tempus nisl enim interdum velit. Pellentesque facilisis nec eros sed consectetur. Fusce a velit sit amet nulla porttitor molestie. Donec vehicula leo in dolor eleifend, in consectetur mi dictum. Donec non efficitur mi. Suspendisse dictum faucibus varius. Donec venenatis massa id arcu tincidunt laoreet. Sed ut tristique mauris. Curabitur condimentum quis risus sed vestibulum. Proin in iaculis mi. In sit amet felis nisi. In euismod, urna eget interdum iaculis, eros felis tincidunt lacus, sit amet imperdiet augue est et nisi. Vestibulum at leo at ex fringilla facilisis ac a lacus. Etiam sed bibendum dolor. Cras est neque, malesuada quis interdum a, sagittis et metus. Nunc commodo tincidunt porta. Vestibulum porta enim quis elit malesuada, eu auctor lorem tristique. Pellentesque ac ante vitae arcu dignissim consectetur eget hendrerit metus. Vestibulum nec posuere tellus. Phasellus mollis urna at erat sagittis, ut ornare odio pellentesque. Nam vel purus tristique, pharetra sapien ac, hendrerit massa. Aenean eget urna eget massa venenatis cursus quis vel risus. Nunc tempor nibh orci, ac tempus justo luctus nec. Maecenas interdum erat ligula, at sollicitudin urna ultrices in. Mauris consequat ipsum et diam maximus sodales. Donec eget risus metus. Fusce lobortis varius turpis a pulvinar. Phasellus diam sem, bibendum at nisl in, molestie condimentum mi. In non urna in mi elementum vestibulum. Proin ultrices nisl nulla, vel semper justo placerat in. Pellentesque sollicitudin faucibus nulla, eu porttitor lacus. Sed a ipsum ut tellus porttitor blandit. Nulla sodales nisi vel ligula pulvinar tempus. Praesent ornare viverra mauris et vulputate. In in nisl in urna pellentesque tincidunt. Maecenas lacus nibh, ultrices in magna ut, faucibus faucibus felis. Ut placerat vestibulum molestie. Cras cursus purus eget dui maximus, non cursus eros pellentesque. Morbi lacinia hendrerit dolor vitae varius. Sed lobortis nunc at ligula mollis, sed interdum lorem pretium. Praesent posuere nisi tortor, nec commodo lorem blandit in. Pellentesque sit amet nunc iaculis, maximus neque vel, finibus erat. Suspendisse interdum metus finibus, congue lorem vel, venenatis elit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aliquam ac massa nec diam laoreet consectetur. Nullam at fringilla neque. Fusce mollis, est et imperdiet euismod, tortor tellus scelerisque lorem, quis dictum enim risus volutpat sem. Etiam feugiat lacus non pellentesque tristique. Suspendisse odio elit, tristique ut enim id, gravida interdum massa. Integer eget bibendum purus. Proin sagittis erat vel interdum semper. Aenean aliquet posuere nisl, eleifend porta mi pharetra non. Aliquam tincidunt magna sit amet turpis iaculis varius. Maecenas quis velit ut felis tristique gravida. Vestibulum interdum odio et arcu bibendum posuere. Phasellus semper ut tellus a sodales. Phasellus dignissim eu elit vel accumsan. Nulla ut nisi sed lacus tempor vulputate. Suspendisse sagittis at sem a feugiat. Nunc accumsan velit urna, a pretium arcu sodales at. In tempor pretium odio, eget tristique nisl finibus ut. Praesent sed erat nunc. Mauris fringilla tortor sem, et pharetra enim volutpat vitae. Morbi eleifend congue erat, id accumsan quam efficitur quis. Quisque id diam sed sem dapibus rutrum. Phasellus vitae felis a lorem rhoncus tempus. Nullam faucibus purus nec dictum condimentum. Curabitur in finibus sapien. Maecenas malesuada consequat dignissim. Ut sed nunc ornare, egestas nulla vel, ultrices elit. Vestibulum a nibh nulla. Vivamus nec sem gravida mauris ultrices interdum. Quisque accumsan rutrum urna sed faucibus. Pellentesque vehicula ipsum ut nisl imperdiet blandit. Integer sit amet congue neque. Mauris vestibulum venenatis erat, id efficitur neque imperdiet a. Suspendisse pellentesque quis turpis sed luctus. Suspendisse potenti. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Praesent ac accumsan diam. Mauris ut eros dapibus, ornare sem sed, porttitor ipsum. Morbi ac consequat mauris, nec fermentum ipsum. Vivamus libero sem, egestas non nisi venenatis, egestas posuere nibh. Quisque auctor libero augue, efficitur maximus nulla tincidunt vehicula. Duis viverra sapien quis turpis fringilla, vitae pharetra nisi sodales. Aliquam imperdiet augue non urna vulputate sollicitudin. In faucibus nulla in tincidunt fermentum. Donec hendrerit at quam pretium sagittis. Vestibulum vehicula sit amet felis ut gravida. Ut egestas, metus a pharetra varius, odio justo posuere diam, eget rhoncus magna mauris sed eros. Fusce felis dolor, tincidunt at fringilla ut, condimentum sit amet nulla. In rhoncus nisi mi, eget gravida magna blandit vitae. Aenean et suscipit ipsum, quis consectetur ex. Duis ligula urna, accumsan at sollicitudin id, volutpat vel diam. Nam dapibus mollis finibus. Nam consequat augue eu ultricies lobortis. Sed lorem augue, blandit blandit interdum eget, aliquet nec dolor. Donec convallis neque gravida dolor tincidunt ultricies. Vivamus feugiat nunc eu lorem commodo, ac blandit arcu convallis. Nunc eget ipsum vel urna lacinia vehicula non ac lorem. Nam varius scelerisque diam, vel pharetra orci pretium vitae. Ut ipsum augue, imperdiet fermentum mollis eu, rhoncus quis felis. Quisque at libero vitae augue vestibulum feugiat sed vel orci. Praesent commodo in libero id varius. Sed ac dolor pellentesque, efficitur massa a, egestas libero. Sed faucibus metus vel ultrices mattis. Vivamus eu augue ipsum. In nec felis ut ante ultricies varius. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam venenatis mi at porta consequat. Donec sit amet purus non ex hendrerit vulputate ut non tellus. Aenean consequat, sem quis molestie finibus, eros sapien porta ipsum, in condimentum tellus metus non lorem. Curabitur quis lorem quis enim porttitor luctus. Sed at ante non dolor ornare bibendum et eget ex. Cras volutpat dignissim nulla, ultricies tempus dui. Aliquam et ex in lorem fringilla fermentum in luctus nisl. Morbi ac arcu tempus, viverra leo vitae, cursus magna. Aliquam erat volutpat. Donec massa lacus, ullamcorper in laoreet et, venenatis at felis. Nulla aliquet ligula vel pretium vehicula. Nulla ac purus vitae sem commodo congue. Suspendisse suscipit lorem sit amet blandit sollicitudin. Curabitur et consectetur dui. Donec finibus placerat diam et rutrum. Aenean tellus quam, suscipit non elementum sed, bibendum nec nunc. Maecenas fermentum pulvinar nulla, a finibus velit consequat vitae. Vestibulum vehicula lobortis consequat. Sed in gravida est. Cras ac eros eget sem congue tincidunt. Aliquam sollicitudin venenatis erat, non ultricies est vulputate ut. Phasellus laoreet arcu at mattis faucibus. Aliquam convallis diam non nibh bibendum pellentesque. Nam rutrum erat vel augue gravida dictum. Nulla lorem urna, tempus rutrum semper sollicitudin, semper quis leo. Mauris lacinia, lacus in commodo placerat, libero nisl viverra magna, sed pellentesque ligula elit vel lorem. In tempor est interdum dui bibendum, ut imperdiet diam pulvinar. Vestibulum euismod convallis nunc ut viverra. Aenean pellentesque a turpis varius semper. Nunc pharetra ac ante eget rhoncus. Donec accumsan purus ipsum, eget elementum eros commodo a. Nunc eu leo id odio hendrerit pretium vitae ac neque. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec tempor, lorem nec placerat dictum, lacus quam laoreet dui, in accumsan massa erat eget sapien. Nullam aliquam iaculis purus ut sollicitudin. Nam sagittis nibh id felis luctus sollicitudin. Aenean a ante volutpat, dignissim augue vel, tristique arcu. Duis convallis posuere magna eu aliquam. Cras non ligula eget magna varius dignissim. Cras lectus ante, feugiat sit amet risus eget, consectetur vulputate metus. Cras et mauris ac sem dictum malesuada. Nam vel nisi purus. Suspendisse quis ullamcorper augue. Nulla sit amet eros bibendum, auctor dolor vitae, ultricies neque. In venenatis nec est a porta. Mauris justo dolor, egestas sit amet risus vitae, ultricies interdum enim. Proin pretium eleifend metus, non mollis tortor ullamcorper at. Quisque nibh mi, lobortis nec massa in, laoreet tincidunt nulla. Fusce eget maximus arcu. Nam quis metus risus. Donec arcu ipsum, feugiat nec velit in, vulputate gravida turpis. Maecenas nec erat elit. Pellentesque sollicitudin posuere elementum. Nam purus sapien, tincidunt non bibendum vitae, tincidunt sit amet orci. Quisque eget velit ut ligula ullamcorper viverra in sed nisi. Praesent facilisis, orci sed tempus ullamcorper, tellus lorem congue leo, sed mollis mauris magna eu libero. Morbi ut vehicula erat. Mauris non nisi mi. Aliquam erat volutpat. Phasellus varius risus eget turpis gravida commodo. Morbi magna neque, convallis eget dolor lacinia, vulputate ultrices eros. Duis et ligula semper, interdum felis eget, pulvinar ipsum. Quisque sed purus tristique, sollicitudin ex id, congue tellus. Aliquam viverra finibus est quis auctor. Aenean risus justo, semper elementum ultrices at, fringilla eget enim. Praesent ut consectetur quam. Duis at fringilla mauris. Duis vestibulum finibus feugiat. Aliquam fringilla luctus pretium. Integer in dui mattis, auctor nulla ac, varius quam. Pellentesque efficitur sollicitudin odio non sollicitudin. Sed ultrices venenatis sodales. Aliquam viverra semper diam at interdum. Nam leo velit, efficitur nec venenatis vitae, pulvinar non mi. Phasellus eleifend nulla id orci vehicula, non efficitur metus tempor. Mauris ac tempus massa, sed volutpat nisi. Vivamus maximus nibh quis maximus dapibus. Maecenas metus elit, viverra vel nisl ut, faucibus accumsan ante. Cras at odio id sapien tempor viverra. Integer rutrum mauris ut sapien porta bibendum. Vestibulum consequat dignissim metus eu varius. Praesent lacinia sit amet dui imperdiet rhoncus. Vivamus consequat nunc augue, sit amet feugiat urna dapibus eget. Etiam sed ex augue. Praesent ut massa finibus, hendrerit odio ac, sagittis elit. Cras arcu nibh, commodo nec sapien ac, dictum rhoncus nisi. Vestibulum pharetra massa in orci varius, vitae accumsan sapien convallis. Pellentesque pellentesque luctus lacus nec euismod. Integer elementum a elit in condimentum. Mauris id orci eros. Sed sed libero laoreet turpis commodo iaculis sit amet vel tortor. Integer purus libero, tristique vel justo in, efficitur vehicula metus. Maecenas non sodales nulla. Phasellus id sodales dui. Nam porta nisl sit amet elit ornare, a maximus orci iaculis. Nullam non tortor mi. Aliquam elit lorem, varius et mauris eget, luctus porta risus. Maecenas vulputate mollis purus, ac varius urna interdum at. In hac habitasse platea dictumst. Quisque eget mauris ultrices, ullamcorper nisl vitae, interdum ante. Integer leo tellus, molestie non dignissim nec, interdum a nibh. Morbi sed ex ut magna aliquam efficitur. Donec lobortis tellus tempor, fringilla arcu sit amet, sollicitudin sapien. Aenean felis purus, consequat vitae vulputate at, tincidunt ut magna. Etiam ac sapien orci. Phasellus sed arcu accumsan, rhoncus tellus a, porttitor ligula. Ut in felis accumsan, facilisis sem scelerisque, dictum nisl. Etiam laoreet pharetra scelerisque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Vestibulum a nisi mollis, aliquet dolor nec, tempor nunc. Nam vehicula posuere accumsan. Aenean eget pretium magna. Fusce porttitor neque eget tempus fermentum. Ut ut consectetur enim. Maecenas erat nulla, ultricies faucibus libero dignissim, interdum varius arcu. Vestibulum risus lorem, elementum et venenatis in, efficitur vitae lacus. Nunc vitae lorem at felis condimentum scelerisque vel vitae nulla. Aliquam pretium suscipit orci, eleifend condimentum ligula fermentum sit amet. Quisque aliquam eros eu neque porta, et efficitur orci suscipit. Nullam congue pharetra metus vel aliquet. Praesent pellentesque eget sem et tristique. Nunc facilisis nisi sed dolor semper, non consequat enim eleifend. Maecenas sagittis, turpis ut pellentesque rhoncus, erat massa iaculis nulla, at aliquet mauris nunc id nisl. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam quis ipsum pulvinar, tempus augue vitae, euismod ipsum. Maecenas tincidunt quam in pharetra consequat. Aenean sit amet ligula in ante viverra aliquet sit amet vel odio. Aliquam ac eros faucibus, mollis ipsum a, consequat massa. Phasellus rhoncus elit neque. Suspendisse finibus quam pulvinar rutrum mollis. In hac habitasse platea dictumst. Mauris lobortis ut sem non pellentesque. Sed non molestie lectus. Proin condimentum justo ac quam fermentum iaculis. Suspendisse non nunc porttitor, maximus orci at, efficitur elit. Maecenas risus mi, finibus ut consectetur quis, porta eget sapien. Etiam id diam tincidunt, volutpat felis et, finibus lectus. Pellentesque non laoreet lectus, non imperdiet arcu. Aenean eros ipsum, condimentum id velit id, aliquet consequat ipsum. Donec sodales ipsum id lobortis dictum. In eget magna mauris. Donec ornare ornare nunc a malesuada. Donec ac nunc vel nibh euismod placerat et at arcu. Nam vitae enim ut lectus lobortis varius. Integer sagittis tellus elit, eget condimentum quam sagittis sit amet. Proin pretium, enim eget commodo eleifend, massa nunc placerat massa, sed placerat dolor libero sagittis tortor. Integer ac sapien cursus, placerat sapien eget, venenatis mi. Aenean elementum risus ut risus accumsan ultrices. Sed elementum nec urna eu gravida. Curabitur dapibus dui elit, et laoreet ipsum consectetur sed. Nam maximus rhoncus maximus. Cras molestie laoreet lectus. Quisque eget posuere dui. Integer vitae nibh euismod, ultricies purus non, porttitor augue. Integer luctus odio felis. Donec pellentesque nunc in massa bibendum feugiat. Donec non nibh euismod, luctus elit sed, mattis enim. Duis commodo tristique augue, et ullamcorper sem pulvinar ut. Nunc egestas porttitor sem sed porttitor. Nam non maximus ipsum. Ut congue, velit in condimentum venenatis, orci nisl imperdiet enim, sit amet posuere justo erat eu justo. Integer nec ultricies velit, ut maximus risus. Sed pellentesque tristique urna eget varius. Vestibulum eu massa urna. Ut eget odio magna. Suspendisse ipsum justo, fermentum at ante in, elementum ultrices erat. Ut volutpat purus lacus, vitae ultricies nisi scelerisque et. Quisque hendrerit sagittis elit, eget fermentum dui. Nam blandit eget est eu ultricies. Nullam non dapibus nibh. Sed posuere molestie ligula at fringilla. Integer vel felis non turpis lacinia porttitor vitae ac metus. Vivamus a mauris et tellus imperdiet rutrum sit amet ac quam. Nam sed leo ultrices, ultrices lorem commodo, eleifend diam. Donec tortor dolor, accumsan facilisis tellus in, consequat vestibulum leo. Phasellus blandit, sem et aliquam facilisis, tellus leo congue velit, eu pretium nisi ligula ac arcu. Pellentesque diam diam, tincidunt vitae urna ut, auctor facilisis massa. Proin ex ex, molestie ac magna ut, rhoncus faucibus lectus. Proin ut placerat augue. Proin mollis mi eget urna finibus malesuada. Etiam tempus mollis feugiat. Morbi vitae lorem et purus sagittis dapibus. Suspendisse potenti. Sed leo sem, pharetra nec lorem viverra, faucibus pretium tortor. Nam eget nisl sit amet arcu malesuada pharetra at vitae erat. Morbi faucibus euismod arcu a pretium. Morbi vehicula risus tellus, in congue sapien sodales ut. Nunc non odio egestas, tristique tortor ac, egestas urna. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Morbi ut vestibulum erat. Nullam porta mauris nulla, at accumsan tellus pharetra eget. Maecenas quis ornare leo. Sed vestibulum nisl sem, ut faucibus leo semper sit amet. Morbi sed metus dui. Pellentesque malesuada in massa quis varius. Suspendisse eget blandit velit, sed luctus mi. Pellentesque feugiat velit sit amet lorem vestibulum, nec ultrices orci tempor. Aliquam erat volutpat. Proin rhoncus gravida mauris ut tristique. Donec pellentesque arcu vitae leo maximus pellentesque. Aliquam faucibus tempus tincidunt. Phasellus sagittis ante sollicitudin egestas pulvinar. Suspendisse semper nulla eget turpis rutrum auctor. Phasellus ac mollis velit, quis finibus turpis. In pretium, tortor non lacinia ultricies, dui mi bibendum eros, at mollis ante orci a metus. In vel ante pellentesque, egestas lectus eget, ornare tellus. Phasellus varius euismod ipsum eget sodales. Mauris vitae lectus mauris. Sed lobortis ac mauris vel consequat. Praesent sollicitudin ligula magna, eget aliquam magna lobortis quis. In tincidunt imperdiet metus quis gravida. Vivamus mattis bibendum pulvinar. Praesent vulputate nibh in felis aliquet lacinia. Ut eget erat ut orci aliquam ornare. Etiam laoreet, neque nec ultrices commodo, ante sem aliquet tellus, sit amet vehicula leo turpis at ante. Donec pulvinar ligula vel enim viverra, nec sollicitudin ex bibendum. Ut vehicula turpis ipsum, fermentum condimentum nisl mattis id. Quisque egestas ex quis orci scelerisque, sit amet tristique metus rutrum. Aenean ac pretium velit. Morbi enim dui, lacinia et diam ac, ornare tincidunt erat. Etiam ultrices orci ut lectus imperdiet, in malesuada mauris tempus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Integer a turpis porttitor, sodales nulla et, finibus neque. Etiam in ipsum in augue viverra dictum. Etiam sit amet metus neque. Praesent sed consequat turpis. Vivamus rutrum nulla ut eleifend sodales. Donec posuere finibus scelerisque. Etiam maximus sollicitudin finibus. Nam mollis, felis eget vehicula laoreet, est libero cursus urna, sit amet malesuada nulla lectus a erat. In ac commodo nisl. Suspendisse sit amet tellus odio. Proin nec lorem tellus. Quisque vestibulum congue turpis vel fringilla. Maecenas eget malesuada elit. Nulla tincidunt iaculis sem nec tristique. Nunc eu lorem vitae arcu posuere pellentesque. Donec tempus mauris quam, ut molestie orci lobortis vitae. Duis nec dui est. Nam vestibulum magna eu lectus ornare, vitae blandit sem varius. Pellentesque vulputate elementum cursus. Sed at ullamcorper lectus. Aliquam erat volutpat. Aliquam sit amet ante nec ex feugiat gravida pharetra et dui. Quisque gravida varius augue, eget venenatis massa finibus at. Maecenas ut arcu non libero suscipit pharetra. Vivamus vulputate tempor est ut faucibus. Nullam tempus tellus eros, ut fermentum magna auctor sed. Sed ut orci quis purus rutrum luctus. Ut eros lacus, pharetra quis rutrum nec, posuere quis magna. Donec mollis, mauris sed pulvinar tempor, erat tortor volutpat enim, a fermentum enim sapien id ante. Nam semper libero quis laoreet pharetra. Curabitur cursus leo semper diam sollicitudin, id congue diam molestie. Suspendisse quam nisi, scelerisque non sollicitudin ut, pellentesque vel tellus. Morbi viverra nisl ut venenatis egestas. Morbi aliquam lectus sit amet neque mattis lacinia. Vivamus vitae vulputate augue. Nunc eget tincidunt urna. Nulla feugiat tempor elit, id scelerisque mauris laoreet vel. Ut at semper dui, nec auctor lorem. Aenean sed magna luctus, mollis tellus in, fringilla nisl. Aenean ante libero, pellentesque eu hendrerit vel, sollicitudin faucibus justo. Curabitur quis pretium turpis. Integer faucibus mi a faucibus iaculis. Cras lacus felis, blandit id hendrerit vel, eleifend eu mi. Donec porttitor aliquam nisi, quis viverra sem efficitur eu. Nam sollicitudin lacinia nulla. Pellentesque cursus quam vel orci aliquam, vitae rutrum urna fermentum. Sed eu elementum ligula. Morbi auctor elit id est lacinia, a consequat dui mattis. Quisque massa leo, laoreet non suscipit suscipit, venenatis nec lacus. Morbi elit velit, varius at varius euismod, blandit eget ipsum. Vivamus sit amet dui ac risus luctus congue. Phasellus dictum, lacus in lacinia malesuada, augue nisi rutrum tellus, a maximus metus neque ac velit. Nam pellentesque et nisi vel lobortis. Integer eleifend aliquam diam, et ultricies arcu ultrices quis. Suspendisse id erat efficitur, rutrum velit in, auctor justo. Donec efficitur dui in vehicula gravida.\"\n }\n}\n\nreturn $input.all().concat($input.all()) ", + "notice": "" + }, + "id": "e5fb96ab-2e0c-4283-96f8-ad2b02fc6eb7", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [ + 460, + 460 + ] + } + ], + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "homeProject": null, + "sharedWithProjects": [], + "usedCredentials": [] + } + } +} diff --git a/cypress/fixtures/templates_search/all_templates_search_response.json b/cypress/fixtures/templates_search/all_templates_search_response.json index 5a0a1eb5ad..fe8ba3e3e4 100644 --- a/cypress/fixtures/templates_search/all_templates_search_response.json +++ b/cypress/fixtures/templates_search/all_templates_search_response.json @@ -2,7 +2,7 @@ "totalWorkflows": 506, "workflows": [ { - "id": 60, + "id": 1, "name": "test1 test1", "totalViews": 120000000, "recentViews": 0, diff --git a/cypress/fixtures/templates_search/test_template_import.json b/cypress/fixtures/templates_search/test_template_import.json deleted file mode 100644 index c77be3db9c..0000000000 --- a/cypress/fixtures/templates_search/test_template_import.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": 60, - "name": "test1 test1", - "workflow": { - "nodes": [ - { - "name": "Start", - "type": "n8n-nodes-base.start", - "position": [ - 250, - 300 - ], - "parameters": {}, - "typeVersion": 1 - } - ], - "connections": {} - } -} diff --git a/cypress/fixtures/templates_search/test_template_preview.json b/cypress/fixtures/templates_search/test_template_preview.json deleted file mode 100644 index 4d3ca1e548..0000000000 --- a/cypress/fixtures/templates_search/test_template_preview.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "workflow": { - "id": 60, - "name": "test1 test1", - "views": 120000000, - "recentViews": 0, - "totalViews": 120000000, - "createdAt": "2019-08-30T16:39:31.362Z", - "description": "here is a description. here is a description. here is a description. \n\n![Screenshot from 20190806 091433.png](fileId:88)", - "workflow": { - "nodes": [ - { - "name": "Start", - "type": "n8n-nodes-base.start", - "position": [ - 250, - 300 - ], - "parameters": {}, - "typeVersion": 1 - } - ], - "connections": {} - }, - "lastUpdatedBy": null, - "workflowInfo": { - "nodeCount": 1, - "nodeTypes": { - "n8n-nodes-base.start": { - "count": 1 - } - } - }, - "user": { - "username": "admin" - }, - "nodes": [ - { - "id": 11, - "icon": "file:amqp.png", - "name": "n8n-nodes-base.amqpTrigger", - "defaults": { - "name": "AMQP Trigger" - }, - "iconData": { - "type": "file", - "fileBuffer": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAB7UlEQVRoge2W4W3CMBCFj26QjkBHSEdIR4AR6Ah0BBgBRqAjhBFgBBghHaEVlV29PN0lDr+o9D7JEjhn+975bJ8JIYQQQgghhBBCCCGEEA9CY2bf0NaBW2uyu7UN2XSOzTyY60J2BzNbObbsH7eTmS2mhHJHE1wmCD7A93ngEAquHaHc2omCcysSXQW74g32BHfwfTEiuCoQm9vuDsEndPYpELxKjjBj0foCEXX6XdM3by3c7aOZPZvZzMzeaBzbIh9pzIuZXaG/RqNIMAq7Ur8XCHQ2kx3LC56DMQ39X4LI23zbAd88ruRHD09wTVF5p+/eBZI5g7O8w5FgXOvsZAI7PxRwS4HGIPbm8wRjBL/Sgp/QNyQYHWySmOxgJBgFeGnPfZHgDVyufET+YMEVCdo7gziCTBbGmRKlGQpCMXOnj+1L6B0JFsxndO3cjjZyjo6OnZeqGb5gqhTQS3qKeK1SwbesfB3IrF/awqu+g8Dgs5SLE37SciHiPUv8rLVp7k2wdl63tDDqgTs8lqpINWGXbSTKe9rlJgXME7C9I6V7oGAWsEzv2gzeN2TstkbCZyIJWBYKWUwtF4foKGU9TpRGdZDSdVDpDNXSVVBLt5TeucS9K6X/E3USX3rshBBCCCGEEEIIIYQQ4tExsx8PuuPnwhCIbgAAAABJRU5ErkJggg==" - }, - "categories": [ - { - "id": 5, - "name": "Development" - }, - { - "id": 6, - "name": "Communication" - } - ], - "displayName": "AMQP Trigger", - "typeVersion": 1 - }, - { - "id": 18, - "icon": "file:autopilot.svg", - "name": "n8n-nodes-base.autopilot", - "defaults": { - "name": "Autopilot" - }, - "iconData": { - "type": "file", - "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjM4IDI2IDM1IDM1Ij48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSIjMThkNGIyIiBzdHJva2Utd2lkdGg9IjMiIGZpbGw9IiMxOGQ0YjIiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDUuNCA0Mi42aDE5LjlsMy40LTQuOEg0MmwzLjQgNC44em0zLjEgOC4zaDEzLjFsMy40LTQuOEg0NS40bDMuMSA0Ljh6bTU0LS43Ii8+PC9zdmc+" - }, - "categories": [ - { - "id": 1, - "name": "Marketing" - } - ], - "displayName": "Autopilot", - "typeVersion": 1 - }, - { - "id": 20, - "icon": "file:lambda.svg", - "name": "n8n-nodes-base.awsLambda", - "defaults": { - "name": "AWS Lambda" - }, - "iconData": { - "type": "file", - "fileBuffer": "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI1MDAiIHZpZXdCb3g9Ii0zLjAyMyAtMC4yMiA0MjAuOTIzIDQzMy41NCIgd2lkdGg9IjI0NDMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIwOC40NSAyMjcuODljLTEuNTkgMi4yNi0yLjkzIDQuMTItNC4yMiA2cS0zMC44NiA0NS40Mi02MS43IDkwLjgzLTI4LjY5IDQyLjI0LTU3LjQ0IDg0LjQzYTMuODggMy44OCAwIDAxLTIuNzMgMS41OXEtNDAuNTktLjM1LTgxLjE2LS44OGMtLjMgMC0uNjEtLjA5LTEuMi0uMThhMTQuNDQgMTQuNDQgMCAwMS43Ni0xLjY1cTI4LjMxLTQzLjg5IDU2LjYyLTg3Ljc2IDI1LjExLTM4Ljg4IDUwLjI1LTc3Ljc0IDI3Ljg2LTQzLjE4IDU1LjY5LTg2LjQyYzIuNzQtNC4yNSA1LjU5LTguNDIgOC4xOS0xMi43NWE1LjI2IDUuMjYgMCAwMC41Ni0zLjgzYy01LTE1Ljk0LTEwLjEtMzEuODQtMTUuMTktNDcuNzQtMi4xOC02LjgxLTQuNDYtMTMuNTgtNi41LTIwLjQzLS42Ni0yLjItMS43NS0yLjg3LTQtMi44Ni0xNyAuMDctMzMuOS4wNS01MC44NS4wNS0zLjIyIDAtMy4yMyAwLTMuMjMtMy4xOCAwLTIwLjg0IDAtNDEuNjgtLjA2LTYyLjUyIDAtMi4zMi43Ni0yLjg0IDIuOTQtMi44NHE1MS4xOS4wOSAxMDIuNCAwYTMuMjkgMy4yOSAwIDAxMy42IDIuNDNxMjcgNjcuOTEgNTQgMTM1Ljc3IDMxLjUgNzkuMTQgNjMgMTU4LjNjNi41MiAxNi4zOCAxMy4wOSAzMi43NSAxOS41NCA0OS4xNy43NyAyIDEuNTcgMi4zOCAzLjU5IDEuNzYgMTcuODktNS41MyAzNS44Mi0xMC45MSA1My43LTE2LjQ1IDIuMjUtLjcgMy4wNy0uMjMgMy43NyAyIDYuMSAxOS4xNyAxMi4zMiAzOC4zIDE4LjUgNTcuNDUuMjEuNjYuMzcgMS4zMy42MiAyLjI1LTEuMjguNDctMi40OCAxLTMuNzEgMS4zNHEtNjEgMTkuMzMtMTIxLjkzIDM4LjY4Yy0xLjk0LjYxLTIuNTItLjA1LTMuMTctMS42OHEtMTguNjEtNDcuMTYtMzcuMzEtOTQuMjgtMTguMjktNDYuMTQtMzYuNi05Mi4yOGMtMS44My00LjYyLTMuNjMtOS4yNi01LjQ2LTEzLjg4LS4yOS0uNzktLjY5LTEuNDgtMS4yNy0yLjd6IiBmaWxsPSIjZmE3ZTE0Ii8+PC9zdmc+" - }, - "categories": [ - { - "id": 5, - "name": "Development" - } - ], - "displayName": "AWS Lambda", - "typeVersion": 1 - }, - { - "id": 40, - "icon": "file:clearbit.svg", - "name": "n8n-nodes-base.clearbit", - "defaults": { - "name": "Clearbit" - }, - "iconData": { - "type": "file", - "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI3MiIgaGVpZ2h0PSI3MiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iNTAlIiB4Mj0iMTAwJSIgeTE9IjAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI0RFRjJGRSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI0RCRjFGRSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJiIiB4MT0iMCUiIHgyPSI1MCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM1N0JDRkQiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM1MUI1RkQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjM3LjUlIiB4Mj0iNjIuNSUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiMxQ0E3RkQiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMxNDhDRkMiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGZpbGw9InVybCgjYSkiIGQ9Ik03MiAzNnYxNi43N2wtLjAwNC44NjhjLS4wNiA2LjAzNS0uNzUgOC4zNTMtMiAxMC42ODhhMTMuNjMgMTMuNjMgMCAwMS01LjY3IDUuNjdsLS4zMjYuMTcxQzYxLjY1OCA3MS4zNjQgNTkuMTYgNzIgNTIuNzcgNzJIMzZWMzZoMzZ6Ii8+PHBhdGggZmlsbD0idXJsKCNiKSIgZD0iTTY0LjMyNiAyLjAwM2ExMy42MyAxMy42MyAwIDAxNS42NyA1LjY3bC4xNzEuMzI3QzcxLjM2NCAxMC4zNDIgNzIgMTIuODQgNzIgMTkuMjNWMzZIMzZWMGgxNi43N2M2LjY4NyAwIDkuMTEyLjY5NiAxMS41NTYgMi4wMDN6Ii8+PHBhdGggZmlsbD0idXJsKCNjKSIgZD0iTTM2IDB2NzJIMTkuMjNsLS44NjgtLjAwNGMtNi4wMzUtLjA2LTguMzUzLS43NS0xMC42ODgtMmExMy42MyAxMy42MyAwIDAxLTUuNjctNS42N0wxLjgzMiA2NEMuNjM2IDYxLjY1OCAwIDU5LjE2IDAgNTIuNzdWMTkuMjNjMC02LjY4Ny42OTYtOS4xMTIgMi4wMDMtMTEuNTU2YTEzLjYzIDEzLjYzIDAgMDE1LjY3LTUuNjdMOCAxLjgzMkMxMC4zNDIuNjM2IDEyLjg0IDAgMTkuMjMgMEgzNnoiLz48L2c+PC9zdmc+" - }, - "categories": [ - { - "id": 2, - "name": "Sales" - } - ], - "displayName": "Clearbit", - "typeVersion": 1 - }, - { - "id": 51, - "icon": "file:convertKit.svg", - "name": "n8n-nodes-base.convertKitTrigger", - "defaults": { - "name": "ConvertKit Trigger" - }, - "iconData": { - "type": "file", - "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTcyIiBoZWlnaHQ9IjE2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNODIuNzIgMTI2LjMxNmMyOS43NyAwIDUyLjc4LTIyLjYyMiA1Mi43OC01MC41MjYgMC0yNi4xNDMtMjEuNjE3LTQyLjEwNi0zNS45MzUtNDIuMTA2LTE5Ljk0NSAwLTM1LjkzIDE0LjA4NC0zOC4xOTggMzQuOTg4LS40MTggMy44NTYtMy40NzYgNy4wOS03LjM1NSA3LjA2MS02LjQyMy0uMDQ2LTE1Ljc0Ni0uMS0yMS42NTgtLjA4LTIuNTU1LjAwOC00LjY2OS0yLjA2NS00LjU0My00LjYxOC44OS0xOC4xMjMgNi45MTQtMzUuMDcgMTguNDAyLTQ4LjA4N0M1OC45NzYgOC40ODggNzcuNTYxIDAgOTkuNTY1IDBjMzYuOTY5IDAgNzEuODY5IDMzLjc4NiA3MS44NjkgNzUuNzkgMCA0Ni41MDgtMzguMzEyIDg0LjIxLTg3LjkyNyA4NC4yMS0zNS4zODQgMC03MS4wMjEtMjMuMjU4LTgzLjQ2NC01NS43NzVhLjcwMi43MDIgMCAwMS0uMDMtLjM3N2MuMTY1LS45NjIuNDk0LTEuODQxLjgxOC0yLjcwNy40NzEtMS4yNTguOTMxLTIuNDg4Ljg2NC0zLjkwNmwtLjIxNS00LjUyOWE1LjUyMyA1LjUyMyAwIDAxMy4xOC01LjI2M2wxLjc5OC0uODQyYTYuOTgyIDYuOTgyIDAgMDAzLjkxMi01LjA3NSA2Ljk5MyA2Ljk5MyAwIDAxNi44ODctNS43MzZjNS4yODIgMCA5Ljg3NSAzLjUxNSAxMS41OSA4LjUxMiA4LjMwNyAyNC4yMTIgMjEuNTExIDQyLjAxNCA1My44NzMgNDIuMDE0eiIgZmlsbD0iI0ZCNjk3MCIvPjwvc3ZnPg==" - }, - "categories": [ - { - "id": 1, - "name": "Marketing" - }, - { - "id": 2, - "name": "Sales" - } - ], - "displayName": "ConvertKit Trigger", - "typeVersion": 1 - } - ], - "categories": [], - "image": [] - } -} diff --git a/cypress/fixtures/workflow-with-unknown-credentials.json b/cypress/fixtures/workflow-with-unknown-credentials.json index 142422227c..9a04cd87e5 100644 --- a/cypress/fixtures/workflow-with-unknown-credentials.json +++ b/cypress/fixtures/workflow-with-unknown-credentials.json @@ -27,7 +27,7 @@ { "parameters": {}, "id": "acdd1bdc-c642-4ea6-ad67-f4201b640cfa", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -37,7 +37,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/workflow-with-unknown-nodes.json b/cypress/fixtures/workflow-with-unknown-nodes.json index 5ea0189e50..3406e512d7 100644 --- a/cypress/fixtures/workflow-with-unknown-nodes.json +++ b/cypress/fixtures/workflow-with-unknown-nodes.json @@ -6,7 +6,7 @@ { "parameters": {}, "id": "40720511-19b6-4421-bdb0-3fb6efef4bc5", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -64,7 +64,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/package.json b/cypress/package.json new file mode 100644 index 0000000000..0e1ce734c0 --- /dev/null +++ b/cypress/package.json @@ -0,0 +1,31 @@ +{ + "name": "n8n-cypress", + "private": true, + "scripts": { + "typecheck": "tsc --noEmit", + "cypress:install": "cypress install", + "test:e2e:ui": "scripts/run-e2e.js ui", + "test:e2e:dev": "scripts/run-e2e.js dev", + "test:e2e:all": "scripts/run-e2e.js all", + "format": "prettier --write . --ignore-path ../.prettierignore", + "lint": "eslint . --quiet", + "lintfix": "eslint . --fix", + "develop": "cd ..; pnpm dev", + "start": "cd ..; pnpm start" + }, + "devDependencies": { + "@types/lodash": "catalog:", + "eslint-plugin-cypress": "^3.3.0", + "n8n-workflow": "workspace:*" + }, + "dependencies": { + "@ngneat/falso": "^7.2.0", + "@sinonjs/fake-timers": "^11.2.2", + "cypress": "^13.11.0", + "cypress-otp": "^1.0.3", + "cypress-real-events": "^1.12.0", + "lodash": "catalog:", + "nanoid": "catalog:", + "start-server-and-test": "^2.0.3" + } +} diff --git a/cypress/pages/bannerStack.ts b/cypress/pages/bannerStack.ts index dce3222126..c4936891ae 100644 --- a/cypress/pages/bannerStack.ts +++ b/cypress/pages/bannerStack.ts @@ -4,5 +4,6 @@ export class BannerStack extends BasePage { getters = { banner: () => cy.getByTestId('banner-stack'), }; + actions = {}; } diff --git a/cypress/pages/base.ts b/cypress/pages/base.ts index 06c832591c..abd7a210a8 100644 --- a/cypress/pages/base.ts +++ b/cypress/pages/base.ts @@ -1,6 +1,7 @@ -import { IE2ETestPage, IE2ETestPageElement } from '../types'; +import type { IE2ETestPage } from '../types'; export class BasePage implements IE2ETestPage { - getters: Record = {}; - actions: Record void> = {}; + getters = {}; + + actions = {}; } diff --git a/cypress/pages/credentials.ts b/cypress/pages/credentials.ts index 24ec88565d..9b20b48ec4 100644 --- a/cypress/pages/credentials.ts +++ b/cypress/pages/credentials.ts @@ -1,7 +1,8 @@ import { BasePage } from './base'; export class CredentialsPage extends BasePage { - url = '/credentials'; + url = '/home/credentials'; + getters = { emptyListCreateCredentialButton: () => cy.getByTestId('empty-resources-list').find('button'), createCredentialButton: () => cy.getByTestId('resources-list-add'), @@ -17,12 +18,15 @@ export class CredentialsPage extends BasePage { this.getters.credentialCard(credentialName).findChildByTestId('credential-card-actions'), credentialDeleteButton: () => cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'), + credentialMoveButton: () => + cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Move'), sort: () => cy.getByTestId('resources-list-sort').first(), sortOption: (label: string) => cy.getByTestId('resources-list-sort-item').contains(label).first(), filtersTrigger: () => cy.getByTestId('resources-list-filters-trigger'), filtersDropdown: () => cy.getByTestId('resources-list-filters-dropdown'), }; + actions = { search: (searchString: string) => { const searchInput = this.getters.searchInput(); diff --git a/cypress/pages/demo.ts b/cypress/pages/demo.ts index 0590fb8def..7e67b79254 100644 --- a/cypress/pages/demo.ts +++ b/cypress/pages/demo.ts @@ -2,20 +2,19 @@ * Actions */ -export function vistDemoPage(theme?: 'dark' | 'light') { +export function visitDemoPage(theme?: 'dark' | 'light') { const query = theme ? `?theme=${theme}` : ''; cy.visit('/workflows/demo' + query); cy.waitForLoad(); cy.window().then((win) => { - // @ts-ignore win.preventNodeViewBeforeUnload = true; }); } export function importWorkflow(workflow: object) { - const OPEN_WORKFLOW = {command: 'openWorkflow', workflow}; - cy.window().then($window => { + const OPEN_WORKFLOW = { command: 'openWorkflow', workflow }; + cy.window().then(($window) => { const message = JSON.stringify(OPEN_WORKFLOW); - $window.postMessage(message, '*') + $window.postMessage(message, '*'); }); } diff --git a/cypress/pages/features/node-creator.ts b/cypress/pages/features/node-creator.ts index 3e6a819443..a0d3995160 100644 --- a/cypress/pages/features/node-creator.ts +++ b/cypress/pages/features/node-creator.ts @@ -1,8 +1,8 @@ import { BasePage } from '../base'; -import { INodeTypeDescription } from 'n8n-workflow'; export class NodeCreator extends BasePage { url = '/workflow/new'; + getters = { plusButton: () => cy.getByTestId('node-creator-plus-button'), canvasAddButton: () => cy.getByTestId('canvas-add-button'), @@ -25,6 +25,7 @@ export class NodeCreator extends BasePage { expandedCategories: () => this.getters.creatorItem().find('>div').filter('.active').invoke('text'), }; + actions = { openNodeCreator: () => { this.getters.plusButton().click(); @@ -33,31 +34,5 @@ export class NodeCreator extends BasePage { selectNode: (displayName: string) => { this.getters.getCreatorItem(displayName).click(); }, - toggleCategory: (category: string) => { - this.getters.getCreatorItem(category).click(); - }, - categorizeNodes: (nodes: INodeTypeDescription[]) => { - const categorizedNodes = nodes.reduce((acc, node) => { - const categories = (node?.codex?.categories || []).map((category: string) => - category.trim(), - ); - - categories.forEach((category: { [key: string]: INodeTypeDescription[] }) => { - // Node creator should show only the latest version of a node - const newerVersion = nodes.find( - (n: INodeTypeDescription) => - n.name === node.name && (n.version > node.version || Array.isArray(n.version)), - ); - - if (acc[category] === undefined) { - acc[category] = []; - } - acc[category].push(newerVersion ?? node); - }); - return acc; - }, {}); - - return categorizedNodes; - }, }; } diff --git a/cypress/pages/mfa-login.ts b/cypress/pages/mfa-login.ts index 50ca5adab7..ae4d916ba9 100644 --- a/cypress/pages/mfa-login.ts +++ b/cypress/pages/mfa-login.ts @@ -5,6 +5,7 @@ import { WorkflowsPage } from './workflows'; export class MfaLoginPage extends BasePage { url = '/mfa'; + getters = { form: () => cy.getByTestId('mfa-login-form'), token: () => cy.getByTestId('token'), diff --git a/cypress/pages/modals/credentials-modal.ts b/cypress/pages/modals/credentials-modal.ts index 08a258a057..f14f6be0b7 100644 --- a/cypress/pages/modals/credentials-modal.ts +++ b/cypress/pages/modals/credentials-modal.ts @@ -9,15 +9,16 @@ export class CredentialsModal extends BasePage { newCredentialTypeOption: (credentialType: string) => cy.getByTestId('new-credential-type-select-option').contains(credentialType), newCredentialTypeButton: () => cy.getByTestId('new-credential-type-button'), - connectionParameters: () => cy.getByTestId('credential-connection-parameter'), connectionParameter: (fieldName: string) => - this.getters.connectionParameters().find(`:contains('${fieldName}') .n8n-input input`), + this.getters.credentialInputs().find(`:contains('${fieldName}') .n8n-input input`), name: () => cy.getByTestId('credential-name'), nameInput: () => cy.getByTestId('credential-name').find('input'), // Saving of the credentials takes a while on the CI so we need to increase the timeout saveButton: () => cy.getByTestId('credential-save-button', { timeout: 5000 }), deleteButton: () => cy.getByTestId('credential-delete-button'), closeButton: () => this.getters.editCredentialModal().find('.el-dialog__close').first(), + oauthConnectButton: () => cy.getByTestId('oauth-connect-button'), + oauthConnectSuccessBanner: () => cy.getByTestId('oauth-connect-success-banner'), credentialsEditModal: () => cy.getByTestId('credential-edit-dialog'), credentialsAuthTypeSelector: () => cy.getByTestId('node-auth-type-selector'), credentialAuthTypeRadioButtons: () => @@ -25,9 +26,10 @@ export class CredentialsModal extends BasePage { credentialInputs: () => cy.getByTestId('credential-connection-parameter'), menu: () => this.getters.editCredentialModal().get('.menu-container'), menuItem: (name: string) => this.getters.menu().get('.n8n-menu-item').contains(name), - usersSelect: () => cy.getByTestId('credential-sharing-modal-users-select'), + usersSelect: () => cy.getByTestId('project-sharing-select').filter(':visible'), testSuccessTag: () => cy.getByTestId('credentials-config-container-test-success'), }; + actions = { addUser: (email: string) => { this.getters.usersSelect().click(); @@ -45,7 +47,7 @@ export class CredentialsModal extends BasePage { if (test) cy.wait('@testCredential'); this.getters.saveButton().should('contain.text', 'Saved'); }, - saveSharing: (test = false) => { + saveSharing: () => { cy.intercept('PUT', '/rest/credentials/*/share').as('shareCredential'); this.getters.saveButton().click({ force: true }); cy.wait('@shareCredential'); @@ -54,7 +56,7 @@ export class CredentialsModal extends BasePage { close: () => { this.getters.closeButton().click(); }, - fillCredentialsForm: () => { + fillCredentialsForm: (closeModal = true) => { this.getters.credentialsEditModal().should('be.visible'); this.getters.credentialInputs().should('have.length.greaterThan', 0); this.getters @@ -64,14 +66,30 @@ export class CredentialsModal extends BasePage { cy.wrap($el).type('test'); }); this.getters.saveButton().click(); - this.getters.closeButton().click(); + if (closeModal) { + this.getters.closeButton().click(); + } + }, + fillField: (fieldName: string, value: string) => { + this.getters + .credentialInputs() + .getByTestId(`parameter-input-${fieldName}`) + .find('input') + .type(value); + }, + createNewCredential: (type: string, closeModal = true) => { + this.getters.newCredentialModal().should('be.visible'); + this.getters.newCredentialTypeSelect().should('be.visible'); + this.getters.newCredentialTypeOption(type).click(); + this.getters.newCredentialTypeButton().click(); + this.actions.fillCredentialsForm(closeModal); }, renameCredential: (newName: string) => { this.getters.nameInput().type('{selectall}'); this.getters.nameInput().type(newName); this.getters.nameInput().type('{enter}'); }, - changeTab: (tabName: string) => { + changeTab: (tabName: 'Sharing') => { this.getters.menuItem(tabName).click(); }, }; diff --git a/cypress/pages/modals/message-box.ts b/cypress/pages/modals/message-box.ts index b54e375ef6..a40c2d1a88 100644 --- a/cypress/pages/modals/message-box.ts +++ b/cypress/pages/modals/message-box.ts @@ -8,6 +8,7 @@ export class MessageBox extends BasePage { confirm: () => this.getters.modal().find('.btn--confirm').first(), cancel: () => this.getters.modal().find('.btn--cancel').first(), }; + actions = { confirm: () => { this.getters.confirm().click({ force: true }); diff --git a/cypress/pages/modals/workflow-sharing-modal.ts b/cypress/pages/modals/workflow-sharing-modal.ts index c013093286..02e183fc81 100644 --- a/cypress/pages/modals/workflow-sharing-modal.ts +++ b/cypress/pages/modals/workflow-sharing-modal.ts @@ -3,10 +3,11 @@ import { BasePage } from '../base'; export class WorkflowSharingModal extends BasePage { getters = { modal: () => cy.getByTestId('workflowShare-modal', { timeout: 5000 }), - usersSelect: () => cy.getByTestId('workflow-sharing-modal-users-select'), + usersSelect: () => cy.getByTestId('project-sharing-select'), saveButton: () => cy.getByTestId('workflow-sharing-modal-save-button'), closeButton: () => this.getters.modal().find('.el-dialog__close').first(), }; + actions = { addUser: (email: string) => { this.getters.usersSelect().click(); diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index f9cb5b1304..018ec43a5d 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -1,5 +1,5 @@ -import { BasePage } from './base'; import { getVisiblePopper, getVisibleSelect } from '../utils'; +import { BasePage } from './base'; export class NDV extends BasePage { getters = { @@ -27,6 +27,7 @@ export class NDV extends BasePage { nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'), savePinnedDataButton: () => this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'), + inputLabel: () => cy.getByTestId('input-label'), outputTableRows: () => this.getters.outputDataContainer().find('table tr'), outputTableHeaders: () => this.getters.outputDataContainer().find('table thead th'), outputTableHeaderByText: (text: string) => this.getters.outputTableHeaders().contains(text), @@ -40,6 +41,12 @@ export class NDV extends BasePage { this.getters.inputTableRow(row).find('td').eq(col), inlineExpressionEditorInput: () => cy.getByTestId('inline-expression-editor-input'), inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'), + inlineExpressionEditorItemInput: () => + cy.getByTestId('inline-expression-editor-item-input').find('input'), + inlineExpressionEditorItemPrevButton: () => + cy.getByTestId('inline-expression-editor-item-prev'), + inlineExpressionEditorItemNextButton: () => + cy.getByTestId('inline-expression-editor-item-next'), nodeParameters: () => cy.getByTestId('node-parameters'), parameterInput: (parameterName: string) => cy.getByTestId(`parameter-input-${parameterName}`), parameterInputIssues: (parameterName: string) => @@ -57,7 +64,9 @@ export class NDV extends BasePage { httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'), nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n), inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'), + inputLinkRun: () => this.getters.inputPanel().findChildByTestId('link-run'), outputRunSelector: () => this.getters.outputPanel().findChildByTestId('run-selector'), + outputLinkRun: () => this.getters.outputPanel().findChildByTestId('link-run'), outputHoveringItem: () => this.getters.outputPanel().findChildByTestId('hovering-item'), inputHoveringItem: () => this.getters.inputPanel().findChildByTestId('hovering-item'), outputBranches: () => this.getters.outputPanel().findChildByTestId('branches'), @@ -68,8 +77,11 @@ export class NDV extends BasePage { resourceLocatorDropdown: (paramName: string) => this.getters.resourceLocator(paramName).find('[data-test-id="resource-locator-dropdown"]'), resourceLocatorErrorMessage: () => cy.getByTestId('rlc-error-container'), + resourceLocatorAddCredentials: () => this.getters.resourceLocatorErrorMessage().find('a'), resourceLocatorModeSelector: (paramName: string) => this.getters.resourceLocator(paramName).find('[data-test-id="rlc-mode-selector"]'), + resourceLocatorSearch: (paramName: string) => + this.getters.resourceLocator(paramName).findChildByTestId('rlc-search'), resourceMapperFieldsContainer: () => cy.getByTestId('mapping-fields-container'), resourceMapperSelectColumn: () => cy.getByTestId('matching-column-select'), resourceMapperRemoveFieldButton: (fieldName: string) => @@ -78,6 +90,7 @@ export class NDV extends BasePage { cy.getByTestId('columns-parameter-input-options-container'), resourceMapperRemoveAllFieldsOption: () => cy.getByTestId('action-removeAllFields'), sqlEditorContainer: () => cy.getByTestId('sql-editor-container'), + htmlEditorContainer: () => cy.getByTestId('html-editor-container'), filterComponent: (paramName: string) => cy.getByTestId(`filter-${paramName}`), filterCombinator: (paramName: string, index = 0) => this.getters.filterComponent(paramName).getByTestId('filter-combinator-select').eq(index), @@ -118,6 +131,12 @@ export class NDV extends BasePage { codeEditorFullscreen: () => this.getters.codeEditorDialog().find('.cm-content'), nodeRunSuccessIndicator: () => cy.getByTestId('node-run-info-success'), nodeRunErrorIndicator: () => cy.getByTestId('node-run-info-danger'), + nodeRunErrorMessage: () => cy.getByTestId('node-error-message'), + nodeRunErrorDescription: () => cy.getByTestId('node-error-description'), + fixedCollectionParameter: (paramName: string) => + cy.getByTestId(`fixed-collection-${paramName}`), + schemaViewNode: () => cy.getByTestId('run-data-schema-node'), + schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'), }; actions = { @@ -140,18 +159,14 @@ export class NDV extends BasePage { cy.contains('Expression').invoke('show').click(); this.getters.inlineExpressionEditorInput().click(); }, - setPinnedData: (data: object) => { + setPinnedData: (data: object | string) => { + const pinnedData = typeof data === 'string' ? data : JSON.stringify(data); this.getters.editPinnedDataButton().click(); this.getters.pinnedDataEditor().click(); this.getters .pinnedDataEditor() - .type( - `{selectall}{backspace}${JSON.stringify(data).replace(new RegExp('{', 'g'), '{{}')}`, - { - delay: 0, - }, - ); + .type(`{selectall}{backspace}${pinnedData.replace(new RegExp('{', 'g'), '{{}')}`); this.actions.savePinnedData(); }, @@ -159,24 +174,21 @@ export class NDV extends BasePage { this.getters.editPinnedDataButton().click(); this.getters.pinnedDataEditor().click(); - this.getters - .pinnedDataEditor() - .type('{selectall}{backspace}', { delay: 0 }) - .paste(JSON.stringify(data)); + this.getters.pinnedDataEditor().type('{selectall}{backspace}').paste(JSON.stringify(data)); this.actions.savePinnedData(); }, clearParameterInput: (parameterName: string) => { - this.getters.parameterInput(parameterName).type(`{selectall}{backspace}`); + this.getters.parameterInput(parameterName).type('{selectall}{backspace}'); }, typeIntoParameterInput: ( parameterName: string, content: string, - opts?: { parseSpecialCharSequences: boolean; delay?: number }, + opts?: { parseSpecialCharSequences: boolean }, ) => { this.getters.parameterInput(parameterName).type(content, opts); }, - selectOptionInParameterDropdown: (parameterName: string, content: string) => { + selectOptionInParameterDropdown: (_: string, content: string) => { getVisibleSelect().find('.option-headline').contains(content).click(); }, rename: (newName: string) => { @@ -206,6 +218,9 @@ export class NDV extends BasePage { this.getters.inputSelect().find('.el-select').click(); this.getters.inputOption().contains(nodeName).click(); }, + expandSchemaViewNode: (nodeName: string) => { + this.getters.schemaViewNodeName().contains(nodeName).click(); + }, addDefaultPinnedData: () => { this.actions.editPinnedData(); this.actions.savePinnedData(); @@ -219,10 +234,10 @@ export class NDV extends BasePage { getVisibleSelect().find('.el-select-dropdown__item').contains(runName).click(); }, toggleOutputRunLinking: () => { - this.getters.outputRunSelector().find('button').click(); + this.getters.outputLinkRun().click(); }, toggleInputRunLinking: () => { - this.getters.inputRunSelector().find('button').click(); + this.getters.inputLinkRun().click(); }, switchOutputBranch: (name: string) => { this.getters.outputBranches().get('span').contains(name).click(); @@ -263,18 +278,15 @@ export class NDV extends BasePage { setInvalidExpression: ({ fieldName, invalidExpression, - delay, }: { fieldName: string; invalidExpression?: string; - delay?: number; }) => { this.actions.typeIntoParameterInput(fieldName, '='); this.actions.typeIntoParameterInput(fieldName, invalidExpression ?? "{{ $('unknown')", { parseSpecialCharSequences: false, - delay, }); - this.actions.validateExpressionPreview(fieldName, `node doesn't exist`); + this.actions.validateExpressionPreview(fieldName, "node doesn't exist"); }, openSettings: () => { this.getters.nodeSettingsTab().click(); @@ -290,6 +302,18 @@ export class NDV extends BasePage { .click({ force: true }); this.getters.parameterInput('operation').find('input').should('have.value', operation); }, + expressionSelectItem: (index: number) => { + this.getters.inlineExpressionEditorItemInput().type(`{selectall}${index}`); + }, + expressionSelectNextItem: () => { + this.getters.inlineExpressionEditorItemNextButton().click(); + }, + expressionSelectPrevItem: () => { + this.getters.inlineExpressionEditorItemPrevButton().click(); + }, + addItemToFixedCollection: (paramName: string) => { + this.getters.fixedCollectionParameter(paramName).getByTestId('fixed-collection-add').click(); + }, }; } diff --git a/cypress/pages/notifications.ts b/cypress/pages/notifications.ts new file mode 100644 index 0000000000..162c536007 --- /dev/null +++ b/cypress/pages/notifications.ts @@ -0,0 +1,17 @@ +type CyGetOptions = Parameters<(typeof cy)['get']>[1]; + +/** + * Getters + */ +export const successToast = () => cy.get('.el-notification:has(.el-notification--success)'); +export const warningToast = () => cy.get('.el-notification:has(.el-notification--warning)'); +export const errorToast = (options?: CyGetOptions) => + cy.get('.el-notification:has(.el-notification--error)', options); +export const infoToast = () => cy.get('.el-notification:has(.el-notification--info)'); + +/** + * Actions + */ +export const clearNotifications = () => { + successToast().find('.el-notification__closeBtn').click({ multiple: true }); +}; diff --git a/cypress/pages/npsSurvey.ts b/cypress/pages/npsSurvey.ts new file mode 100644 index 0000000000..b68d33797d --- /dev/null +++ b/cypress/pages/npsSurvey.ts @@ -0,0 +1,16 @@ +/** + * Getters + */ + +export const getNpsSurvey = () => cy.getByTestId('nps-survey-modal'); + +export const getNpsSurveyRatings = () => cy.getByTestId('nps-survey-ratings'); + +export const getNpsSurveyEmail = () => cy.getByTestId('nps-survey-email'); + +export const getNpsSurveyClose = () => + cy.getByTestId('nps-survey-modal').find('button.el-drawer__close-btn'); + +/** + * Actions + */ diff --git a/cypress/pages/settings-community-nodes.ts b/cypress/pages/settings-community-nodes.ts new file mode 100644 index 0000000000..454dc95e21 --- /dev/null +++ b/cypress/pages/settings-community-nodes.ts @@ -0,0 +1,22 @@ +export const getCommunityCards = () => { + return cy.getByTestId('community-package-card'); +}; + +export const visitCommunityNodesSettings = () => { + cy.visit('/settings/community-nodes'); +}; + +export const installFirstCommunityNode = (nodeName: string) => { + cy.getByTestId('action-box').find('button').click(); + cy.getByTestId('communityPackageInstall-modal').find('input').eq(0).type(nodeName); + cy.getByTestId('user-agreement-checkbox').click(); + cy.getByTestId('install-community-package-button').click(); +}; + +export const confirmCommunityNodeUpdate = () => { + cy.getByTestId('communityPackageManageConfirm-modal').find('button').eq(1).click(); +}; + +export const confirmCommunityNodeUninstall = () => { + cy.getByTestId('communityPackageManageConfirm-modal').find('button').eq(1).click(); +}; diff --git a/cypress/pages/settings-log-streaming.ts b/cypress/pages/settings-log-streaming.ts index 2d056a4444..cc1ea1250d 100644 --- a/cypress/pages/settings-log-streaming.ts +++ b/cypress/pages/settings-log-streaming.ts @@ -1,8 +1,9 @@ -import { BasePage } from './base'; import { getVisibleSelect } from '../utils'; +import { BasePage } from './base'; export class SettingsLogStreamingPage extends BasePage { url = '/settings/log-streaming'; + getters = { getActionBoxUnlicensed: () => cy.getByTestId('action-box-unlicensed'), getActionBoxLicensed: () => cy.getByTestId('action-box-licensed'), @@ -17,6 +18,7 @@ export class SettingsLogStreamingPage extends BasePage { getDestinationDeleteButton: () => cy.getByTestId('destination-delete-button'), getDestinationCards: () => cy.getByTestId('destination-card'), }; + actions = { clickContactUs: () => this.getters.getContactUsButton().click(), clickAddFirstDestination: () => this.getters.getAddFirstDestinationButton().click(), diff --git a/cypress/pages/settings-personal.ts b/cypress/pages/settings-personal.ts index 716625beb5..9872fbc668 100644 --- a/cypress/pages/settings-personal.ts +++ b/cypress/pages/settings-personal.ts @@ -1,13 +1,14 @@ +import generateOTPToken from 'cypress-otp'; import { ChangePasswordModal } from './modals/change-password-modal'; import { MfaSetupModal } from './modals/mfa-setup-modal'; import { BasePage } from './base'; -import generateOTPToken from 'cypress-otp'; const changePasswordModal = new ChangePasswordModal(); const mfaSetupModal = new MfaSetupModal(); export class PersonalSettingsPage extends BasePage { url = '/settings/personal'; + secret = ''; getters = { @@ -23,11 +24,13 @@ export class PersonalSettingsPage extends BasePage { themeSelector: () => cy.getByTestId('theme-select'), selectOptionsVisible: () => cy.get('.el-select-dropdown:visible .el-select-dropdown__item'), }; + actions = { changeTheme: (theme: 'System default' | 'Dark' | 'Light') => { this.getters.themeSelector().click(); this.getters.selectOptionsVisible().should('have.length', 3); this.getters.selectOptionsVisible().contains(theme).click(); + this.getters.saveSettingsButton().realClick(); }, loginAndVisit: (email: string, password: string) => { cy.signin({ email, password }); diff --git a/cypress/pages/settings-usage.ts b/cypress/pages/settings-usage.ts index cd3dfd3596..85300fe05f 100644 --- a/cypress/pages/settings-usage.ts +++ b/cypress/pages/settings-usage.ts @@ -2,6 +2,8 @@ import { BasePage } from './base'; export class SettingsUsagePage extends BasePage { url = '/settings/usage'; + getters = {}; + actions = {}; } diff --git a/cypress/pages/settings-users.ts b/cypress/pages/settings-users.ts index e3c80e5bcc..d188896225 100644 --- a/cypress/pages/settings-users.ts +++ b/cypress/pages/settings-users.ts @@ -11,6 +11,7 @@ const settingsSidebar = new SettingsSidebar(); export class SettingsUsersPage extends BasePage { url = '/settings/users'; + getters = { setUpOwnerButton: () => cy.getByTestId('action-box').find('button').first(), inviteButton: () => cy.getByTestId('settings-users-invite-button').last(), @@ -34,6 +35,7 @@ export class SettingsUsersPage extends BasePage { deleteUserButton: () => this.getters.confirmDeleteModal().find('button:contains("Delete")'), deleteDataInput: () => cy.getByTestId('delete-data-input').find('input').first(), }; + actions = { goToOwnerSetup: () => this.getters.setUpOwnerButton().click(), loginAndVisit: (email: string, password: string, isOwner: boolean) => { @@ -41,10 +43,10 @@ export class SettingsUsersPage extends BasePage { workflowPage.actions.visit(); mainSidebar.actions.goToSettings(); if (isOwner) { - settingsSidebar.getters.menuItem('Users').click(); + settingsSidebar.getters.users().click(); cy.url().should('match', new RegExp(this.url)); } else { - settingsSidebar.getters.menuItem('Users').should('not.exist'); + settingsSidebar.getters.users().should('not.exist'); // Should be redirected to workflows page if trying to access UM url cy.visit('/settings/users'); cy.url().should('match', new RegExp(workflowsPage.url)); diff --git a/cypress/pages/settings.ts b/cypress/pages/settings.ts index 264b525dee..74c3b0fe76 100644 --- a/cypress/pages/settings.ts +++ b/cypress/pages/settings.ts @@ -2,8 +2,10 @@ import { BasePage } from './base'; export class SettingsPage extends BasePage { url = '/settings'; + getters = { menuItems: () => cy.getByTestId('menu-item'), }; + actions = {}; } diff --git a/cypress/pages/sidebar/main-sidebar.ts b/cypress/pages/sidebar/main-sidebar.ts index 5379b1f889..4266b93688 100644 --- a/cypress/pages/sidebar/main-sidebar.ts +++ b/cypress/pages/sidebar/main-sidebar.ts @@ -1,27 +1,24 @@ import { BasePage } from '../base'; import { WorkflowsPage } from '../workflows'; -const workflowsPage = new WorkflowsPage(); - export class MainSidebar extends BasePage { getters = { - menuItem: (menuLabel: string) => - cy.getByTestId('menu-item').filter(`:contains("${menuLabel}")`), - settings: () => this.getters.menuItem('Settings'), - templates: () => this.getters.menuItem('Templates'), - workflows: () => this.getters.menuItem('Workflows'), - credentials: () => this.getters.menuItem('Credentials'), - executions: () => this.getters.menuItem('Executions'), - adminPanel: () => this.getters.menuItem('Admin Panel'), - userMenu: () => cy.get('div[class="action-dropdown-container"]'), + menuItem: (id: string) => cy.getByTestId('menu-item').get('#' + id), + settings: () => this.getters.menuItem('settings'), + settingsBack: () => cy.getByTestId('settings-back'), + templates: () => this.getters.menuItem('templates'), + workflows: () => this.getters.menuItem('workflows'), + credentials: () => this.getters.menuItem('credentials'), + executions: () => this.getters.menuItem('executions'), + adminPanel: () => this.getters.menuItem('cloud-admin'), + userMenu: () => cy.getByTestId('user-menu'), logo: () => cy.getByTestId('n8n-logo'), }; + actions = { goToSettings: () => { - this.getters.settings().should('be.visible'); - // We must wait before ElementUI menu is done with its animations - cy.get('[data-old-overflow]').should('not.exist'); - this.getters.settings().click(); + this.getters.userMenu().click(); + cy.getByTestId('user-menu-item-settings').should('be.visible').click(); }, goToCredentials: () => { this.getters.credentials().should('be.visible'); @@ -31,8 +28,8 @@ export class MainSidebar extends BasePage { openUserMenu: () => { this.getters.userMenu().click(); }, - openUserMenu: () => { - this.getters.userMenu().click(); + closeSettings: () => { + this.getters.settingsBack().click(); }, signout: () => { const workflowsPage = new WorkflowsPage(); diff --git a/cypress/pages/sidebar/settings-sidebar.ts b/cypress/pages/sidebar/settings-sidebar.ts index 6d519d6c31..17d43b65e7 100644 --- a/cypress/pages/sidebar/settings-sidebar.ts +++ b/cypress/pages/sidebar/settings-sidebar.ts @@ -2,11 +2,11 @@ import { BasePage } from '../base'; export class SettingsSidebar extends BasePage { getters = { - menuItem: (menuLabel: string) => - cy.getByTestId('menu-item').filter(`:contains("${menuLabel}")`), - users: () => this.getters.menuItem('Users'), + menuItem: (id: string) => cy.getByTestId('menu-item').get('#' + id), + users: () => this.getters.menuItem('settings-users'), back: () => cy.getByTestId('settings-back'), }; + actions = { goToUsers: () => { this.getters.users().should('be.visible'); diff --git a/cypress/pages/signin.ts b/cypress/pages/signin.ts index 1b2b35c22f..22d0fd163a 100644 --- a/cypress/pages/signin.ts +++ b/cypress/pages/signin.ts @@ -4,6 +4,7 @@ import { WorkflowsPage } from './workflows'; export class SigninPage extends BasePage { url = '/signin'; + getters = { form: () => cy.getByTestId('auth-form'), email: () => cy.getByTestId('email'), diff --git a/cypress/pages/template-credential-setup.ts b/cypress/pages/template-credential-setup.ts index d673261fdf..3fa4d20671 100644 --- a/cypress/pages/template-credential-setup.ts +++ b/cypress/pages/template-credential-setup.ts @@ -1,22 +1,6 @@ -import { CredentialsModal, MessageBox } from './modals'; import * as formStep from '../composables/setup-template-form-step'; import { overrideFeatureFlag } from '../composables/featureFlags'; - -export type TemplateTestData = { - id: number; - fixture: string; -}; - -export const testData = { - simpleTemplate: { - id: 1205, - fixture: 'Test_Template_1.json', - }, - templateWithoutCredentials: { - id: 1344, - fixture: 'Test_Template_2.json', - }, -}; +import { CredentialsModal, MessageBox } from './modals'; const credentialsModal = new CredentialsModal(); const messageBox = new MessageBox(); diff --git a/cypress/pages/template-workflow.ts b/cypress/pages/template-workflow.ts deleted file mode 100644 index d1e8630a12..0000000000 --- a/cypress/pages/template-workflow.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { BasePage } from './base'; - -export class TemplateWorkflowPage extends BasePage { - url = '/templates'; - - getters = { - useTemplateButton: () => cy.get('[data-test-id="use-template-button"]'), - description: () => cy.get('[data-test-id="template-description"]'), - }; - - actions = { - visit: (templateId: number) => { - cy.visit(`${this.url}/${templateId}`); - }, - - clickUseThisWorkflowButton: () => { - this.getters.useTemplateButton().click(); - }, - - openTemplate: (template: { - workflow: { - id: number; - name: string; - description: string; - user: { username: string }; - image: { id: number; url: string }[]; - }; - }, templateHost: string) => { - cy.intercept('GET', `${templateHost}/api/templates/workflows/${template.workflow.id}`, { - statusCode: 200, - body: template, - }).as('getTemplate'); - - this.actions.visit(template.workflow.id); - cy.wait('@getTemplate'); - }, - }; -} diff --git a/cypress/pages/templates.ts b/cypress/pages/templates.ts index 4c0225be48..a17da87ba2 100644 --- a/cypress/pages/templates.ts +++ b/cypress/pages/templates.ts @@ -5,6 +5,7 @@ export class TemplatesPage extends BasePage { getters = { useTemplateButton: () => cy.getByTestId('use-template-button'), + description: () => cy.getByTestId('template-description'), templateCards: () => cy.getByTestId('template-card'), firstTemplateCard: () => this.getters.templateCards().first(), allCategoriesFilter: () => cy.getByTestId('template-filter-all-categories'), @@ -14,50 +15,30 @@ export class TemplatesPage extends BasePage { collectionCountLabel: () => cy.getByTestId('collection-count-label'), templateCountLabel: () => cy.getByTestId('template-count-label'), templatesLoadingContainer: () => cy.getByTestId('templates-loading-container'), - expandCategoriesButton: () => cy.getByTestId('expand-categories-button'), }; actions = { - openSingleTemplateView: (templateId: number) => { - cy.visit(`${this.url}/${templateId}`); - cy.waitForLoad(); - }, - - openOnboardingFlow: (id: number, name: string, workflow: object, templatesHost: string) => { - const apiResponse = { - id, - name, - workflow, - }; + openOnboardingFlow: () => { cy.intercept('POST', '/rest/workflows').as('createWorkflow'); - cy.intercept('GET', `${templatesHost}/api/workflows/templates/${id}`, { - statusCode: 200, - body: apiResponse, - }).as('getTemplate'); cy.intercept('GET', 'rest/workflows/**').as('getWorkflow'); - cy.visit(`/workflows/onboarding/${id}`); + cy.visit('/workflows/onboarding/1'); + cy.window().then((win) => { + win.preventNodeViewBeforeUnload = true; + }); - cy.wait('@getTemplate'); - cy.wait(['@createWorkflow', '@getWorkflow']); + cy.wait(['@getTemplate', '@createWorkflow', '@getWorkflow']); }, - importTemplate: (id: number, name: string, workflow: object, templatesHost: string) => { - const apiResponse = { - id, - name, - workflow, - }; - cy.intercept('GET', `${templatesHost}/api/workflows/templates/${id}`, { - statusCode: 200, - body: apiResponse, - }).as('getTemplate'); + importTemplate: () => { cy.intercept('GET', 'rest/workflows/**').as('getWorkflow'); - cy.visit(`/workflows/templates/${id}`); + cy.visit('/workflows/templates/1'); + cy.window().then((win) => { + win.preventNodeViewBeforeUnload = true; + }); - cy.wait('@getTemplate'); - cy.wait('@getWorkflow'); + cy.wait(['@getTemplate', '@getWorkflow']); }, }; } diff --git a/cypress/pages/variables.ts b/cypress/pages/variables.ts index 6091e5cf1b..c74624686e 100644 --- a/cypress/pages/variables.ts +++ b/cypress/pages/variables.ts @@ -3,6 +3,7 @@ import Chainable = Cypress.Chainable; export class VariablesPage extends BasePage { url = '/variables'; + getters = { unavailableResourcesList: () => cy.getByTestId('unavailable-resources-list'), emptyResourcesList: () => cy.getByTestId('empty-resources-list'), @@ -14,7 +15,7 @@ export class VariablesPage extends BasePage { createVariableButton: () => cy.getByTestId('resources-list-add'), variablesRows: () => cy.getByTestId('variables-row'), variablesEditableRows: () => - cy.getByTestId('variables-row').filter((index, row) => !!row.querySelector('input')), + cy.getByTestId('variables-row').filter((_, row) => !!row.querySelector('input')), variableRow: (key: string) => this.getters.variablesRows().contains(key).parents('[data-test-id="variables-row"]'), editableRowCancelButton: (row: Chainable>) => @@ -35,7 +36,7 @@ export class VariablesPage extends BasePage { deleteVariable: (key: string) => { const row = this.getters.variableRow(key); row.within(() => { - cy.getByTestId('variable-row-delete-button').click(); + cy.getByTestId('variable-row-delete-button').should('not.be.disabled').click(); }); const modal = cy.get('[role="dialog"]'); @@ -53,7 +54,7 @@ export class VariablesPage extends BasePage { editRow: (key: string) => { const row = this.getters.variableRow(key); row.within(() => { - cy.getByTestId('variable-row-edit-button').click(); + cy.getByTestId('variable-row-edit-button').should('not.be.disabled').click(); }); }, setRowValue: (row: Chainable>, field: 'key' | 'value', value: string) => { diff --git a/cypress/pages/workerView.ts b/cypress/pages/workerView.ts index e14bfd36a2..f442468c52 100644 --- a/cypress/pages/workerView.ts +++ b/cypress/pages/workerView.ts @@ -2,6 +2,7 @@ import { BasePage } from './base'; export class WorkerViewPage extends BasePage { url = '/settings/workers'; + getters = { workerCards: () => cy.getByTestId('worker-card'), workerCard: (workerId: string) => this.getters.workerCards().contains(workerId), diff --git a/cypress/pages/workflow-executions-tab.ts b/cypress/pages/workflow-executions-tab.ts index eb855f026f..27285d28b8 100644 --- a/cypress/pages/workflow-executions-tab.ts +++ b/cypress/pages/workflow-executions-tab.ts @@ -24,7 +24,9 @@ export class WorkflowExecutionsTab extends BasePage { executionPreviewId: () => this.getters.executionPreviewDetails().find('[data-test-id="execution-preview-id"]'), executionDebugButton: () => cy.getByTestId('execution-debug-button'), + workflowExecutionPreviewIframe: () => cy.getByTestId('workflow-preview-iframe'), }; + actions = { toggleNodeEnabled: (nodeName: string) => { workflowPage.getters.canvasNodeByName(nodeName).click(); @@ -32,7 +34,7 @@ export class WorkflowExecutionsTab extends BasePage { }, createManualExecutions: (count: number) => { for (let i = 0; i < count; i++) { - cy.intercept('POST', '/rest/workflows/run').as('workflowExecution'); + cy.intercept('POST', '/rest/workflows/**/run').as('workflowExecution'); workflowPage.actions.executeWorkflow(); cy.wait('@workflowExecution'); } diff --git a/cypress/pages/workflow-history.ts b/cypress/pages/workflow-history.ts index 18cd6ed999..1b9d7328b1 100644 --- a/cypress/pages/workflow-history.ts +++ b/cypress/pages/workflow-history.ts @@ -1,7 +1,7 @@ -import { BasePage } from "./base"; +import { BasePage } from './base'; export class WorkflowHistoryPage extends BasePage { - getters = { - workflowHistoryCloseButton: () => cy.getByTestId('workflow-history-close-button'), - } + getters = { + workflowHistoryCloseButton: () => cy.getByTestId('workflow-history-close-button'), + }; } diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index a3443c81b5..0c2a269607 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -1,13 +1,13 @@ import { META_KEY } from '../constants'; -import { BasePage } from './base'; import { getVisibleSelect } from '../utils'; +import { getUniqueWorkflowName } from '../utils/workflowUtils'; +import { BasePage } from './base'; import { NodeCreator } from './features/node-creator'; -type CyGetOptions = Parameters<(typeof cy)['get']>[1]; - const nodeCreator = new NodeCreator(); export class WorkflowPage extends BasePage { url = '/workflow/new'; + getters = { workflowNameInputContainer: () => cy.getByTestId('workflow-name-input', { timeout: 5000 }), workflowNameInput: () => @@ -48,11 +48,6 @@ export class WorkflowPage extends BasePage { canvasNodePlusEndpointByName: (nodeName: string, index = 0) => { return cy.get(this.getters.getEndpointSelector('plus', nodeName, index)); }, - successToast: () => cy.get('.el-notification:has(.el-notification--success)'), - warningToast: () => cy.get('.el-notification:has(.el-notification--warning)'), - errorToast: (options?: CyGetOptions) => - cy.get('.el-notification:has(.el-notification--error)', options), - infoToast: () => cy.get('.el-notification:has(.el-notification--info)'), activatorSwitch: () => cy.getByTestId('workflow-activate-switch'), workflowMenu: () => cy.getByTestId('workflow-menu'), firstStepButton: () => cy.getByTestId('canvas-add-button'), @@ -134,18 +129,21 @@ export class WorkflowPage extends BasePage { colors: () => cy.getByTestId('color'), contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`), }; + actions = { - visit: (preventNodeViewUnload = true) => { + visit: (preventNodeViewUnload = true, appDate?: number) => { cy.visit(this.url); + if (appDate) { + cy.setAppDate(appDate); + } cy.waitForLoad(); cy.window().then((win) => { - // @ts-ignore win.preventNodeViewBeforeUnload = preventNodeViewUnload; }); }, addInitialNodeToCanvas: ( nodeDisplayName: string, - opts?: { keepNdvOpen?: boolean; action?: string, isTrigger?: boolean}, + opts?: { keepNdvOpen?: boolean; action?: string; isTrigger?: boolean }, ) => { this.getters.canvasPlusButton().click(); this.getters.nodeCreatorSearchBar().type(nodeDisplayName); @@ -286,7 +284,7 @@ export class WorkflowPage extends BasePage { }, saveWorkflowUsingKeyboardShortcut: () => { cy.intercept('POST', '/rest/workflows').as('createWorkflow'); - cy.get('body').type(META_KEY, { release: false }).type('s'); + this.actions.hitSaveWorkflow(); }, deleteNode: (name: string) => { this.getters.canvasNodeByName(name).first().click(); @@ -298,10 +296,13 @@ export class WorkflowPage extends BasePage { this.getters.workflowNameInput().should('be.enabled'); this.getters.workflowNameInput().clear().type(name).type('{enter}'); }, - activateWorkflow: () => { - cy.intercept('PATCH', '/rest/workflows/*').as('activateWorkflow'); + clickWorkflowActivator: () => { this.getters.activatorSwitch().find('input').first().should('be.enabled'); this.getters.activatorSwitch().click(); + }, + activateWorkflow: () => { + cy.intercept('PATCH', '/rest/workflows/*').as('activateWorkflow'); + this.actions.clickWorkflowActivator(); cy.wait('@activateWorkflow'); cy.get('body').type('{esc}'); }, @@ -311,6 +312,9 @@ export class WorkflowPage extends BasePage { cy.get('body').type(newName); cy.get('body').type('{enter}'); }, + renameWithUniqueName: () => { + this.actions.renameWorkflow(getUniqueWorkflowName()); + }, addTags: (tags: string | string[]) => { if (!Array.isArray(tags)) tags = [tags]; @@ -318,7 +322,6 @@ export class WorkflowPage extends BasePage { this.getters.workflowTagsInput().type(tag); this.getters.workflowTagsInput().type('{enter}'); }); - cy.realPress('Tab'); // For a brief moment the Element UI tag component shows the tags as(+X) string // so we need to wait for it to disappear this.getters.workflowTagsContainer().should('not.contain', `+${tags.length}`); @@ -327,46 +330,56 @@ export class WorkflowPage extends BasePage { cy.getByTestId('zoom-to-fit').click(); }, pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => { - // Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling) - this.getters.nodeViewBackground().trigger('wheel', { - force: true, - bubbles: true, - ctrlKey: true, - pageX: cy.window().innerWidth / 2, - pageY: cy.window().innerHeight / 2, - deltaMode: 1, - deltaY: mode === 'zoomOut' ? steps : -steps, + cy.window().then((win) => { + // Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling) + this.getters.nodeViewBackground().trigger('wheel', { + force: true, + bubbles: true, + ctrlKey: true, + pageX: win.innerWidth / 2, + pageY: win.innerHeight / 2, + deltaMode: 1, + deltaY: mode === 'zoomOut' ? steps : -steps, + }); }); }, + /** Certain keyboard shortcuts are not possible on Cypress via a simple `.type`, and some delays are needed to emulate these events */ + hitComboShortcut: (modifier: string, key: string) => { + cy.get('body').wait(100).type(modifier, { delay: 100, release: false }).type(key); + }, hitUndo: () => { - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('z'); + this.actions.hitComboShortcut(`{${META_KEY}}`, 'z'); }, hitRedo: () => { - cy.get('body') - .type(META_KEY, { delay: 500, release: false }) - .type('{shift}', { release: false }) - .type('z'); + cy.get('body').type(`{${META_KEY}+shift+z}`); }, - selectAll: () => { - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('a'); + hitSelectAll: () => { + this.actions.hitComboShortcut(`{${META_KEY}}`, 'a'); + }, + hitDeleteAllNodes: () => { + this.actions.hitSelectAll(); + cy.get('body').type('{backspace}'); }, hitDisableNodeShortcut: () => { cy.get('body').type('d'); }, hitCopy: () => { - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('c'); + this.actions.hitComboShortcut(`{${META_KEY}}`, 'c'); }, hitPinNodeShortcut: () => { cy.get('body').type('p'); }, - hitExecuteWorkflowShortcut: () => { - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('{enter}'); + hitSaveWorkflow: () => { + cy.get('body').type(`{${META_KEY}+s}`); }, - hitDuplicateNodeShortcut: () => { - cy.get('body').type(META_KEY, { delay: 500, release: false }).type('d'); + hitExecuteWorkflow: () => { + cy.get('body').type(`{${META_KEY}+enter}`); }, - hitAddStickyShortcut: () => { - cy.get('body').type('{shift}', { delay: 500, release: false }).type('S'); + hitDuplicateNode: () => { + cy.get('body').type(`{${META_KEY}+d}`); + }, + hitAddSticky: () => { + cy.get('body').type('{shift+S}'); }, executeWorkflow: () => { this.getters.executeWorkflowButton().click(); @@ -386,11 +399,7 @@ export class WorkflowPage extends BasePage { this.actions.addNodeToCanvas(newNodeName, false, false, action); }, - deleteNodeBetweenNodes: ( - sourceNodeName: string, - targetNodeName: string, - newNodeName: string, - ) => { + deleteNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string) => { this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover(); this.getters .getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName) @@ -413,7 +422,7 @@ export class WorkflowPage extends BasePage { .find('[data-test-id="change-sticky-color"]') .click({ force: true }); }, - pickColor: (index: number) => { + pickColor: () => { this.getters.colors().eq(1).click(); }, editSticky: (content: string) => { diff --git a/cypress/pages/workflows.ts b/cypress/pages/workflows.ts index 56a3c44923..5829ecb863 100644 --- a/cypress/pages/workflows.ts +++ b/cypress/pages/workflows.ts @@ -1,7 +1,8 @@ import { BasePage } from './base'; export class WorkflowsPage extends BasePage { - url = '/workflows'; + url = '/home/workflows'; + getters = { newWorkflowButtonCard: () => cy.getByTestId('new-workflow-card'), newWorkflowTemplateCard: () => cy.getByTestId('new-workflow-template-card'), @@ -23,6 +24,8 @@ export class WorkflowsPage extends BasePage { this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-actions'), workflowDeleteButton: () => cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'), + workflowMoveButton: () => + cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Move'), workflowFilterButton: () => cy.getByTestId('resources-list-filters-trigger').filter(':visible'), workflowTagsDropdown: () => cy.getByTestId('tags-dropdown'), workflowTagItem: (tag: string) => cy.getByTestId('tag').contains(tag), @@ -34,13 +37,6 @@ export class WorkflowsPage extends BasePage { // Not yet implemented // myWorkflows: () => cy.getByTestId('my-workflows'), // allWorkflows: () => cy.getByTestId('all-workflows'), - suggestedTemplatesPageContainer: () => cy.getByTestId('suggested-templates-page-container'), - suggestedTemplatesCards: () => cy.get('.agile__slides--regular [data-test-id=templates-info-card]'), - suggestedTemplatesNewWorkflowButton: () => cy.getByTestId('suggested-templates-new-workflow-button'), - suggestedTemplatesSectionContainer: () => cy.getByTestId('suggested-templates-section-container'), - suggestedTemplatesPreviewModal: () => cy.getByTestId('suggested-templates-preview-modal'), - suggestedTemplatesUseTemplateButton: () => cy.getByTestId('use-template-button'), - suggestedTemplatesSectionDescription: () => cy.getByTestId('suggested-template-section-description'), }; actions = { diff --git a/scripts/run-e2e.js b/cypress/scripts/run-e2e.js similarity index 88% rename from scripts/run-e2e.js rename to cypress/scripts/run-e2e.js index a5d75c5f4b..8096a70caf 100755 --- a/scripts/run-e2e.js +++ b/cypress/scripts/run-e2e.js @@ -13,7 +13,6 @@ function runTests(options) { process.env.N8N_USER_FOLDER = userFolder; process.env.E2E_TESTS = 'true'; process.env.NODE_OPTIONS = '--dns-result-order=ipv4first'; - process.env.VUE_APP_MAX_PINNED_DATA_SIZE = `${16 * 1024}`; if (options.customEnv) { Object.keys(options.customEnv).forEach((key) => { @@ -50,7 +49,7 @@ switch (scenario) { break; case 'dev': runTests({ - startCommand: 'dev', + startCommand: 'develop', url: 'http://localhost:8080/favicon.ico', testCommand: 'cypress open', customEnv: { @@ -59,10 +58,13 @@ switch (scenario) { }); break; case 'all': + const specSuiteFilter = process.argv[3]; + const specParam = specSuiteFilter ? ` --spec **/*${specSuiteFilter}*` : ''; + runTests({ startCommand: 'start', url: 'http://localhost:5678/favicon.ico', - testCommand: 'cypress run --headless', + testCommand: `cypress run --headless ${specParam}`, }); break; default: diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index a92dc2ce06..a7fa994289 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,4 +1,6 @@ import 'cypress-real-events'; +import FakeTimers from '@sinonjs/fake-timers'; +import type { IN8nUISettings } from 'n8n-workflow'; import { WorkflowPage } from '../pages'; import { BACKEND_BASE_URL, @@ -7,33 +9,43 @@ import { INSTANCE_OWNER, N8N_AUTH_COOKIE, } from '../constants'; +import { getUniqueWorkflowName } from '../utils/workflowUtils'; + +Cypress.Commands.add('setAppDate', (targetDate: number | Date) => { + cy.window().then((win) => { + FakeTimers.withGlobal(win).install({ + now: targetDate, + toFake: ['Date'], + shouldAdvanceTime: true, + }); + }); +}); Cypress.Commands.add('getByTestId', (selector, ...args) => { return cy.get(`[data-test-id="${selector}"]`, ...args); }); -Cypress.Commands.add('createFixtureWorkflow', (fixtureKey, workflowName) => { - const workflowPage = new WorkflowPage(); - - // We need to force the click because the input is hidden - workflowPage.getters - .workflowImportInput() - .selectFile(`cypress/fixtures/${fixtureKey}`, { force: true }); - - cy.waitForLoad(false); - workflowPage.actions.setWorkflowName(workflowName); - workflowPage.getters.saveButton().should('contain', 'Saved'); - workflowPage.actions.zoomToFit(); -}); - Cypress.Commands.add( - 'findChildByTestId', - { prevSubject: true }, - (subject: Cypress.Chainable>, childTestId) => { - return subject.find(`[data-test-id="${childTestId}"]`); + 'createFixtureWorkflow', + (fixtureKey: string, workflowName = getUniqueWorkflowName()) => { + const workflowPage = new WorkflowPage(); + + // We need to force the click because the input is hidden + workflowPage.getters + .workflowImportInput() + .selectFile(`fixtures/${fixtureKey}`, { force: true }); + + cy.waitForLoad(false); + workflowPage.actions.setWorkflowName(workflowName); + workflowPage.getters.saveButton().should('contain', 'Saved'); + workflowPage.actions.zoomToFit(); }, ); +Cypress.Commands.addQuery('findChildByTestId', function (testId: string) { + return (subject: Cypress.Chainable) => subject.find(`[data-test-id="${testId}"]`); +}); + Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => { // These aliases are set-up before each test in cypress/support/e2e.ts // we can't set them up here because at this point it would be too late @@ -46,7 +58,7 @@ Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => { }); Cypress.Commands.add('signin', ({ email, password }) => { - Cypress.session.clearAllSavedSessions(); + void Cypress.session.clearAllSavedSessions(); cy.session([email, password], () => cy.request({ method: 'POST', @@ -57,21 +69,22 @@ Cypress.Commands.add('signin', ({ email, password }) => { ); }); -Cypress.Commands.add('signinAsOwner', () => { - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); -}); +Cypress.Commands.add('signinAsOwner', () => cy.signin(INSTANCE_OWNER)); +Cypress.Commands.add('signinAsAdmin', () => cy.signin(INSTANCE_ADMIN)); +Cypress.Commands.add('signinAsMember', (index = 0) => cy.signin(INSTANCE_MEMBERS[index])); Cypress.Commands.add('signout', () => { cy.request({ method: 'POST', url: `${BACKEND_BASE_URL}/rest/logout`, - headers: { 'browser-id': localStorage.getItem('n8n-browserId') } + headers: { 'browser-id': localStorage.getItem('n8n-browserId') }, }); cy.getCookie(N8N_AUTH_COOKIE).should('not.exist'); }); -Cypress.Commands.add('interceptREST', (method, url) => { - cy.intercept(method, `${BACKEND_BASE_URL}/rest${url}`); +export let settings: Partial; +Cypress.Commands.add('overrideSettings', (value: Partial) => { + settings = value; }); const setFeature = (feature: string, enabled: boolean) => @@ -80,12 +93,19 @@ const setFeature = (feature: string, enabled: boolean) => enabled, }); +const setQuota = (feature: string, value: number) => + cy.request('PATCH', `${BACKEND_BASE_URL}/rest/e2e/quota`, { + feature: `quota:${feature}`, + value, + }); + const setQueueMode = (enabled: boolean) => cy.request('PATCH', `${BACKEND_BASE_URL}/rest/e2e/queue-mode`, { enabled, }); Cypress.Commands.add('enableFeature', (feature: string) => setFeature(feature, true)); +Cypress.Commands.add('changeQuota', (feature: string, value: number) => setQuota(feature, value)); Cypress.Commands.add('disableFeature', (feature: string) => setFeature(feature, false)); Cypress.Commands.add('enableQueueMode', () => setQueueMode(true)); Cypress.Commands.add('disableQueueMode', () => setQueueMode(false)); @@ -121,7 +141,7 @@ Cypress.Commands.add('paste', { prevSubject: true }, (selector, pastePayload) => }); Cypress.Commands.add('drag', (selector, pos, options) => { - const index = options?.index || 0; + const index = options?.index ?? 0; const [xDiff, yDiff] = pos; const element = typeof selector === 'string' ? cy.get(selector).eq(index) : selector; element.should('exist'); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 69bb74ec88..3968a09b5b 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,31 +1,65 @@ -import { INSTANCE_OWNER } from '../constants'; -import './commands'; +import cloneDeep from 'lodash/cloneDeep'; +import merge from 'lodash/merge'; +import { settings } from './commands'; before(() => { cy.resetDatabase(); - Cypress.on('uncaught:exception', (err) => { - return !err.message.includes('ResizeObserver'); + Cypress.on('uncaught:exception', (error) => { + return !error.message.includes('ResizeObserver'); }); }); beforeEach(() => { if (!cy.config('disableAutoLogin')) { - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + cy.signinAsOwner(); } cy.window().then((win): void => { win.localStorage.setItem('N8N_THEME', 'light'); }); - cy.intercept('GET', '/rest/settings').as('loadSettings'); + cy.intercept('GET', '/rest/settings', (req) => { + // Disable cache + delete req.headers['if-none-match']; + req.on('response', (res) => { + const defaultSettings = res.body.data; + res.send({ data: merge(cloneDeep(defaultSettings), settings) }); + }); + }).as('loadSettings'); + cy.intercept('GET', '/types/nodes.json').as('loadNodeTypes'); // Always intercept the request to test credentials and return a success cy.intercept('POST', '/rest/credentials/test', { - statusCode: 200, - body: { - data: { status: 'success', message: 'Tested successfully' }, + data: { status: 'success', message: 'Tested successfully' }, + }).as('credentialTest'); + + cy.intercept('POST', '/rest/license/renew', {}); + + cy.intercept({ pathname: '/api/health' }, { status: 'OK' }).as('healthCheck'); + cy.intercept({ pathname: '/api/versions/*' }, [ + { + name: '1.45.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 bug fixes', }, - }); + { + 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 core functionality and bug fixes', + }, + ]).as('getVersions'); }); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index f31e50c578..9819e7c3a1 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -1,7 +1,11 @@ // Load type definitions that come with Cypress module /// -import { Interception } from 'cypress/types/net-stubbing'; +import type { IN8nUISettings } from 'n8n-workflow'; + +Cypress.Keyboard.defaults({ + keystrokeDelay: 0, +}); interface SigninPayload { email: string; @@ -18,24 +22,37 @@ declare global { config(key: keyof SuiteConfigOverrides): boolean; getByTestId( selector: string, - ...args: (Partial | undefined)[] + ...args: Array | undefined> ): Chainable>; findChildByTestId(childTestId: string): Chainable>; - createFixtureWorkflow(fixtureKey: string, workflowName: string): void; + /** + * Creates a workflow from the given fixture and optionally renames it. + * + * @param fixtureKey + * @param [workflowName] Optional name for the workflow. A random nanoid is used if not given + */ + createFixtureWorkflow(fixtureKey: string, workflowName?: string): void; + /** @deprecated use signinAsOwner, signinAsAdmin or signinAsMember instead */ signin(payload: SigninPayload): void; signinAsOwner(): void; + signinAsAdmin(): void; + /** + * Omitting the index will default to index 0. + */ + signinAsMember(index?: number): void; signout(): void; - interceptREST(method: string, url: string): Chainable; + overrideSettings(value: Partial): void; enableFeature(feature: string): void; disableFeature(feature: string): void; enableQueueMode(): void; disableQueueMode(): void; + changeQuota(feature: string, value: number): void; waitForLoad(waitForIntercepts?: boolean): void; grantBrowserPermissions(...permissions: string[]): void; readClipboard(): Chainable; paste(pastePayload: string): void; drag( - selector: string | Cypress.Chainable>, + selector: string | Chainable>, target: [number, number], options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean }, ): void; @@ -44,12 +61,17 @@ declare global { shouldNotHaveConsoleErrors(): void; window(): Chainable< AUTWindow & { + innerWidth: number; + innerHeight: number; + preventNodeViewBeforeUnload?: boolean; + maxPinnedDataSize?: number; featureFlags: { - override: (feature: string, value: any) => void; + override: (feature: string, value: unknown) => void; }; } >; resetDatabase(): void; + setAppDate(targetDate: number | Date): void; } } } diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 26a5da716b..cd0a1f3a97 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -4,8 +4,9 @@ "sourceMap": false, "declaration": false, "lib": ["esnext", "dom"], - "types": ["cypress", "node"] + "types": ["cypress", "node", "cypress-real-events"] }, "include": ["**/*.ts"], - "exclude": ["**/dist/**/*", "**/node_modules/**/*"] + "exclude": ["**/dist/**/*", "**/node_modules/**/*"], + "references": [{ "path": "../packages/workflow/tsconfig.build.json" }] } diff --git a/cypress/types.ts b/cypress/types.ts index d7e776d49a..6186c4201d 100644 --- a/cypress/types.ts +++ b/cypress/types.ts @@ -1,12 +1,24 @@ export type IE2ETestPageElement = ( - ...args: any[] + ...args: unknown[] ) => | Cypress.Chainable> | Cypress.Chainable> | Cypress.Chainable>; +type Getter = IE2ETestPageElement | ((key: string | number) => IE2ETestPageElement); + export interface IE2ETestPage { url?: string; - getters: Record; - actions: Record void>; + getters: Record; + actions: Record void>; +} + +interface Execution { + workflowId: string; +} + +export interface ExecutionResponse { + data: { + results: Execution[]; + }; } diff --git a/cypress/utils/executions.ts b/cypress/utils/executions.ts index 81748af505..e42e2152d6 100644 --- a/cypress/utils/executions.ts +++ b/cypress/utils/executions.ts @@ -1,5 +1,5 @@ -import { ITaskData } from '../../packages/workflow/src'; -import { IPinData } from '../../packages/workflow'; +import { nanoid } from 'nanoid'; +import type { IDataObject, IPinData, ITaskData, ITaskDataConnections } from 'n8n-workflow'; import { clickExecuteWorkflowButton } from '../composables/workflow'; export function createMockNodeExecutionData( @@ -10,7 +10,7 @@ export function createMockNodeExecutionData( executionStatus = 'success', jsonData, ...rest - }: Partial & { jsonData?: Record }, + }: Partial & { jsonData?: Record }, ): Record { return { [name]: { @@ -29,7 +29,7 @@ export function createMockNodeExecutionData( ]; return acc; - }, {}) + }, {} as ITaskDataConnections) : data, source: [null], ...rest, @@ -75,7 +75,7 @@ export function createMockWorkflowExecutionData({ }; } -export function runMockWorkflowExcution({ +export function runMockWorkflowExecution({ trigger, lastNodeExecuted, runData, @@ -86,9 +86,9 @@ export function runMockWorkflowExcution({ runData: Array>; workflowExecutionData?: ReturnType; }) { - const executionId = Math.random().toString(36).substring(4); + const executionId = nanoid(8); - cy.intercept('POST', '/rest/workflows/run', { + cy.intercept('POST', '/rest/workflows/**/run', { statusCode: 201, body: { data: { @@ -105,7 +105,7 @@ export function runMockWorkflowExcution({ cy.wait('@runWorkflow'); - const resolvedRunData = {}; + const resolvedRunData: Record = {}; runData.forEach((nodeExecution) => { const nodeName = Object.keys(nodeExecution)[0]; const nodeRunData = nodeExecution[nodeName]; diff --git a/cypress/utils/workflowUtils.ts b/cypress/utils/workflowUtils.ts new file mode 100644 index 0000000000..5001dbe1b6 --- /dev/null +++ b/cypress/utils/workflowUtils.ts @@ -0,0 +1,5 @@ +import { nanoid } from 'nanoid'; + +export function getUniqueWorkflowName(workflowNamePrefix?: string) { + return workflowNamePrefix ? `${workflowNamePrefix} ${nanoid(12)}` : nanoid(12); +} diff --git a/docker/images/n8n-base/Dockerfile b/docker/images/n8n-base/Dockerfile index 1a3236fae5..aee2028ae2 100644 --- a/docker/images/n8n-base/Dockerfile +++ b/docker/images/n8n-base/Dockerfile @@ -1,4 +1,4 @@ -ARG NODE_VERSION=18 +ARG NODE_VERSION=20 # 1. Use a builder step to download various dependencies FROM node:${NODE_VERSION}-alpine as builder diff --git a/docker/images/n8n-custom/Dockerfile b/docker/images/n8n-custom/Dockerfile index ed98f852fa..17f0d1c517 100644 --- a/docker/images/n8n-custom/Dockerfile +++ b/docker/images/n8n-custom/Dockerfile @@ -1,7 +1,7 @@ -ARG NODE_VERSION=18 +ARG NODE_VERSION=20 # 1. Create an image to build n8n -FROM --platform=linux/amd64 n8nio/base:${NODE_VERSION} as builder +FROM --platform=linux/amd64 n8nio/base:${NODE_VERSION} AS builder # Build the application from source WORKDIR /src @@ -11,7 +11,7 @@ RUN pnpm build # Delete all dev dependencies RUN jq 'del(.pnpm.patchedDependencies)' package.json > package.json.tmp; mv package.json.tmp package.json -RUN node scripts/trim-fe-packageJson.js +RUN node .github/scripts/trim-fe-packageJson.js # Delete any source code, source-mapping, or typings RUN find . -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" -o -name "tsconfig.json" -o -name "*.tsbuildinfo" | xargs rm -rf @@ -32,7 +32,9 @@ COPY --from=builder /compiled /usr/local/lib/node_modules/n8n COPY docker/images/n8n/docker-entrypoint.sh / RUN \ - pnpm rebuild --dir /usr/local/lib/node_modules/n8n sqlite3 && \ + cd /usr/local/lib/node_modules/n8n && \ + npm rebuild sqlite3 && \ + cd - && \ ln -s /usr/local/lib/node_modules/n8n/bin/n8n /usr/local/bin/n8n && \ mkdir .n8n && \ chown node:node .n8n diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index af7803820c..2da1bc1f47 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -1,9 +1,15 @@ -ARG NODE_VERSION=18 +ARG NODE_VERSION=20 FROM n8nio/base:${NODE_VERSION} ARG N8N_VERSION RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi +LABEL org.opencontainers.image.title="n8n" +LABEL org.opencontainers.image.description="Workflow Automation Tool" +LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n" +LABEL org.opencontainers.image.url="https://n8n.io" +LABEL org.opencontainers.image.version=${N8N_VERSION} + ENV N8N_VERSION=${N8N_VERSION} ENV NODE_ENV=production ENV N8N_RELEASE_TYPE=stable diff --git a/docker/images/n8n/README.md b/docker/images/n8n/README.md index 573b5eb0a0..d654596150 100644 --- a/docker/images/n8n/README.md +++ b/docker/images/n8n/README.md @@ -9,25 +9,25 @@ n8n is an extendable workflow automation tool. With a [fair-code](https://fairco ## Contents - [n8n - Workflow automation tool](#n8n---workflow-automation-tool) - - [Contents](#contents) - - [Demo](#demo) - - [Available integrations](#available-integrations) - - [Documentation](#documentation) - - [Start n8n in Docker](#start-n8n-in-docker) - - [Start with tunnel](#start-with-tunnel) - - [Persist data](#persist-data) - - [Start with other Database](#start-with-other-database) - - [Use with PostgresDB](#use-with-postgresdb) - - [Passing Sensitive Data via File](#passing-sensitive-data-via-file) - - [Example Setup with Lets Encrypt](#example-setup-with-lets-encrypt) - - [Updating a running docker-compose instance](#updating-a-running-docker-compose-instance) - - [Setting Timezone](#setting-timezone) - - [Build Docker-Image](#build-docker-image) - - [What does n8n mean and how do you pronounce it?](#what-does-n8n-mean-and-how-do-you-pronounce-it) - - [Support](#support) - - [Jobs](#jobs) - - [Upgrading](#upgrading) - - [License](#license) + - [Contents](#contents) + - [Demo](#demo) + - [Available integrations](#available-integrations) + - [Documentation](#documentation) + - [Start n8n in Docker](#start-n8n-in-docker) + - [Start with tunnel](#start-with-tunnel) + - [Persist data](#persist-data) + - [Start with other Database](#start-with-other-database) + - [Use with PostgresDB](#use-with-postgresdb) + - [Passing Sensitive Data via File](#passing-sensitive-data-via-file) + - [Example Setup with Lets Encrypt](#example-setup-with-lets-encrypt) + - [Updating a running docker-compose instance](#updating-a-running-docker-compose-instance) + - [Setting Timezone](#setting-timezone) + - [Build Docker-Image](#build-docker-image) + - [What does n8n mean and how do you pronounce it?](#what-does-n8n-mean-and-how-do-you-pronounce-it) + - [Support](#support) + - [Jobs](#jobs) + - [Upgrading](#upgrading) + - [License](#license) ## Demo @@ -129,7 +129,7 @@ docker run -it --rm \ docker.n8n.io/n8nio/n8n ``` -A full working setup with docker-compose can be found [here](https://github.com/n8n-io/n8n/blob/master/docker/compose/withPostgres/README.md) +A full working setup with docker-compose can be found [here](https://github.com/n8n-io/n8n-hosting/blob/main/docker-compose/withPostgres/README.md) ## Passing Sensitive Data via File diff --git a/docker/images/n8n/docker-entrypoint.sh b/docker/images/n8n/docker-entrypoint.sh index 63a7c1dca6..2205826e4c 100755 --- a/docker/images/n8n/docker-entrypoint.sh +++ b/docker/images/n8n/docker-entrypoint.sh @@ -1,4 +1,11 @@ #!/bin/sh +if [ -d /opt/custom-certificates ]; then + echo "Trusting custom certificates from /opt/custom-certificates." + export NODE_OPTIONS=--use-openssl-ca $NODE_OPTIONS + export SSL_CERT_DIR=/opt/custom-certificates + c_rehash /opt/custom-certificates +fi + if [ "$#" -gt 0 ]; then # Got started with arguments exec n8n "$@" diff --git a/jest.config.js b/jest.config.js index f3f7824c14..3caac38ef9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -24,9 +24,11 @@ const config = { // This resolve the path mappings from the tsconfig relative to each jest.config.js moduleNameMapper: Object.entries(paths || {}).reduce((acc, [path, [mapping]]) => { path = `^${path.replace(/\/\*$/, '/(.*)$')}`; - mapping = mapping.replace(/^\.\/(?:(.*)\/)?\*$/, '$1'); + mapping = mapping.replace(/^\.?\.\/(?:(.*)\/)?\*$/, '$1'); mapping = mapping ? `/${mapping}` : ''; - acc[path] = '' + (baseUrl ? `/${baseUrl.replace(/^\.\//, '')}` : '') + mapping + '/$1'; + acc[path] = mapping.startsWith('/test') + ? '' + mapping + '/$1' + : '' + (baseUrl ? `/${baseUrl.replace(/^\.\//, '')}` : '') + mapping + '/$1'; return acc; }, {}), setupFilesAfterEnv: ['jest-expect-message'], diff --git a/n8n.code-workspace b/n8n.code-workspace new file mode 100644 index 0000000000..9d32d7aa04 --- /dev/null +++ b/n8n.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": ".", + }, + ], +} diff --git a/package.json b/package.json index 1ac5ad8844..e05eeb9845 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,45 @@ { "name": "n8n-monorepo", - "version": "1.39.0", + "version": "1.53.0", "private": true, - "homepage": "https://n8n.io", "engines": { - "node": ">=18.10", - "pnpm": ">=8.14" + "node": ">=20.15", + "pnpm": ">=9.5" }, - "packageManager": "pnpm@8.14.3", + "packageManager": "pnpm@9.6.0", "scripts": { "preinstall": "node scripts/block-npm-install.js", "build": "turbo run build", - "build:backend": "pnpm --filter=!@n8n/chat --filter=!n8n-design-system --filter=!n8n-editor-ui build", - "build:frontend": "pnpm --filter=@n8n/chat --filter=n8n-design-system --filter=n8n-editor-ui build", - "typecheck": "turbo run typecheck", - "dev": "turbo run dev --parallel --filter=!n8n-design-system --filter=!@n8n/chat", - "dev:ai": "turbo run dev --parallel --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core", + "build:backend": "turbo run build:backend", + "build:frontend": "turbo run build:frontend", + "build:nodes": "turbo run build:nodes", + "typecheck": "turbo typecheck", + "dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat", + "dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core", "clean": "turbo run clean --parallel", "format": "turbo run format && node scripts/format.mjs", "lint": "turbo run lint", "lintfix": "turbo run lintfix", + "lint:backend": "turbo run lint:backend", + "lint:nodes": "turbo run lint:nodes", + "lint:frontend": "turbo run lint:frontend", "optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo", "start": "run-script-os", "start:default": "cd packages/cli/bin && ./n8n", "start:tunnel": "./packages/cli/bin/n8n start --tunnel", "start:windows": "cd packages/cli/bin && n8n", "test": "turbo run test", - "test:backend": "pnpm --filter=!@n8n/chat --filter=!n8n-design-system --filter=!n8n-editor-ui --filter=!n8n-nodes-base --filter=!@n8n/n8n-nodes-langchain test", - "test:nodes": "pnpm --filter=n8n-nodes-base --filter=@n8n/n8n-nodes-langchain test", - "test:frontend": "pnpm --filter=@n8n/chat --filter=n8n-design-system --filter=n8n-editor-ui test", + "test:backend": "turbo run test:backend --concurrency=1", + "test:frontend": "turbo run test:frontend --concurrency=1", + "test:nodes": "turbo run test:nodes --concurrency=1", "watch": "turbo run watch --parallel", "webhook": "./packages/cli/bin/n8n webhook", - "worker": "./packages/cli/bin/n8n worker", - "cypress:install": "cypress install", - "cypress:open": "CYPRESS_BASE_URL=http://localhost:8080 cypress open", - "test:e2e:ui": "scripts/run-e2e.js ui", - "test:e2e:dev": "scripts/run-e2e.js dev", - "test:e2e:all": "scripts/run-e2e.js all" - }, - "dependencies": { - "n8n": "workspace:*" + "worker": "./packages/cli/bin/n8n worker" }, "devDependencies": { "@n8n_io/eslint-config": "workspace:*", - "@ngneat/falso": "^6.4.0", "@types/jest": "^29.5.3", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.2.1", - "cross-env": "^7.0.3", - "cypress": "^13.6.2", - "cypress-otp": "^1.0.3", - "cypress-real-events": "^1.11.0", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", "jest-expect-message": "^1.1.3", @@ -61,33 +50,30 @@ "p-limit": "^3.1.0", "rimraf": "^5.0.1", "run-script-os": "^1.0.7", - "start-server-and-test": "^2.0.3", - "supertest": "^6.3.4", + "supertest": "^7.0.0", "ts-jest": "^29.1.1", "tsc-alias": "^1.8.7", "tsc-watch": "^6.0.4", - "turbo": "1.10.12", - "typescript": "*", - "vite": "^5.1.6", - "vite-plugin-checker": "^0.6.4", - "vitest": "^1.3.1", - "vue-tsc": "^2.0.6" + "turbo": "2.0.6", + "typescript": "*" }, "pnpm": { "onlyBuiltDependencies": [ "sqlite3" ], "overrides": { - "@langchain/core": "0.1.41", "@types/node": "^18.16.16", "axios": "1.6.7", "chokidar": "3.5.2", + "esbuild": "^0.20.2", "formidable": "3.5.1", "prettier": "^3.2.5", + "pug": "^3.0.3", "semver": "^7.5.4", "tslib": "^2.6.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.4.2" + "typescript": "^5.5.2", + "ws": ">=8.17.1" }, "patchedDependencies": { "typedi@0.10.0": "patches/typedi@0.10.0.patch", @@ -96,7 +82,7 @@ "pyodide@0.23.4": "patches/pyodide@0.23.4.patch", "@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch", "@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch", - "vite-plugin-checker@0.6.4": "patches/vite-plugin-checker@0.6.4.patch" + "@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch" } } } diff --git a/packages/@n8n/chat/LICENSE.md b/packages/@n8n/chat/LICENSE.md deleted file mode 100644 index aab68b6d93..0000000000 --- a/packages/@n8n/chat/LICENSE.md +++ /dev/null @@ -1,86 +0,0 @@ -# License - -Portions of this software are licensed as follows: - -- Content of branches other than the main branch (i.e. "master") are not licensed. -- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License. - To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License - specifically allowing you access to such source code files and as defined in "LICENSE_EE.md". -- All third party components incorporated into the n8n Software are licensed under the original license - provided by the owner of the applicable component. -- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use - License" as defined below. - -## Sustainable Use License - -Version 1.0 - -### Acceptance - -By using the software, you agree to all of the terms and conditions below. - -### Copyright License - -The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license -to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject -to the limitations below. - -### Limitations - -You may use or modify the software only for your own internal business purposes or for non-commercial or -personal use. You may distribute the software or provide it to others only if you do so free of charge for -non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of -the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. - -### Patents - -The licensor grants you a license, under any patent claims the licensor can license, or becomes able to -license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case -subject to the limitations and conditions in this license. This license does not cover any patent claims that -you cause to be infringed by modifications or additions to the software. If you or your company make any -written claim that the software infringes or contributes to infringement of any patent, your patent license -for the software granted under these terms ends immediately. If your company makes such a claim, your patent -license ends immediately for work on behalf of your company. - -### Notices - -You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these -terms. If you modify the software, you must include in any modified copies of the software a prominent notice -stating that you have modified the software. - -### No Other Rights - -These terms do not imply any licenses other than those expressly granted in these terms. - -### Termination - -If you use the software in violation of these terms, such use is not licensed, and your license will -automatically terminate. If the licensor provides you with a notice of your violation, and you cease all -violation of this license no later than 30 days after you receive that notice, your license will be reinstated -retroactively. However, if you violate these terms after such reinstatement, any additional violation of these -terms will cause your license to terminate automatically and permanently. - -### No Liability - -As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will -not be liable to you for any damages arising out of these terms or the use or nature of the software, under -any kind of legal claim. - -### Definitions - -The “licensor” is the entity offering these terms. - -The “software” is the software the licensor makes available under these terms, including any portion of it. - -“You” refers to the individual or entity agreeing to these terms. - -“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus -all organizations that have control over, are under the control of, or are under common control with that -organization. Control means ownership of substantially all the assets of an entity, or the power to direct its -management and policies by vote, contract, or otherwise. Control can be direct or indirect. - -“Your license” is the license granted to you for the software under these terms. - -“Use” means anything you do with the software requiring your license. - -“Trademark” means trademarks, service marks, and similar rights. diff --git a/packages/@n8n/chat/README.md b/packages/@n8n/chat/README.md index 2cb9babbf1..538d72ce0a 100644 --- a/packages/@n8n/chat/README.md +++ b/packages/@n8n/chat/README.md @@ -31,9 +31,9 @@ Open the **Webhook** node and replace `YOUR_PRODUCTION_WEBHOOK_URL` with your pr Add the following code to your HTML page. ```html - + + + + + diff --git a/packages/@n8n/chat/src/components/ChatWindow.vue b/packages/@n8n/chat/src/components/ChatWindow.vue index 1d1f7ae3c2..8e0d05daf8 100644 --- a/packages/@n8n/chat/src/components/ChatWindow.vue +++ b/packages/@n8n/chat/src/components/ChatWindow.vue @@ -1,7 +1,5 @@