mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge remote-tracking branch 'upstream/master' into node-azure-cosmosdb
This commit is contained in:
commit
e779822043
|
@ -29,11 +29,14 @@ jobs:
|
|||
tenant-id: ${{ secrets.BENCHMARK_ARM_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.BENCHMARK_ARM_SUBSCRIPTION_ID }}
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
|
9
.github/workflows/benchmark-nightly.yml
vendored
9
.github/workflows/benchmark-nightly.yml
vendored
|
@ -48,11 +48,14 @@ jobs:
|
|||
with:
|
||||
terraform_version: '1.8.5'
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: pnpm
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
|
|
@ -16,11 +16,14 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
|
9
.github/workflows/check-pr-title.yml
vendored
9
.github/workflows/check-pr-title.yml
vendored
|
@ -17,11 +17,14 @@ jobs:
|
|||
- name: Check out branch
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
|
14
.github/workflows/chromatic.yml
vendored
14
.github/workflows/chromatic.yml
vendored
|
@ -8,13 +8,14 @@ on:
|
|||
types: [submitted]
|
||||
|
||||
concurrency:
|
||||
group: chromatic-${{ github.event.pull_request.number || github.ref }}
|
||||
group: chromatic-${{ github.event.pull_request.number || github.ref }}-${{github.event.review.state}}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
get-metadata:
|
||||
name: Get Metadata
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.review.state == 'approved'
|
||||
steps:
|
||||
- name: Check out current commit
|
||||
uses: actions/checkout@v4
|
||||
|
@ -54,11 +55,16 @@ jobs:
|
|||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Publish to Chromatic
|
||||
|
|
18
.github/workflows/ci-master.yml
vendored
18
.github/workflows/ci-master.yml
vendored
|
@ -7,30 +7,35 @@ on:
|
|||
|
||||
jobs:
|
||||
install-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: useblacksmith/setup-node@v5
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: pnpm
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
uses: useblacksmith/caching-for-turbo@v1
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache/save@v4.0.0
|
||||
uses: useblacksmith/cache/save@v5
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-base:build
|
||||
|
@ -48,6 +53,7 @@ jobs:
|
|||
cacheKey: ${{ github.sha }}-base:build
|
||||
collectCoverage: ${{ matrix.node-version == '20.x' }}
|
||||
ignoreTurboCache: ${{ matrix.node-version == '20.x' }}
|
||||
skipFrontendTests: ${{ matrix.node-version != '20.x' }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
|
|
44
.github/workflows/ci-postgres-mysql.yml
vendored
44
.github/workflows/ci-postgres-mysql.yml
vendored
|
@ -23,11 +23,16 @@ jobs:
|
|||
if: github.event_name != 'pull_request_review' || startsWith(github.event.pull_request.base.ref, 'release/')
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
|
@ -52,11 +57,16 @@ jobs:
|
|||
DB_SQLITE_POOL_SIZE: 4
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
|
@ -81,11 +91,16 @@ jobs:
|
|||
DB_MYSQLDB_PASSWORD: password
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
|
@ -118,11 +133,16 @@ jobs:
|
|||
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.2
|
||||
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
|
|
17
.github/workflows/ci-pull-requests.yml
vendored
17
.github/workflows/ci-pull-requests.yml
vendored
|
@ -9,23 +9,28 @@ on:
|
|||
jobs:
|
||||
install-and-build:
|
||||
name: Install & Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: useblacksmith/setup-node@v5
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: pnpm
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
uses: useblacksmith/caching-for-turbo@v1
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
@ -37,7 +42,7 @@ jobs:
|
|||
run: pnpm typecheck
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache/save@v4.0.0
|
||||
uses: useblacksmith/cache/save@v5
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-base:build
|
||||
|
|
12
.github/workflows/docker-base-image.yml
vendored
12
.github/workflows/docker-base-image.yml
vendored
|
@ -20,26 +20,28 @@ jobs:
|
|||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/images/n8n-base/Dockerfile
|
||||
|
|
10
.github/workflows/docker-images-benchmark.yml
vendored
10
.github/workflows/docker-images-benchmark.yml
vendored
|
@ -19,20 +19,22 @@ jobs:
|
|||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/@n8n/benchmark/Dockerfile
|
||||
|
|
83
.github/workflows/docker-images-custom.yml
vendored
Normal file
83
.github/workflows/docker-images-custom.yml
vendored
Normal file
|
@ -0,0 +1,83 @@
|
|||
name: Docker Custom Image CI
|
||||
run-name: Build ${{ inputs.branch }} - ${{ inputs.user }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'GitHub branch to create image off.'
|
||||
required: true
|
||||
tag:
|
||||
description: 'Name of the docker tag to create.'
|
||||
required: true
|
||||
merge-master:
|
||||
description: 'Merge with master.'
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
user:
|
||||
description: ''
|
||||
required: false
|
||||
default: 'none'
|
||||
start-url:
|
||||
description: 'URL to call after workflow is kicked off.'
|
||||
required: false
|
||||
default: ''
|
||||
success-url:
|
||||
description: 'URL to call after Docker Image got built successfully.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Call Start URL - optionally
|
||||
if: ${{ github.event.inputs.start-url != '' }}
|
||||
run: curl -v -X POST -d 'url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' ${{github.event.inputs.start-url}} || echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- name: Merge Master - optionally
|
||||
if: github.event.inputs.merge-master
|
||||
run: git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push image to GHCR
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/images/n8n-custom/Dockerfile
|
||||
build-args: |
|
||||
N8N_RELEASE_TYPE=development
|
||||
platforms: linux/amd64
|
||||
provenance: false
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ghcr.io/${{ github.repository_owner }}/n8n:${{ inputs.tag }}
|
||||
|
||||
- name: Call Success URL - optionally
|
||||
if: ${{ github.event.inputs.success-url != '' }}
|
||||
run: curl -v ${{github.event.inputs.success-url}} || echo ""
|
||||
shell: bash
|
86
.github/workflows/docker-images-nightly.yml
vendored
86
.github/workflows/docker-images-nightly.yml
vendored
|
@ -1,74 +1,40 @@
|
|||
name: Docker Nightly Image CI
|
||||
run-name: Build ${{ inputs.branch }} - ${{ inputs.user }}
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'GitHub branch to create image off.'
|
||||
required: true
|
||||
default: 'master'
|
||||
tag:
|
||||
description: 'Name of the docker tag to create.'
|
||||
required: true
|
||||
default: 'nightly'
|
||||
merge-master:
|
||||
description: 'Merge with master.'
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
user:
|
||||
description: ''
|
||||
required: false
|
||||
default: 'schedule'
|
||||
start-url:
|
||||
description: 'URL to call after workflow is kicked off.'
|
||||
required: false
|
||||
default: ''
|
||||
success-url:
|
||||
description: 'URL to call after Docker Image got built successfully.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
N8N_TAG: ${{ inputs.tag || 'nightly' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Call Start URL - optionally
|
||||
run: |
|
||||
[[ "${{github.event.inputs.start-url}}" != "" ]] && curl -v -X POST -d 'url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' ${{github.event.inputs.start-url}} || echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch || 'master' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Merge Master - optionally
|
||||
run: |
|
||||
[[ "${{github.event.inputs.merge-master}}" == "true" ]] && git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Build and push to DockerHub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
- name: Build and push image to GHCR and DockerHub
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/images/n8n-custom/Dockerfile
|
||||
|
@ -79,24 +45,6 @@ jobs:
|
|||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ env.N8N_TAG }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: env.N8N_TAG == 'nightly'
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Push image to GHCR
|
||||
if: env.N8N_TAG == 'nightly'
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag ghcr.io/${{ github.repository_owner }}/n8n:nightly \
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/n8n:nightly
|
||||
${{ secrets.DOCKER_USERNAME }}/n8n:nightly
|
||||
|
||||
- name: Call Success URL - optionally
|
||||
run: |
|
||||
[[ "${{github.event.inputs.success-url}}" != "" ]] && curl -v ${{github.event.inputs.success-url}} || echo ""
|
||||
shell: bash
|
||||
|
|
7
.github/workflows/e2e-reusable.yml
vendored
7
.github/workflows/e2e-reusable.yml
vendored
|
@ -41,11 +41,6 @@ on:
|
|||
description: 'PR number to run tests for.'
|
||||
required: false
|
||||
type: number
|
||||
node_view_version:
|
||||
description: 'Node View version to run tests with.'
|
||||
required: false
|
||||
default: '1'
|
||||
type: string
|
||||
secrets:
|
||||
CYPRESS_RECORD_KEY:
|
||||
description: 'Cypress record key.'
|
||||
|
@ -165,7 +160,7 @@ jobs:
|
|||
spec: '${{ inputs.spec }}'
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
CYPRESS_NODE_VIEW_VERSION: ${{ inputs.node_view_version }}
|
||||
CYPRESS_NODE_VIEW_VERSION: 2
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
E2E_TESTS: true
|
||||
|
|
3
.github/workflows/e2e-tests-pr.yml
vendored
3
.github/workflows/e2e-tests-pr.yml
vendored
|
@ -5,13 +5,14 @@ on:
|
|||
types: [submitted]
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.event.pull_request.number || github.ref }}
|
||||
group: e2e-${{ github.event.pull_request.number || github.ref }}-${{github.event.review.state}}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
get-metadata:
|
||||
name: Get Metadata
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.review.state == 'approved'
|
||||
steps:
|
||||
- name: Check out current commit
|
||||
uses: actions/checkout@v4
|
||||
|
|
6
.github/workflows/e2e-tests.yml
vendored
6
.github/workflows/e2e-tests.yml
vendored
|
@ -27,11 +27,6 @@ on:
|
|||
description: 'URL to call after workflow is done.'
|
||||
required: false
|
||||
default: ''
|
||||
node_view_version:
|
||||
description: 'Node View version to run tests with.'
|
||||
required: false
|
||||
default: '1'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
calls-start-url:
|
||||
|
@ -51,7 +46,6 @@ jobs:
|
|||
branch: ${{ github.event.inputs.branch || 'master' }}
|
||||
user: ${{ github.event.inputs.user || 'PR User' }}
|
||||
spec: ${{ github.event.inputs.spec || 'e2e/*' }}
|
||||
node_view_version: ${{ github.event.inputs.node_view_version || '1' }}
|
||||
secrets:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
|
||||
|
|
18
.github/workflows/linting-reusable.yml
vendored
18
.github/workflows/linting-reusable.yml
vendored
|
@ -17,23 +17,28 @@ on:
|
|||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: useblacksmith/setup-node@v5
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: pnpm
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
uses: useblacksmith/caching-for-turbo@v1
|
||||
|
||||
- name: Build
|
||||
if: ${{ inputs.cacheKey == '' }}
|
||||
|
@ -41,10 +46,11 @@ jobs:
|
|||
|
||||
- name: Restore cached build artifacts
|
||||
if: ${{ inputs.cacheKey != '' }}
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
uses: useblacksmith/cache/restore@v5
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ inputs.cacheKey }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Lint Backend
|
||||
run: pnpm lint:backend
|
||||
|
|
8
.github/workflows/release-create-pr.yml
vendored
8
.github/workflows/release-create-pr.yml
vendored
|
@ -35,13 +35,17 @@ jobs:
|
|||
fetch-depth: 0
|
||||
ref: ${{ github.event.inputs.base-branch }}
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- run: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- name: Bump package versions
|
||||
run: |
|
||||
echo "NEXT_RELEASE=$(node .github/scripts/bump-versions.mjs)" >> $GITHUB_ENV
|
||||
|
|
30
.github/workflows/release-publish.yml
vendored
30
.github/workflows/release-publish.yml
vendored
|
@ -25,11 +25,15 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set release version in env
|
||||
|
@ -73,26 +77,28 @@ jobs:
|
|||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
with:
|
||||
context: ./docker/images/n8n
|
||||
build-args: |
|
||||
|
@ -159,6 +165,14 @@ jobs:
|
|||
version: ${{ needs.publish-to-npm.outputs.release }}
|
||||
sourcemaps: packages/cli/dist packages/core/dist packages/nodes-base/dist packages/@n8n/n8n-nodes-langchain/dist
|
||||
|
||||
- name: Create a task runner release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
projects: ${{ secrets.SENTRY_TASK_RUNNER_PROJECT }}
|
||||
version: ${{ needs.publish-to-npm.outputs.release }}
|
||||
sourcemaps: packages/core/dist packages/workflow/dist packages/@n8n/task-runner/dist
|
||||
|
||||
trigger-release-note:
|
||||
name: Trigger a release note
|
||||
needs: [publish-to-npm, create-github-release]
|
||||
|
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
- run: |
|
||||
|
@ -34,7 +34,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: docker/login-action@v3.0.0
|
||||
- uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
@ -46,7 +46,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: docker/login-action@v3.0.0
|
||||
- uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
|
22
.github/workflows/test-workflows.yml
vendored
22
.github/workflows/test-workflows.yml
vendored
|
@ -22,11 +22,16 @@ jobs:
|
|||
!contains(github.event.pull_request.labels.*.name, 'community')
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
|
@ -48,11 +53,16 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
|
|
6
.github/workflows/units-tests-dispatch.yml
vendored
6
.github/workflows/units-tests-dispatch.yml
vendored
|
@ -12,6 +12,11 @@ on:
|
|||
description: 'PR number to run tests for.'
|
||||
required: false
|
||||
type: number
|
||||
skipFrontendTests:
|
||||
description: 'Skip Frontend tests'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
|
@ -37,3 +42,4 @@ jobs:
|
|||
uses: ./.github/workflows/units-tests-reusable.yml
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.branch }}
|
||||
skipFrontendTests: ${{ inputs.skipFrontendTests }}
|
||||
|
|
21
.github/workflows/units-tests-reusable.yml
vendored
21
.github/workflows/units-tests-reusable.yml
vendored
|
@ -26,6 +26,10 @@ on:
|
|||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skipFrontendTests:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
description: 'Codecov upload token.'
|
||||
|
@ -34,7 +38,7 @@ on:
|
|||
jobs:
|
||||
unit-test:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
env:
|
||||
TURBO_FORCE: ${{ inputs.ignoreTurboCache }}
|
||||
COVERAGE_ENABLED: ${{ inputs.collectCoverage }}
|
||||
|
@ -43,18 +47,21 @@ jobs:
|
|||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- run: corepack enable
|
||||
- name: Use Node.js ${{ inputs.nodeVersion }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: useblacksmith/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ inputs.nodeVersion }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.31
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
uses: useblacksmith/caching-for-turbo@v1
|
||||
|
||||
- name: Build
|
||||
if: ${{ inputs.cacheKey == '' }}
|
||||
|
@ -62,10 +69,11 @@ jobs:
|
|||
|
||||
- name: Restore cached build artifacts
|
||||
if: ${{ inputs.cacheKey != '' }}
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
uses: useblacksmith/cache/restore@v5
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ inputs.cacheKey }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Test Backend
|
||||
run: pnpm test:backend
|
||||
|
@ -74,6 +82,7 @@ jobs:
|
|||
run: pnpm test:nodes
|
||||
|
||||
- name: Test Frontend
|
||||
if: ${{ !inputs.skipFrontendTests }}
|
||||
run: pnpm test:frontend
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
|
|
194
CHANGELOG.md
194
CHANGELOG.md
|
@ -1,9 +1,199 @@
|
|||
## [1.74.1](https://github.com/n8n-io/n8n/compare/n8n@1.74.0...n8n@1.74.1) (2025-01-09)
|
||||
# [1.78.0](https://github.com/n8n-io/n8n/compare/n8n@1.77.0...n8n@1.78.0) (2025-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** Fix parameter input validation ([#12532](https://github.com/n8n-io/n8n/issues/12532)) ([6f757f1](https://github.com/n8n-io/n8n/commit/6f757f10bd9102394d2a0b6bbc795f90444f66d2))
|
||||
* **AI Agent Node:** Ignore SSL errors option for SQLAgent ([#13052](https://github.com/n8n-io/n8n/issues/13052)) ([a90529f](https://github.com/n8n-io/n8n/commit/a90529fd51ca88bc9640d24490dbeb2023c98e30))
|
||||
* **Code Node:** Do not validate code within comments ([#12938](https://github.com/n8n-io/n8n/issues/12938)) ([cdfa225](https://github.com/n8n-io/n8n/commit/cdfa22593b69cf647c2a798d6571a9bbbd11c1b2))
|
||||
* **core:** "Respond to Webhook" should work with workflows with waiting nodes ([#12806](https://github.com/n8n-io/n8n/issues/12806)) ([e8635f2](https://github.com/n8n-io/n8n/commit/e8635f257433748f4d7d2c4b0ae794de6bff5b28))
|
||||
* **core:** Do not emit `workflow-post-execute` event for waiting executions ([#13065](https://github.com/n8n-io/n8n/issues/13065)) ([1593b6c](https://github.com/n8n-io/n8n/commit/1593b6cb4112ab2a85ca93c4eaec7d5f088895b1))
|
||||
* **core:** Do not enable strict type validation by default for resource mapper ([#13037](https://github.com/n8n-io/n8n/issues/13037)) ([fdcff90](https://github.com/n8n-io/n8n/commit/fdcff9082b97314f8b04579ab6fa81c724916320))
|
||||
* **core:** Fix empty node execution stack ([#12945](https://github.com/n8n-io/n8n/issues/12945)) ([7031569](https://github.com/n8n-io/n8n/commit/7031569a028bcc85558fcb614f8143d68a7f81f0))
|
||||
* **core:** Only use new resource mapper type validation when it is enabled ([#13099](https://github.com/n8n-io/n8n/issues/13099)) ([a37c8e8](https://github.com/n8n-io/n8n/commit/a37c8e8fb86aaa3244ac13500ffa0e7c0d809a6f))
|
||||
* **editor:** Actually enforce the version and don't break for old values in local storage ([#13025](https://github.com/n8n-io/n8n/issues/13025)) ([884a7e2](https://github.com/n8n-io/n8n/commit/884a7e23f84258756d8dcdd2dfe933bdedf61adc))
|
||||
* **editor:** Add telemetry to source control feature ([#13016](https://github.com/n8n-io/n8n/issues/13016)) ([18eaa54](https://github.com/n8n-io/n8n/commit/18eaa5423dfc9348374c2cff4ae0e6f152268fbb))
|
||||
* **editor:** Allow switch to `Fixed` for boolean and number parameters with invalid expressions ([#12948](https://github.com/n8n-io/n8n/issues/12948)) ([118be24](https://github.com/n8n-io/n8n/commit/118be24d25f001525ced03d9426a6129fa5a2053))
|
||||
* **editor:** Allow to re-open sub-connection node creator if already active ([#13041](https://github.com/n8n-io/n8n/issues/13041)) ([16d59e9](https://github.com/n8n-io/n8n/commit/16d59e98edc427bf68edbce4cd2174a44d6dcfb1))
|
||||
* **editor:** Code node overwrites code when switching nodes after edits ([#13078](https://github.com/n8n-io/n8n/issues/13078)) ([00e3ebc](https://github.com/n8n-io/n8n/commit/00e3ebc9e2e0b8cc2d88b678c3a2a21602dac010))
|
||||
* **editor:** Fix execution running status listener for chat messages ([#12951](https://github.com/n8n-io/n8n/issues/12951)) ([4d55a29](https://github.com/n8n-io/n8n/commit/4d55a294600dc2c86f6f7019da923b66a4b9de7e))
|
||||
* **editor:** Fix position of connector buttons when the line is straight ([#13034](https://github.com/n8n-io/n8n/issues/13034)) ([3a908ac](https://github.com/n8n-io/n8n/commit/3a908aca17f0bc1cf5fb5eb8813cc94f27f0bcdf))
|
||||
* **editor:** Fix showing and hiding canvas edge toolbar when hovering ([#13009](https://github.com/n8n-io/n8n/issues/13009)) ([ac7bc4f](https://github.com/n8n-io/n8n/commit/ac7bc4f1911f913233eeeae5d229432fdff332c4))
|
||||
* **editor:** Make AI transform node read only in executions view ([#12970](https://github.com/n8n-io/n8n/issues/12970)) ([ce1deb8](https://github.com/n8n-io/n8n/commit/ce1deb8aea528eef996fc774d0fff1dc61df5843))
|
||||
* **editor:** Prevent infinite loop in expressions crashing the browser ([#12732](https://github.com/n8n-io/n8n/issues/12732)) ([8c2dbcf](https://github.com/n8n-io/n8n/commit/8c2dbcfeced70a0a84137773269cc6db2928d174))
|
||||
* **editor:** Refine push modal layout ([#12886](https://github.com/n8n-io/n8n/issues/12886)) ([212a5bf](https://github.com/n8n-io/n8n/commit/212a5bf23eb11cc3296e7a8d002a4b7727d5193c))
|
||||
* **editor:** SchemaView renders duplicate structures properly ([#12943](https://github.com/n8n-io/n8n/issues/12943)) ([0d8a544](https://github.com/n8n-io/n8n/commit/0d8a544975f72724db931778d7e3ace8a12b6cfc))
|
||||
* **editor:** Update node issues when opening execution ([#12972](https://github.com/n8n-io/n8n/issues/12972)) ([1a91523](https://github.com/n8n-io/n8n/commit/1a915239c6571d7744023c6df6242dabe97c912e))
|
||||
* **editor:** Use correct connection index when connecting adjancent nodes after deleting a node ([#12973](https://github.com/n8n-io/n8n/issues/12973)) ([c7a15d5](https://github.com/n8n-io/n8n/commit/c7a15d5980d181a865f8e2ec6a5f70d0681dcf56))
|
||||
* **GitHub Node:** Don't truncate filenames retrieved from GitHub ([#12923](https://github.com/n8n-io/n8n/issues/12923)) ([7e18447](https://github.com/n8n-io/n8n/commit/7e1844757fe0d544e8881d229d16af95ed53fb21))
|
||||
* **Google Cloud Firestore Node:** Fix potential prototype pollution vulnerability ([#13035](https://github.com/n8n-io/n8n/issues/13035)) ([f150f79](https://github.com/n8n-io/n8n/commit/f150f79ad6c7d43e036688b1de8d6c2c8140aca9))
|
||||
* Increment runIndex in WorkflowToolV2 tool executions to avoid reusing out of date inputs ([#13008](https://github.com/n8n-io/n8n/issues/13008)) ([cc907fb](https://github.com/n8n-io/n8n/commit/cc907fbca9aa00fe07dd54a2fcac8983f2321ad1))
|
||||
* Sync partial execution version of FE and BE, also allow enforcing a specific version ([#12840](https://github.com/n8n-io/n8n/issues/12840)) ([a155043](https://github.com/n8n-io/n8n/commit/a15504329bac582225185705566297d9cc27bf73))
|
||||
* **Wise Node:** Use ISO formatting for timestamps ([#10288](https://github.com/n8n-io/n8n/issues/10288)) ([1a2d39a](https://github.com/n8n-io/n8n/commit/1a2d39a158c9a61bdaf11124b09ae70de65ebbf1))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add reusable frontend `composables` package ([#13077](https://github.com/n8n-io/n8n/issues/13077)) ([ef87da4](https://github.com/n8n-io/n8n/commit/ef87da4c193a08e089e48044906a4f5ce9959a22))
|
||||
* Add support for client credentials with Azure Log monitor ([#13038](https://github.com/n8n-io/n8n/issues/13038)) ([2c2d631](https://github.com/n8n-io/n8n/commit/2c2d63157b7866f1a68cc45c5823e29570ccff77))
|
||||
* Allow multi API creation via the UI ([#12845](https://github.com/n8n-io/n8n/issues/12845)) ([ad3250c](https://github.com/n8n-io/n8n/commit/ad3250ceb0df84379917e684d54d4100e3bf44f5))
|
||||
* Allow setting API keys expiration ([#12954](https://github.com/n8n-io/n8n/issues/12954)) ([9bcbc2c](https://github.com/n8n-io/n8n/commit/9bcbc2c2ccbb88537e9b7554c92b631118d870f1))
|
||||
* **core:** Add sorting to GET `/workflows` endpoint ([#13029](https://github.com/n8n-io/n8n/issues/13029)) ([b60011a](https://github.com/n8n-io/n8n/commit/b60011a1808d47f32ab84e685dba0e915e82df8f))
|
||||
* **core:** Enable usage as a tool for more nodes ([#12930](https://github.com/n8n-io/n8n/issues/12930)) ([9deb759](https://github.com/n8n-io/n8n/commit/9deb75916e4eb63b899ba79b40cbd24b69a752db))
|
||||
* **core:** Handle Declarative nodes more like regular nodes ([#13007](https://github.com/n8n-io/n8n/issues/13007)) ([a65a9e6](https://github.com/n8n-io/n8n/commit/a65a9e631b13bbe70ad64727fb1109ae7cd014eb))
|
||||
* **Discord Node:** New sendAndWait operation ([#12894](https://github.com/n8n-io/n8n/issues/12894)) ([d47bfdd](https://github.com/n8n-io/n8n/commit/d47bfddd656367454b51da39cf87dbfb2bd59eb2))
|
||||
* **editor:** Display schema preview for unexecuted nodes ([#12901](https://github.com/n8n-io/n8n/issues/12901)) ([0063bbb](https://github.com/n8n-io/n8n/commit/0063bbb30b45b3af92aff4c0f76b905d50a71a2d))
|
||||
* **editor:** Easy $fromAI Button for AI Tools ([#12587](https://github.com/n8n-io/n8n/issues/12587)) ([2177376](https://github.com/n8n-io/n8n/commit/21773764d37c37a6464a3885d3fa548a5feb4fd8))
|
||||
* **editor:** Show fixed collection parameter issues in UI ([#12899](https://github.com/n8n-io/n8n/issues/12899)) ([12d686c](https://github.com/n8n-io/n8n/commit/12d686ce52694f4c0b88f92a744451c1b0c66dec))
|
||||
* **Facebook Graph API Node:** Update node to support API v22.0 ([#13024](https://github.com/n8n-io/n8n/issues/13024)) ([0bc0fc6](https://github.com/n8n-io/n8n/commit/0bc0fc6c1226688c29bf5f8f0ba7e8f244e16fbc))
|
||||
* **HTTP Request Tool Node:** Relax binary data detection ([#13048](https://github.com/n8n-io/n8n/issues/13048)) ([b67a003](https://github.com/n8n-io/n8n/commit/b67a003e0b154d4e8c04392bec1c7b28171b5908))
|
||||
* Human in the loop section ([#12883](https://github.com/n8n-io/n8n/issues/12883)) ([9590e5d](https://github.com/n8n-io/n8n/commit/9590e5d58b8964de9ce901bf07b537926d18b6b7))
|
||||
* **n8n Form Node:** Add Hidden Fields ([#12803](https://github.com/n8n-io/n8n/issues/12803)) ([0da1114](https://github.com/n8n-io/n8n/commit/0da1114981978e371b216bdabc0c3bbdceeefa09))
|
||||
* **n8n Form Node:** Respond with Text ([#12979](https://github.com/n8n-io/n8n/issues/12979)) ([182fc15](https://github.com/n8n-io/n8n/commit/182fc150bec62e9a5e2801d6c403e4a6bd35f728))
|
||||
* **OpenAI Chat Model Node, OpenAI Node:** Include o3 models in model selection ([#13005](https://github.com/n8n-io/n8n/issues/13005)) ([37d152c](https://github.com/n8n-io/n8n/commit/37d152c148cafbe493c22e07f5d55ff24fcb0ca4))
|
||||
* **Summarize Node:** Preserves original field data type ([#13069](https://github.com/n8n-io/n8n/issues/13069)) ([be5e49d](https://github.com/n8n-io/n8n/commit/be5e49d56c09d65c9768e948471626cfd3606c0c))
|
||||
|
||||
|
||||
|
||||
# [1.77.0](https://github.com/n8n-io/n8n/compare/n8n@1.76.0...n8n@1.77.0) (2025-01-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Account for pre-execution failure in scaling mode ([#12815](https://github.com/n8n-io/n8n/issues/12815)) ([b4d27c4](https://github.com/n8n-io/n8n/commit/b4d27c49e32bfacbd2690bf1c07194562f6a4a61))
|
||||
* **core:** Display the last activated plan name when multiple are activated ([#12835](https://github.com/n8n-io/n8n/issues/12835)) ([03365f0](https://github.com/n8n-io/n8n/commit/03365f096d3d5c8e3a6537f37cda412959705346))
|
||||
* **core:** Fix possible corruption of OAuth2 credential ([#12880](https://github.com/n8n-io/n8n/issues/12880)) ([ac84ea1](https://github.com/n8n-io/n8n/commit/ac84ea14452cbcec95f14073e8e70427169e6a7f))
|
||||
* **core:** Fix usage of external libs in task runner ([#12788](https://github.com/n8n-io/n8n/issues/12788)) ([3d9d5bf](https://github.com/n8n-io/n8n/commit/3d9d5bf9d58f3c49830d42a140d6c8c6b59952dc))
|
||||
* **core:** Handle max stalled count error better ([#12824](https://github.com/n8n-io/n8n/issues/12824)) ([eabf160](https://github.com/n8n-io/n8n/commit/eabf1609577cd94a6bad5020c34378d840a13bc0))
|
||||
* **core:** Improve error handling in credential decryption and parsing ([#12868](https://github.com/n8n-io/n8n/issues/12868)) ([0c86bf2](https://github.com/n8n-io/n8n/commit/0c86bf2b3761bb93fd3cedba7a483ae5d97bd332))
|
||||
* **core:** Renew license on startup for instances with detached floating entitlements ([#12884](https://github.com/n8n-io/n8n/issues/12884)) ([f32eef8](https://github.com/n8n-io/n8n/commit/f32eef85bd066ee9b54d110355c6b80124d67437))
|
||||
* **core:** Update execution entity and execution data in transaction ([#12756](https://github.com/n8n-io/n8n/issues/12756)) ([1f43181](https://github.com/n8n-io/n8n/commit/1f4318136011bffaad04527790a9eba79effce35))
|
||||
* **core:** Validate credential data before encryption ([#12885](https://github.com/n8n-io/n8n/issues/12885)) ([3d27a14](https://github.com/n8n-io/n8n/commit/3d27a1498702206b738cf978d037191306cec42b))
|
||||
* **editor:** Add notice when user hits the limit for execution metadata item length ([#12676](https://github.com/n8n-io/n8n/issues/12676)) ([02df25c](https://github.com/n8n-io/n8n/commit/02df25c450a0a384a32d0815d8a2faec7562a8ae))
|
||||
* **editor:** Don't send run data for full manual executions ([#12687](https://github.com/n8n-io/n8n/issues/12687)) ([9139dc3](https://github.com/n8n-io/n8n/commit/9139dc3c2916186648fb5bf63d14fcb90773eb1c))
|
||||
* **editor:** Fix sub-execution links in empty output tables ([#12781](https://github.com/n8n-io/n8n/issues/12781)) ([114ed88](https://github.com/n8n-io/n8n/commit/114ed88368d137443b9c6605d4fe11b02053549d))
|
||||
* **editor:** Fix workflow move project select filtering ([#12764](https://github.com/n8n-io/n8n/issues/12764)) ([358d284](https://github.com/n8n-io/n8n/commit/358d2843e5e468071d6764419169811e93138c35))
|
||||
* **editor:** Focus executions iframe when n8n is ready to delegate keyboard events ([#12741](https://github.com/n8n-io/n8n/issues/12741)) ([d506218](https://github.com/n8n-io/n8n/commit/d5062189dbca02dfdf485fc220cc2a7b05e3e6cc))
|
||||
* **editor:** Handle large payloads in the AI Assistant requests better ([#12747](https://github.com/n8n-io/n8n/issues/12747)) ([eb4dea1](https://github.com/n8n-io/n8n/commit/eb4dea1ca891bb7ac07c8bbbae8803de080c4623))
|
||||
* **editor:** Hide Set up Template button for empty workflows ([#12808](https://github.com/n8n-io/n8n/issues/12808)) ([36e615b](https://github.com/n8n-io/n8n/commit/36e615b28f395623457bbb9bf4ab6fd69102b6ea))
|
||||
* **editor:** Load appropriate credentials in canvas V2 for new workflow ([#12722](https://github.com/n8n-io/n8n/issues/12722)) ([2020dc5](https://github.com/n8n-io/n8n/commit/2020dc502feae6cae827dfbcc40ffed89bcc334a))
|
||||
* **editor:** Properly set active project in new canvas ([#12810](https://github.com/n8n-io/n8n/issues/12810)) ([648c6f9](https://github.com/n8n-io/n8n/commit/648c6f9315b16b885e04716e7e0035a73b358fb0))
|
||||
* **editor:** Render inline SVGs correctly on the external secrets settings page ([#12802](https://github.com/n8n-io/n8n/issues/12802)) ([5820ade](https://github.com/n8n-io/n8n/commit/5820ade1e4b9d638c9b6369aef369d6dc9320da6))
|
||||
* **editor:** Show input selector when node has error ([#12813](https://github.com/n8n-io/n8n/issues/12813)) ([5b760e7](https://github.com/n8n-io/n8n/commit/5b760e7f7fc612b10307b4871e24b549f5d9d420))
|
||||
* **editor:** Show mappings by default in sub-node NDVs when the root node isn't executed ([#12642](https://github.com/n8n-io/n8n/issues/12642)) ([fb662dd](https://github.com/n8n-io/n8n/commit/fb662dd95cae3bc51d05d05e32e772d05adafa1e))
|
||||
* **Postgres PGVector Store Node:** Release postgres connections back to the pool ([#12723](https://github.com/n8n-io/n8n/issues/12723)) ([663dfb4](https://github.com/n8n-io/n8n/commit/663dfb48defd944f88f0ecc4f3347ea4f8a7c831))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add DeepSeek Chat Model node ([#12873](https://github.com/n8n-io/n8n/issues/12873)) ([9918afa](https://github.com/n8n-io/n8n/commit/9918afa51b16116abb73692a66df84e48128f406))
|
||||
* Add OpenRouter node ([#12882](https://github.com/n8n-io/n8n/issues/12882)) ([dc85b02](https://github.com/n8n-io/n8n/commit/dc85b022d111d1e8b038ca1a9f6a1041f19cf2b0))
|
||||
* Add timeout options to sendAndWait operations ([#12753](https://github.com/n8n-io/n8n/issues/12753)) ([3e9f24d](https://github.com/n8n-io/n8n/commit/3e9f24ddf462349145d89fe183313c95512c699b))
|
||||
* **API:** Add route for schema static files ([#12770](https://github.com/n8n-io/n8n/issues/12770)) ([d981b56](https://github.com/n8n-io/n8n/commit/d981b5659a26f92b11e5d0cd5570504fd683626c))
|
||||
* **core:** Explicitly report external hook failures ([#12830](https://github.com/n8n-io/n8n/issues/12830)) ([a24e442](https://github.com/n8n-io/n8n/commit/a24e4420bb9023f808acd756d125dffaea325968))
|
||||
* **core:** Rename two task runner env vars ([#12763](https://github.com/n8n-io/n8n/issues/12763)) ([60187ca](https://github.com/n8n-io/n8n/commit/60187cab9bc9d21aa6ba710d772c068324e429f1))
|
||||
* **editor:** Add evaluation workflow and enhance workflow selector with pinned data support ([#12773](https://github.com/n8n-io/n8n/issues/12773)) ([be967eb](https://github.com/n8n-io/n8n/commit/be967ebec07fab223513f93f50bcc389b9a4c548))
|
||||
* **editor:** Always keep at least one executing node indicator in the workflow ([#12829](https://github.com/n8n-io/n8n/issues/12829)) ([c25c613](https://github.com/n8n-io/n8n/commit/c25c613a04a6773fa4014d9a0d290e443bcabbe0))
|
||||
* **Google Chat Node:** Updates ([#12827](https://github.com/n8n-io/n8n/issues/12827)) ([e146ad0](https://github.com/n8n-io/n8n/commit/e146ad021a0be22cf51bafa3c015d03550e03d97))
|
||||
* **Microsoft Outlook Node:** New operation sendAndWait ([#12795](https://github.com/n8n-io/n8n/issues/12795)) ([f4bf55f](https://github.com/n8n-io/n8n/commit/f4bf55f0d8278ff954344cf6397c10d8261b39a4))
|
||||
* **n8n Form Node:** Add read-only/custom HTML form elements ([#12760](https://github.com/n8n-io/n8n/issues/12760)) ([ba8aa39](https://github.com/n8n-io/n8n/commit/ba8aa3921613c590caaac627fbb9837ccaf87783))
|
||||
* **Send Email Node:** New operation sendAndWait ([#12775](https://github.com/n8n-io/n8n/issues/12775)) ([a197fbb](https://github.com/n8n-io/n8n/commit/a197fbb21b5642843d8bc3e657049aca99e0729d))
|
||||
* **Summarize Node:** Turns error when field not found in items into warning ([#11889](https://github.com/n8n-io/n8n/issues/11889)) ([d7dda3f](https://github.com/n8n-io/n8n/commit/d7dda3f5de52925e554455f9f10e51bd173ea856))
|
||||
* **Telegram Node:** New operation sendAndWait ([#12771](https://github.com/n8n-io/n8n/issues/12771)) ([2c58d47](https://github.com/n8n-io/n8n/commit/2c58d47f8eee1f865ecc1eeb89aa20c69c28abae))
|
||||
|
||||
|
||||
|
||||
# [1.76.0](https://github.com/n8n-io/n8n/compare/n8n@1.75.0...n8n@1.76.0) (2025-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Align saving behavior in `workflowExecuteAfter` hooks ([#12731](https://github.com/n8n-io/n8n/issues/12731)) ([9d76210](https://github.com/n8n-io/n8n/commit/9d76210a570e025d01d1f6596667abf40fbd8d12))
|
||||
* **core:** AugmentObject should handle the constructor property correctly ([#12744](https://github.com/n8n-io/n8n/issues/12744)) ([36bc164](https://github.com/n8n-io/n8n/commit/36bc164da486f2e2d05091b457b8eea6521ca22e))
|
||||
* **core:** Fix keyboard shortcuts for non-ansi layouts ([#12672](https://github.com/n8n-io/n8n/issues/12672)) ([4c8193f](https://github.com/n8n-io/n8n/commit/4c8193fedc2e3967c9a06c0652483128df509653))
|
||||
* **core:** Fix license CLI commands showing incorrect renewal setting ([#12759](https://github.com/n8n-io/n8n/issues/12759)) ([024ada8](https://github.com/n8n-io/n8n/commit/024ada822c1bc40958e594bb08707cf77d3397ec))
|
||||
* **core:** Fix license initialization failure on startup ([#12737](https://github.com/n8n-io/n8n/issues/12737)) ([ac2f647](https://github.com/n8n-io/n8n/commit/ac2f6476c114f51fafb9b7b66e41e0c87f4a1bf6))
|
||||
* **core:** Recover successful data-less executions ([#12720](https://github.com/n8n-io/n8n/issues/12720)) ([a39b8bd](https://github.com/n8n-io/n8n/commit/a39b8bd32be50c8323e415f820b25b4bcb81d960))
|
||||
* **core:** Remove run data of utility nodes for partial executions v2 ([#12673](https://github.com/n8n-io/n8n/issues/12673)) ([b66a9dc](https://github.com/n8n-io/n8n/commit/b66a9dc8fb6f7b19122cacbb7e2f86b4c921c3fb))
|
||||
* **core:** Sync `hookFunctionsSave` and `hookFunctionsSaveWorker` ([#12740](https://github.com/n8n-io/n8n/issues/12740)) ([d410b8f](https://github.com/n8n-io/n8n/commit/d410b8f5a7e99658e1e8dcb2e02901bd01ce9c59))
|
||||
* **core:** Update isDocker check to return true on kubernetes/containerd ([#12603](https://github.com/n8n-io/n8n/issues/12603)) ([c55dac6](https://github.com/n8n-io/n8n/commit/c55dac66ed97a2317d4c696c3b505790ec5d72fe))
|
||||
* **editor:** Add unicode code points to expression language for emoji ([#12633](https://github.com/n8n-io/n8n/issues/12633)) ([819ebd0](https://github.com/n8n-io/n8n/commit/819ebd058d1d60b3663d92b4a652728da7134a3b))
|
||||
* **editor:** Correct missing whitespace in JSON output ([#12677](https://github.com/n8n-io/n8n/issues/12677)) ([b098b19](https://github.com/n8n-io/n8n/commit/b098b19c7f0e3a9848c3fcfa012999050f2d3c7a))
|
||||
* **editor:** Defer crypto.randomUUID call in CodeNodeEditor ([#12630](https://github.com/n8n-io/n8n/issues/12630)) ([58f6532](https://github.com/n8n-io/n8n/commit/58f6532630bacd288d3c0a79b40150f465898419))
|
||||
* **editor:** Fix Code node bug erasing and overwriting code when switching between nodes ([#12637](https://github.com/n8n-io/n8n/issues/12637)) ([02d953d](https://github.com/n8n-io/n8n/commit/02d953db34ec4e44977a8ca908628b62cca82fde))
|
||||
* **editor:** Fix execution list hover & selection colour in dark mode ([#12628](https://github.com/n8n-io/n8n/issues/12628)) ([95c40c0](https://github.com/n8n-io/n8n/commit/95c40c02cb8fef77cf633cf5aec08e98746cff36))
|
||||
* **editor:** Fix JsonEditor with expressions ([#12739](https://github.com/n8n-io/n8n/issues/12739)) ([56c93ca](https://github.com/n8n-io/n8n/commit/56c93caae026738c1c0bebb4187b238e34a330f6))
|
||||
* **editor:** Fix navbar height flickering during load ([#12738](https://github.com/n8n-io/n8n/issues/12738)) ([a96b3f0](https://github.com/n8n-io/n8n/commit/a96b3f0091798a52bb33107b919b5d8287ba7506))
|
||||
* **editor:** Open chat when executing agent node in canvas v2 ([#12617](https://github.com/n8n-io/n8n/issues/12617)) ([457edd9](https://github.com/n8n-io/n8n/commit/457edd99bb853d8ccf3014605d5823933f3c0bc6))
|
||||
* **editor:** Partial execution of a workflow with manual chat trigger ([#12662](https://github.com/n8n-io/n8n/issues/12662)) ([2f81b29](https://github.com/n8n-io/n8n/commit/2f81b29d341535b512df0aa01b25a91d109f113f))
|
||||
* **editor:** Show connector label above the line when it's straight ([#12622](https://github.com/n8n-io/n8n/issues/12622)) ([c97bd48](https://github.com/n8n-io/n8n/commit/c97bd48a77643b9c2a5d7218e21b957af15cee0b))
|
||||
* **editor:** Show run workflow button when chat trigger has pinned data ([#12616](https://github.com/n8n-io/n8n/issues/12616)) ([da8aafc](https://github.com/n8n-io/n8n/commit/da8aafc0e3a1b5d862f0723d0d53d2c38bcaebc3))
|
||||
* **editor:** Update workflow re-initialization to use query parameter ([#12650](https://github.com/n8n-io/n8n/issues/12650)) ([982131a](https://github.com/n8n-io/n8n/commit/982131a75a32f741c120156826c303989aac189c))
|
||||
* **Execute Workflow Node:** Pass binary data to sub-workflow ([#12635](https://github.com/n8n-io/n8n/issues/12635)) ([e9c152e](https://github.com/n8n-io/n8n/commit/e9c152e369a4c2762bd8e6ad17eaa704bb3771bb))
|
||||
* **Google Gemini Chat Model Node:** Add base URL support for Google Gemini Chat API ([#12643](https://github.com/n8n-io/n8n/issues/12643)) ([14f4bc7](https://github.com/n8n-io/n8n/commit/14f4bc769027789513808b4000444edf99dc5d1c))
|
||||
* **GraphQL Node:** Change default request format to json instead of graphql ([#11346](https://github.com/n8n-io/n8n/issues/11346)) ([c7c122f](https://github.com/n8n-io/n8n/commit/c7c122f9173df824cc1b5ab864333bffd0d31f82))
|
||||
* **Jira Software Node:** Get custom fields(RLC) in update operation for server deployment type ([#12719](https://github.com/n8n-io/n8n/issues/12719)) ([353df79](https://github.com/n8n-io/n8n/commit/353df7941117e20547cd4f3fc514979a54619720))
|
||||
* **n8n Form Node:** Remove the ability to change the formatting of dates ([#12666](https://github.com/n8n-io/n8n/issues/12666)) ([14904ff](https://github.com/n8n-io/n8n/commit/14904ff77951fef23eb789a43947492a4cd3fa20))
|
||||
* **OpenAI Chat Model Node:** Fix loading of custom models when using custom credential URL ([#12634](https://github.com/n8n-io/n8n/issues/12634)) ([7cc553e](https://github.com/n8n-io/n8n/commit/7cc553e3b277a16682bfca1ea08cb98178e38580))
|
||||
* **OpenAI Chat Model Node:** Restore default model value ([#12745](https://github.com/n8n-io/n8n/issues/12745)) ([d1b6692](https://github.com/n8n-io/n8n/commit/d1b6692736182fa2eab768ba3ad0adb8504ebbbd))
|
||||
* **Postgres Chat Memory Node:** Do not terminate the connection pool ([#12674](https://github.com/n8n-io/n8n/issues/12674)) ([e7f00bc](https://github.com/n8n-io/n8n/commit/e7f00bcb7f2dce66ca07a9322d50f96356c1a43d))
|
||||
* **Postgres Node:** Allow using composite key in upsert queries ([#12639](https://github.com/n8n-io/n8n/issues/12639)) ([83ce3a9](https://github.com/n8n-io/n8n/commit/83ce3a90963ba76601234f4314363a8ccc310f0f))
|
||||
* **Wait Node:** Fix for hasNextPage in waiting forms ([#12636](https://github.com/n8n-io/n8n/issues/12636)) ([652b8d1](https://github.com/n8n-io/n8n/commit/652b8d170b9624d47b5f2d8d679c165cc14ea548))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add credential only node for Microsoft Azure Monitor ([#12645](https://github.com/n8n-io/n8n/issues/12645)) ([6ef8882](https://github.com/n8n-io/n8n/commit/6ef8882a108c672ab097c9dd1c590d4e9e7f3bcc))
|
||||
* Add Miro credential only node ([#12746](https://github.com/n8n-io/n8n/issues/12746)) ([5b29086](https://github.com/n8n-io/n8n/commit/5b29086e2f9b7f638fac4440711f673438e57492))
|
||||
* Add SSM endpoint to AWS credentials ([#12212](https://github.com/n8n-io/n8n/issues/12212)) ([565c7b8](https://github.com/n8n-io/n8n/commit/565c7b8b9cfd3e10f6a2c60add96fea4c4d95d33))
|
||||
* **core:** Enable task runner by default ([#12726](https://github.com/n8n-io/n8n/issues/12726)) ([9e2a01a](https://github.com/n8n-io/n8n/commit/9e2a01aeaf36766a1cf7a1d9a4d6e02f45739bd3))
|
||||
* **editor:** Force final canvas v2 migration and remove switcher from UI ([#12717](https://github.com/n8n-io/n8n/issues/12717)) ([29335b9](https://github.com/n8n-io/n8n/commit/29335b9b6acf97c817bea70688e8a2786fbd8889))
|
||||
* **editor:** VariablesView Reskin - Add Filters for missing values ([#12611](https://github.com/n8n-io/n8n/issues/12611)) ([1eeb788](https://github.com/n8n-io/n8n/commit/1eeb788d327287d21eab7ad6f2156453ab7642c7))
|
||||
* **Jira Software Node:** Personal Access Token credential type ([#11038](https://github.com/n8n-io/n8n/issues/11038)) ([1c7a38f](https://github.com/n8n-io/n8n/commit/1c7a38f6bab108daa47401cd98c185590bf299a8))
|
||||
* **n8n Form Trigger Node:** Form Improvements ([#12590](https://github.com/n8n-io/n8n/issues/12590)) ([f167578](https://github.com/n8n-io/n8n/commit/f167578b3251e553a4d000e731e1bb60348916ad))
|
||||
* Synchronize deletions when pulling from source control ([#12170](https://github.com/n8n-io/n8n/issues/12170)) ([967ee4b](https://github.com/n8n-io/n8n/commit/967ee4b89b94b92fc3955c56bf4c9cca0bd64eac))
|
||||
|
||||
|
||||
|
||||
# [1.75.0](https://github.com/n8n-io/n8n/compare/n8n@1.74.0...n8n@1.75.0) (2025-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** AugmentObject should check for own propeties correctly ([#12534](https://github.com/n8n-io/n8n/issues/12534)) ([0cdf393](https://github.com/n8n-io/n8n/commit/0cdf39374305e6bbcedb047db7d3756168e6e89e))
|
||||
* **core:** Disallow code generation in task runner ([#12522](https://github.com/n8n-io/n8n/issues/12522)) ([35b6180](https://github.com/n8n-io/n8n/commit/35b618098b7d23e272bf77b55c172dbe531c821f))
|
||||
* **core:** Fix node exclusion on the frontend types ([#12544](https://github.com/n8n-io/n8n/issues/12544)) ([b2cbed9](https://github.com/n8n-io/n8n/commit/b2cbed9865888f6f3bc528984d4091d86a88f0d6))
|
||||
* **core:** Fix orchestration flow with expired license ([#12444](https://github.com/n8n-io/n8n/issues/12444)) ([ecff3b7](https://github.com/n8n-io/n8n/commit/ecff3b732a028d7225bfbed4ffc65dc20c4ed608))
|
||||
* **core:** Fix Sentry error reporting on task runners ([#12495](https://github.com/n8n-io/n8n/issues/12495)) ([88c0838](https://github.com/n8n-io/n8n/commit/88c0838dd72f11646bdb3586223d6c16631cccab))
|
||||
* **core:** Improve cyclic dependency check in the DI container ([#12600](https://github.com/n8n-io/n8n/issues/12600)) ([c3c4a20](https://github.com/n8n-io/n8n/commit/c3c4a200024fb08afb9380357d1490c6707c5ec3))
|
||||
* **core:** Only show personal credentials in the personal space ([#12433](https://github.com/n8n-io/n8n/issues/12433)) ([8a42d55](https://github.com/n8n-io/n8n/commit/8a42d55d91f4a37fff5669d52d52428b3a4ddd44))
|
||||
* **core:** Prefix package name in `supportedNodes` on generated types as well ([#12514](https://github.com/n8n-io/n8n/issues/12514)) ([4a1a999](https://github.com/n8n-io/n8n/commit/4a1a9993624c92dd81f5418f9268cb93878069ab))
|
||||
* **core:** Prevent prototype pollution in task runner ([#12588](https://github.com/n8n-io/n8n/issues/12588)) ([bdf266c](https://github.com/n8n-io/n8n/commit/bdf266cf55032d05641b20dce8804412dc93b6d5))
|
||||
* **core:** Prevent prototype pollution of internal classes in task runner ([#12610](https://github.com/n8n-io/n8n/issues/12610)) ([eceee7f](https://github.com/n8n-io/n8n/commit/eceee7f3f8899d200b1c5720087cc494eec22e6a))
|
||||
* **core:** Use timing safe function to compare runner auth tokens ([#12485](https://github.com/n8n-io/n8n/issues/12485)) ([8fab98f](https://github.com/n8n-io/n8n/commit/8fab98f3f1f767d05825d24cbf155d56375fdb3e))
|
||||
* **core:** Validate values which are intentionally 0 ([#12382](https://github.com/n8n-io/n8n/issues/12382)) ([562506e](https://github.com/n8n-io/n8n/commit/562506e92aeb26423145801bff80037e5ce2ac46))
|
||||
* Don't break oauth credentials when updating them and allow fixing broken oauth credentials by repeating the authorization flow ([#12563](https://github.com/n8n-io/n8n/issues/12563)) ([73897c7](https://github.com/n8n-io/n8n/commit/73897c7662a432834eb6f9d0f9ace8d986c1acb5))
|
||||
* **editor:** Don't show toolsUnused notice if run had errors ([#12529](https://github.com/n8n-io/n8n/issues/12529)) ([3ec5b28](https://github.com/n8n-io/n8n/commit/3ec5b2850c47057032e61c2acdbdfc1dcdd931f7))
|
||||
* **editor:** Ensure proper "AI Template" URL construction in node creator ([#12566](https://github.com/n8n-io/n8n/issues/12566)) ([13bf69f](https://github.com/n8n-io/n8n/commit/13bf69f75c67bc37a37013e776525768676a4b88))
|
||||
* **editor:** Fix NDV resize handle and scrollbar overlapping ([#12509](https://github.com/n8n-io/n8n/issues/12509)) ([c28f302](https://github.com/n8n-io/n8n/commit/c28f302c2f863bd7aa73ad52e5d040f927e33220))
|
||||
* **editor:** Fix parameter input validation ([#12532](https://github.com/n8n-io/n8n/issues/12532)) ([6711cbc](https://github.com/n8n-io/n8n/commit/6711cbcc641a2fc70f5c15a7e2dcc640a3f98b66))
|
||||
* **editor:** Fix selection rectangle context menu on new canvas ([#12584](https://github.com/n8n-io/n8n/issues/12584)) ([c8e3c53](https://github.com/n8n-io/n8n/commit/c8e3c5399efde93486c1dd5c373cb2c5ff8a0691))
|
||||
* **editor:** Fix the `openselectivenodecreator` custom action on new canvas ([#12580](https://github.com/n8n-io/n8n/issues/12580)) ([2110e9a](https://github.com/n8n-io/n8n/commit/2110e9a0513b8c36beb85302e0d38a2658ea5d6e))
|
||||
* **editor:** Fix workflow initilisation for test definition routes & add unit tests ([#12507](https://github.com/n8n-io/n8n/issues/12507)) ([2775f61](https://github.com/n8n-io/n8n/commit/2775f617ae5c267c0a1ce7a54d05d4077cdbc0f7))
|
||||
* **editor:** Make clicking item in RLC work the first time on small screens ([#12585](https://github.com/n8n-io/n8n/issues/12585)) ([479933f](https://github.com/n8n-io/n8n/commit/479933fbd5c88e783827960e018abb979de8a039))
|
||||
* **editor:** Make sure code editors work correctly in fullscreen ([#12597](https://github.com/n8n-io/n8n/issues/12597)) ([aa1f3a7](https://github.com/n8n-io/n8n/commit/aa1f3a7d989883d55df3777775b8d7d336f6e3b7))
|
||||
* **editor:** Override selected nodes on single click without Meta/Ctrl key ([#12549](https://github.com/n8n-io/n8n/issues/12549)) ([02c2d5e](https://github.com/n8n-io/n8n/commit/02c2d5e71d15b9292fddd585f47bd8334da468c5))
|
||||
* **editor:** Show NDV errors when opening existing nodes with errors ([#12567](https://github.com/n8n-io/n8n/issues/12567)) ([bee7267](https://github.com/n8n-io/n8n/commit/bee7267fe38ab12a79fa4ec0e775f45d98d48aa5))
|
||||
* **editor:** Swap Activate/Deactivate texts in FloatingToolbar ([#12526](https://github.com/n8n-io/n8n/issues/12526)) ([44679b4](https://github.com/n8n-io/n8n/commit/44679b42aa1e14bc7069bee47d0a91ca84b1dba4))
|
||||
* **editor:** Update filter and feedback for source control ([#12504](https://github.com/n8n-io/n8n/issues/12504)) ([865fc21](https://github.com/n8n-io/n8n/commit/865fc21276727e8d88ccee0355147904b81c4421))
|
||||
* **editor:** Update selected node when navigating via flowing nodes ([#12581](https://github.com/n8n-io/n8n/issues/12581)) ([88659d8](https://github.com/n8n-io/n8n/commit/88659d8a2901786c894902e19466f395bcdaab8e))
|
||||
* **Google Calendar Node:** Updates and fixes ([#10715](https://github.com/n8n-io/n8n/issues/10715)) ([7227a29](https://github.com/n8n-io/n8n/commit/7227a29845fd178ced4d281597c62e7a03245456))
|
||||
* **Spotify Node:** Fix issue with null values breaking the response ([#12080](https://github.com/n8n-io/n8n/issues/12080)) ([a56a462](https://github.com/n8n-io/n8n/commit/a56a46259d257003c813103578260d625b3f17dd))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **editor:** Make node credential select searchable ([#12497](https://github.com/n8n-io/n8n/issues/12497)) ([91277c4](https://github.com/n8n-io/n8n/commit/91277c44f1cf3f334b3b50d47d7dcc79b11c7c63))
|
||||
* **editor:** Persist sidebar collapsed status preference ([#12505](https://github.com/n8n-io/n8n/issues/12505)) ([dba7d46](https://github.com/n8n-io/n8n/commit/dba7d46f3ec91d26a597a50dede7b6ca292c728f))
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ This automatically sets up file-links between modules which depend on each other
|
|||
|
||||
We recommend enabling [Node.js corepack](https://nodejs.org/docs/latest-v16.x/api/corepack.html) with `corepack enable`.
|
||||
|
||||
With Node.js v16.17 or newer, you can install the latest version of pnpm: `corepack prepare pnpm@latest --activate`. If you use an older version install at least version 7.18 of pnpm via: `corepack prepare pnpm@7.18.0 --activate`.
|
||||
With Node.js v16.17 or newer, you can install the latest version of pnpm: `corepack prepare pnpm@latest --activate`. If you use an older version install at least version 9.15 of pnpm via: `corepack prepare pnpm@9.15.5 --activate`.
|
||||
|
||||
**IMPORTANT**: If you have installed Node.js via homebrew, you'll need to run `brew install corepack`, since homebrew explicitly removes `npm` and `corepack` from [the `node` formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/node.rb#L66).
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ component_management:
|
|||
- packages/@n8n/codemirror-lang/**
|
||||
- packages/design-system/**
|
||||
- packages/editor-ui/**
|
||||
- packages/frontend/**
|
||||
- component_id: nodes_packages
|
||||
name: Nodes
|
||||
paths:
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
* Getters
|
||||
*/
|
||||
|
||||
import { clearNotifications } from '../../pages/notifications';
|
||||
|
||||
export function getCredentialConnectionParameterInputs() {
|
||||
return cy.getByTestId('credential-connection-parameter');
|
||||
}
|
||||
|
@ -55,5 +57,6 @@ export function setCredentialValues(values: Record<string, string>, save = true)
|
|||
if (save) {
|
||||
saveCredential();
|
||||
closeCredentialModal();
|
||||
clearNotifications();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
* Getters
|
||||
*/
|
||||
|
||||
import { getVisibleSelect } from '../utils/popper';
|
||||
import { getVisiblePopper, getVisibleSelect } from '../utils/popper';
|
||||
|
||||
export function getNdvContainer() {
|
||||
return cy.getByTestId('ndv');
|
||||
}
|
||||
|
||||
export function getCredentialSelect(eq = 0) {
|
||||
return cy.getByTestId('node-credentials-select').eq(eq);
|
||||
|
@ -36,6 +40,18 @@ export function getOutputPanel() {
|
|||
return cy.getByTestId('output-panel');
|
||||
}
|
||||
|
||||
export function getFixedCollection(collectionName: string) {
|
||||
return cy.getByTestId(`fixed-collection-${collectionName}`);
|
||||
}
|
||||
|
||||
export function getResourceLocator(paramName: string) {
|
||||
return cy.getByTestId(`resource-locator-${paramName}`);
|
||||
}
|
||||
|
||||
export function getResourceLocatorInput(paramName: string) {
|
||||
return getResourceLocator(paramName).find('[data-test-id="rlc-input-container"]');
|
||||
}
|
||||
|
||||
export function getOutputPanelDataContainer() {
|
||||
return getOutputPanel().getByTestId('ndv-data-container');
|
||||
}
|
||||
|
@ -84,6 +100,30 @@ export function getOutputPanelRelatedExecutionLink() {
|
|||
return getOutputPanel().getByTestId('related-execution-link');
|
||||
}
|
||||
|
||||
export function getNodeOutputHint() {
|
||||
return cy.getByTestId('ndv-output-run-node-hint');
|
||||
}
|
||||
|
||||
export function getWorkflowCards() {
|
||||
return cy.getByTestId('resources-list-item');
|
||||
}
|
||||
|
||||
export function getWorkflowCard(workflowName: string) {
|
||||
return getWorkflowCards().contains(workflowName).parents('[data-test-id="resources-list-item"]');
|
||||
}
|
||||
|
||||
export function getWorkflowCardContent(workflowName: string) {
|
||||
return getWorkflowCard(workflowName).findChildByTestId('card-content');
|
||||
}
|
||||
|
||||
export function getNodeRunInfoStale() {
|
||||
return cy.getByTestId('node-run-info-stale');
|
||||
}
|
||||
|
||||
export function getNodeOutputErrorMessage() {
|
||||
return getOutputPanel().findChildByTestId('node-error-message');
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
@ -110,12 +150,20 @@ export function clickExecuteNode() {
|
|||
getExecuteNodeButton().click();
|
||||
}
|
||||
|
||||
export function clickResourceLocatorInput(paramName: string) {
|
||||
getResourceLocatorInput(paramName).click();
|
||||
}
|
||||
|
||||
export function setParameterInputByName(name: string, value: string) {
|
||||
getParameterInputByName(name).clear().type(value);
|
||||
}
|
||||
|
||||
export function toggleParameterCheckboxInputByName(name: string) {
|
||||
getParameterInputByName(name).find('input[type="checkbox"]').realClick();
|
||||
export function checkParameterCheckboxInputByName(name: string) {
|
||||
getParameterInputByName(name).find('input[type="checkbox"]').check({ force: true });
|
||||
}
|
||||
|
||||
export function uncheckParameterCheckboxInputByName(name: string) {
|
||||
getParameterInputByName(name).find('input[type="checkbox"]').uncheck({ force: true });
|
||||
}
|
||||
|
||||
export function setParameterSelectByContent(name: string, content: string) {
|
||||
|
@ -127,3 +175,86 @@ export function changeOutputRunSelector(runName: string) {
|
|||
getOutputRunSelector().click();
|
||||
getVisibleSelect().find('.el-select-dropdown__item').contains(runName).click();
|
||||
}
|
||||
|
||||
export function addItemToFixedCollection(collectionName: string) {
|
||||
getFixedCollection(collectionName).getByTestId('fixed-collection-add').click();
|
||||
}
|
||||
|
||||
export function typeIntoFixedCollectionItem(collectionName: string, index: number, value: string) {
|
||||
getFixedCollection(collectionName).within(() =>
|
||||
cy.getByTestId('parameter-input').eq(index).type(value),
|
||||
);
|
||||
}
|
||||
|
||||
export function selectResourceLocatorItem(
|
||||
resourceLocator: string,
|
||||
index: number,
|
||||
expectedText: string,
|
||||
) {
|
||||
clickResourceLocatorInput(resourceLocator);
|
||||
|
||||
getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist');
|
||||
getVisiblePopper()
|
||||
.findChildByTestId('rlc-item')
|
||||
.eq(index)
|
||||
.find('span')
|
||||
.should('contain.text', expectedText)
|
||||
.click();
|
||||
}
|
||||
|
||||
export function clickWorkflowCardContent(workflowName: string) {
|
||||
getWorkflowCardContent(workflowName).click();
|
||||
}
|
||||
|
||||
export function assertNodeOutputHintExists() {
|
||||
getNodeOutputHint().should('exist');
|
||||
}
|
||||
|
||||
export function assertNodeOutputErrorMessageExists() {
|
||||
return getNodeOutputErrorMessage().should('exist');
|
||||
}
|
||||
|
||||
// Note that this only validates the expectedContent is *included* in the output table
|
||||
export function assertOutputTableContent(expectedContent: unknown[][]) {
|
||||
for (const [i, row] of expectedContent.entries()) {
|
||||
for (const [j, value] of row.entries()) {
|
||||
// + 1 to skip header
|
||||
getOutputTbodyCell(1 + i, j).should('have.text', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function populateMapperFields(fields: ReadonlyArray<[string, string]>) {
|
||||
for (const [name, value] of fields) {
|
||||
getParameterInputByName(name).type(value);
|
||||
|
||||
// Click on a parent to dismiss the pop up which hides the field below.
|
||||
getParameterInputByName(name).parent().parent().parent().parent().click('topLeft');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate multiValue fixedCollections. Only supports fixedCollections for which all fields can be defined via keyboard typing
|
||||
*
|
||||
* @param items - 2D array of items to populate, i.e. [["myField1", "String"], ["myField2", "Number"]]
|
||||
* @param collectionName - name of the fixedCollection to populate
|
||||
* @param offset - amount of 'parameter-input's before start, e.g. from a controlling dropdown that makes the fields appear
|
||||
* @returns
|
||||
*/
|
||||
export function populateFixedCollection<T extends readonly string[]>(
|
||||
items: readonly T[],
|
||||
collectionName: string,
|
||||
offset: number = 0,
|
||||
) {
|
||||
if (items.length === 0) return;
|
||||
const n = items[0].length;
|
||||
for (const [i, params] of items.entries()) {
|
||||
addItemToFixedCollection(collectionName);
|
||||
for (const [j, param] of params.entries()) {
|
||||
getFixedCollection(collectionName)
|
||||
.getByTestId('parameter-input')
|
||||
.eq(offset + i * n + j)
|
||||
.type(`${param}{downArrow}{enter}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
81
cypress/composables/webhooks.ts
Normal file
81
cypress/composables/webhooks.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { BACKEND_BASE_URL } from '../constants';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
export const waitForWebhook = 500;
|
||||
|
||||
export interface SimpleWebhookCallOptions {
|
||||
method: string;
|
||||
webhookPath: string;
|
||||
responseCode?: number;
|
||||
respondWith?: string;
|
||||
executeNow?: boolean;
|
||||
responseData?: string;
|
||||
authentication?: string;
|
||||
}
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
export const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||
const {
|
||||
authentication,
|
||||
method,
|
||||
webhookPath,
|
||||
responseCode,
|
||||
respondWith,
|
||||
responseData,
|
||||
executeNow = true,
|
||||
} = options;
|
||||
|
||||
workflowPage.actions.addInitialNodeToCanvas('Webhook');
|
||||
workflowPage.actions.openNode('Webhook');
|
||||
|
||||
cy.getByTestId('parameter-input-httpMethod').click();
|
||||
getVisibleSelect().find('.option-headline').contains(method).click();
|
||||
cy.getByTestId('parameter-input-path')
|
||||
.find('.parameter-input')
|
||||
.find('input')
|
||||
.clear()
|
||||
.type(webhookPath);
|
||||
|
||||
if (authentication) {
|
||||
cy.getByTestId('parameter-input-authentication').click();
|
||||
getVisibleSelect().find('.option-headline').contains(authentication).click();
|
||||
}
|
||||
|
||||
if (responseCode) {
|
||||
cy.get('.param-options').click();
|
||||
getVisibleSelect().contains('Response Code').click();
|
||||
cy.get('.parameter-item-wrapper > .parameter-input-list-wrapper').children().click();
|
||||
getVisibleSelect().contains('201').click();
|
||||
}
|
||||
|
||||
if (respondWith) {
|
||||
cy.getByTestId('parameter-input-responseMode').click();
|
||||
getVisibleSelect().find('.option-headline').contains(respondWith).click();
|
||||
}
|
||||
|
||||
if (responseData) {
|
||||
cy.getByTestId('parameter-input-responseData').click();
|
||||
getVisibleSelect().find('.option-headline').contains(responseData).click();
|
||||
}
|
||||
|
||||
const callEndpoint = (fn: (response: Cypress.Response<unknown>) => void) => {
|
||||
cy.request(method, `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(fn);
|
||||
};
|
||||
|
||||
if (executeNow) {
|
||||
ndv.actions.execute();
|
||||
cy.wait(waitForWebhook);
|
||||
|
||||
callEndpoint((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
ndv.getters.outputPanel().contains('headers');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
callEndpoint,
|
||||
};
|
||||
};
|
|
@ -1,12 +1,14 @@
|
|||
import { getManualChatModal } from './modals/chat-modal';
|
||||
import { clickGetBackToCanvas, getParameterInputByName } from './ndv';
|
||||
import { ROUTES } from '../constants';
|
||||
import type { OpenContextMenuOptions } from '../types';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
|
||||
export type EndpointType =
|
||||
| 'main'
|
||||
| 'ai_chain'
|
||||
| 'ai_document'
|
||||
| 'ai_embedding'
|
||||
|
@ -23,9 +25,75 @@ export type EndpointType =
|
|||
* Getters
|
||||
*/
|
||||
|
||||
export function getAddInputEndpointByType(nodeName: string, endpointType: EndpointType) {
|
||||
return cy.get(
|
||||
`.add-input-endpoint[data-jtk-scope-${endpointType}][data-endpoint-name="${nodeName}"]`,
|
||||
export function getCanvas() {
|
||||
return cy.getByTestId('canvas');
|
||||
}
|
||||
|
||||
export function getCanvasPane() {
|
||||
return cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('node-view-background'),
|
||||
() => getCanvas().find('.vue-flow__pane'),
|
||||
);
|
||||
}
|
||||
|
||||
export function getContextMenu() {
|
||||
return cy.getByTestId('context-menu').find('.el-dropdown-menu');
|
||||
}
|
||||
|
||||
export function getContextMenuAction(action: string) {
|
||||
return cy.getByTestId(`context-menu-item-${action}`);
|
||||
}
|
||||
|
||||
export function getInputPlusHandle(nodeName: string) {
|
||||
return cy.ifCanvasVersion(
|
||||
() => cy.get(`.add-input-endpoint[data-endpoint-name="${nodeName}"]`),
|
||||
() =>
|
||||
cy.get(
|
||||
`[data-test-id="canvas-node-input-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function getInputPlusHandleByType(nodeName: string, endpointType: EndpointType) {
|
||||
return cy.ifCanvasVersion(
|
||||
() =>
|
||||
cy.get(
|
||||
`.add-input-endpoint[data-jtk-scope-${endpointType}][data-endpoint-name="${nodeName}"]`,
|
||||
),
|
||||
() =>
|
||||
cy.get(
|
||||
`[data-test-id="canvas-node-input-handle"][data-connection-type="${endpointType}"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function getOutputHandle(nodeName: string) {
|
||||
return cy.ifCanvasVersion(
|
||||
() => cy.get(`.add-output-endpoint[data-endpoint-name="${nodeName}"]`),
|
||||
() => cy.get(`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"]`),
|
||||
);
|
||||
}
|
||||
|
||||
export function getOutputPlusHandle(nodeName: string) {
|
||||
return cy.ifCanvasVersion(
|
||||
() => cy.get(`.add-output-endpoint[data-endpoint-name="${nodeName}"]`),
|
||||
() =>
|
||||
cy.get(
|
||||
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function getOutputPlusHandleByType(nodeName: string, endpointType: EndpointType) {
|
||||
return cy.ifCanvasVersion(
|
||||
() =>
|
||||
cy.get(
|
||||
`.add-output-endpoint[data-jtk-scope-${endpointType}][data-endpoint-name="${nodeName}"]`,
|
||||
),
|
||||
() =>
|
||||
cy.get(
|
||||
`[data-test-id="canvas-node-output-handle"][data-connection-type="${endpointType}"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -33,8 +101,8 @@ export function getNodeCreatorItems() {
|
|||
return cy.getByTestId('item-iterator-item');
|
||||
}
|
||||
|
||||
export function getExecuteWorkflowButton() {
|
||||
return cy.getByTestId('execute-workflow-button');
|
||||
export function getExecuteWorkflowButton(triggerNodeName?: string) {
|
||||
return cy.getByTestId(`execute-workflow-button${triggerNodeName ? `-${triggerNodeName}` : ''}`);
|
||||
}
|
||||
|
||||
export function getManualChatButton() {
|
||||
|
@ -52,6 +120,13 @@ export function getNodeByName(name: string) {
|
|||
);
|
||||
}
|
||||
|
||||
export function getNodeRenderedTypeByName(name: string) {
|
||||
return cy.ifCanvasVersion(
|
||||
() => getNodeByName(name),
|
||||
() => getNodeByName(name).find('[data-canvas-node-render-type]'),
|
||||
);
|
||||
}
|
||||
|
||||
export function getWorkflowHistoryCloseButton() {
|
||||
return cy.getByTestId('workflow-history-close-button');
|
||||
}
|
||||
|
@ -63,10 +138,24 @@ export function disableNode(name: string) {
|
|||
}
|
||||
|
||||
export function getConnectionBySourceAndTarget(source: string, target: string) {
|
||||
return cy.ifCanvasVersion(
|
||||
() =>
|
||||
cy
|
||||
.get('.jtk-connector')
|
||||
.filter(`[data-source-node="${source}"][data-target-node="${target}"]`)
|
||||
.eq(0),
|
||||
() =>
|
||||
cy
|
||||
.getByTestId('edge')
|
||||
.filter(`[data-source-node-name="${source}"][data-target-node-name="${target}"]`)
|
||||
.eq(0),
|
||||
);
|
||||
}
|
||||
|
||||
export function getConnectionLabelBySourceAndTarget(source: string, target: string) {
|
||||
return cy
|
||||
.get('.jtk-connector')
|
||||
.filter(`[data-source-node="${source}"][data-target-node="${target}"]`)
|
||||
.eq(0);
|
||||
.getByTestId('edge-label')
|
||||
.filter(`[data-source-node-name="${source}"][data-target-node-name="${target}"]`);
|
||||
}
|
||||
|
||||
export function getNodeCreatorSearchBar() {
|
||||
|
@ -78,10 +167,7 @@ export function getNodeCreatorPlusButton() {
|
|||
}
|
||||
|
||||
export function getCanvasNodes() {
|
||||
return cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('canvas-node'),
|
||||
() => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'),
|
||||
);
|
||||
return cy.getByTestId('canvas-node');
|
||||
}
|
||||
|
||||
export function getCanvasNodeByName(nodeName: string) {
|
||||
|
@ -141,7 +227,7 @@ function connectNodeToParent(
|
|||
parentNodeName: string,
|
||||
exactMatch = false,
|
||||
) {
|
||||
getAddInputEndpointByType(parentNodeName, endpointType).click({ force: true });
|
||||
getInputPlusHandleByType(parentNodeName, endpointType).click({ force: true });
|
||||
if (exactMatch) {
|
||||
getNodeCreatorItems()
|
||||
.contains(new RegExp('^' + nodeName + '$', 'g'))
|
||||
|
@ -158,7 +244,19 @@ export function addSupplementalNodeToParent(
|
|||
exactMatch = false,
|
||||
) {
|
||||
connectNodeToParent(nodeName, endpointType, parentNodeName, exactMatch);
|
||||
getConnectionBySourceAndTarget(parentNodeName, nodeName).should('exist');
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
getConnectionBySourceAndTarget(parentNodeName, nodeName).should('exist');
|
||||
},
|
||||
() => {
|
||||
if (endpointType === 'main') {
|
||||
getConnectionBySourceAndTarget(parentNodeName, nodeName).should('exist');
|
||||
} else {
|
||||
getConnectionBySourceAndTarget(nodeName, parentNodeName).should('exist');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function addLanguageModelNodeToParent(
|
||||
|
@ -196,8 +294,8 @@ export function addRetrieverNodeToParent(nodeName: string, parentNodeName: strin
|
|||
addSupplementalNodeToParent(nodeName, 'ai_retriever', parentNodeName);
|
||||
}
|
||||
|
||||
export function clickExecuteWorkflowButton() {
|
||||
getExecuteWorkflowButton().click();
|
||||
export function clickExecuteWorkflowButton(triggerNodeName?: string) {
|
||||
getExecuteWorkflowButton(triggerNodeName).click();
|
||||
}
|
||||
|
||||
export function clickManualChatButton() {
|
||||
|
@ -229,3 +327,34 @@ export function deleteNode(name: string) {
|
|||
getCanvasNodeByName(name).first().click();
|
||||
cy.get('body').type('{del}');
|
||||
}
|
||||
|
||||
export function openContextMenu(
|
||||
nodeName?: string,
|
||||
{ method = 'right-click', anchor = 'center' }: OpenContextMenuOptions = {},
|
||||
) {
|
||||
let target;
|
||||
if (nodeName) {
|
||||
target =
|
||||
method === 'right-click' ? getNodeRenderedTypeByName(nodeName) : getNodeByName(nodeName);
|
||||
} else {
|
||||
target = getCanvasPane();
|
||||
}
|
||||
|
||||
if (method === 'right-click') {
|
||||
target.rightclick(nodeName ? anchor : 'topLeft', { force: true });
|
||||
} else {
|
||||
target.realHover();
|
||||
target.find('[data-test-id="overflow-node-button"]').click({ force: true });
|
||||
}
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => {},
|
||||
() => {
|
||||
getContextMenu().should('be.visible');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function clickContextMenuAction(action: string) {
|
||||
getContextMenuAction(action).click();
|
||||
}
|
||||
|
|
15
cypress/composables/workflowsPage.ts
Normal file
15
cypress/composables/workflowsPage.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Getters
|
||||
*/
|
||||
|
||||
export function getWorkflowsPageUrl() {
|
||||
return '/home/workflows';
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
||||
export function visitWorkflowsPage() {
|
||||
cy.visit(getWorkflowsPageUrl());
|
||||
}
|
|
@ -1,16 +1,15 @@
|
|||
import { getCanvasNodes } from '../composables/workflow';
|
||||
import {
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
CODE_NODE_NAME,
|
||||
SET_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
} from '../constants';
|
||||
import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
|
||||
import { NDV } from '../pages/ndv';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
// Suite-specific constants
|
||||
const CODE_NODE_NEW_NAME = 'Something else';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
const messageBox = new MessageBoxClass();
|
||||
const ndv = new NDV();
|
||||
|
@ -20,40 +19,6 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.actions.visit();
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix redo connections
|
||||
it('should undo/redo adding node in the middle', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeBetweenNodes(
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
CODE_NODE_NAME,
|
||||
SET_NODE_NAME,
|
||||
);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
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', cssLeft + 'px')
|
||||
.should('have.css', 'top', cssTop + 'px');
|
||||
});
|
||||
});
|
||||
|
||||
it('should undo/redo deleting node using context menu', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
@ -115,34 +80,60 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix moving of nodes via e2e tests
|
||||
it('should undo/redo moving nodes', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => {
|
||||
const initialPosition = $node.position();
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
||||
|
||||
WorkflowPage.getters.canvasNodeByName(CODE_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.zoomToFit();
|
||||
|
||||
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);
|
||||
getCanvasNodes()
|
||||
.last()
|
||||
.then(($node) => {
|
||||
const { x: x1, y: y1 } = $node[0].getBoundingClientRect();
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
||||
clickToFinish: true,
|
||||
});
|
||||
},
|
||||
() => {
|
||||
cy.drag(getCanvasNodes().last(), [50, 150], {
|
||||
realMouse: true,
|
||||
abs: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
getCanvasNodes()
|
||||
.last()
|
||||
.then(($node) => {
|
||||
const { x: x2, y: y2 } = $node[0].getBoundingClientRect();
|
||||
expect(x2).to.be.greaterThan(x1);
|
||||
expect(y2).to.be.greaterThan(y1);
|
||||
});
|
||||
|
||||
WorkflowPage.actions.hitUndo();
|
||||
|
||||
getCanvasNodes()
|
||||
.last()
|
||||
.then(($node) => {
|
||||
const { x: x3, y: y3 } = $node[0].getBoundingClientRect();
|
||||
expect(x3).to.equal(x1);
|
||||
expect(y3).to.equal(y1);
|
||||
});
|
||||
|
||||
WorkflowPage.actions.hitRedo();
|
||||
|
||||
getCanvasNodes()
|
||||
.last()
|
||||
.then(($node) => {
|
||||
const { x: x4, y: y4 } = $node[0].getBoundingClientRect();
|
||||
expect(x4).to.be.greaterThan(x1);
|
||||
expect(y4).to.be.greaterThan(y1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should undo/redo deleting a connection using context menu', () => {
|
||||
|
@ -155,17 +146,6 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
// FIXME: Canvas V2: Fix disconnecting by moving
|
||||
it('should undo/redo deleting a connection by moving it away', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
cy.drag('.rect-input-endpoint.jtk-endpoint-connected', [0, -100]);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
it('should undo/redo disabling a node using context menu', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
|
@ -204,23 +184,6 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix undo renaming node
|
||||
it('should undo/redo renaming node using keyboard shortcut', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').trigger('keydown', { key: 'F2' });
|
||||
cy.get('.rename-prompt').should('be.visible');
|
||||
cy.get('body').type(CODE_NODE_NEW_NAME);
|
||||
cy.get('body').type('{enter}');
|
||||
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 duplicating a node', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
@ -243,77 +206,6 @@ describe('Undo/Redo', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Figure out why moving doesn't work from e2e
|
||||
it('should undo/redo multiple steps', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
// WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
|
||||
// Disable last node
|
||||
WorkflowPage.getters.canvasNodes().last().click();
|
||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
|
||||
// Move first one
|
||||
WorkflowPage.actions
|
||||
.getNodePosition(WorkflowPage.getters.canvasNodes().first())
|
||||
.then((initialPosition) => {
|
||||
WorkflowPage.getters.canvasNodes().first().click();
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
||||
clickToFinish: true,
|
||||
});
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.first()
|
||||
.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();
|
||||
cy.get('body').type('{backspace}');
|
||||
|
||||
// First undo: Should return deleted node
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
||||
// Second undo: Should move first node to it's original position
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.first()
|
||||
.should('have.css', 'left', `${initialPosition.left}px`)
|
||||
.should('have.css', 'top', `${initialPosition.top}px`);
|
||||
// Third undo: Should enable last node
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 0);
|
||||
|
||||
// First redo: Should disable last node
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
// Second redo: Should move the first node
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.first()
|
||||
.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);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to copy and paste pinned data nodes in workflows with dynamic Switch node', () => {
|
||||
cy.fixture('Test_workflow_form_switch.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
|
|
|
@ -129,7 +129,7 @@ describe('Inline expression editor', () => {
|
|||
|
||||
// Run workflow
|
||||
ndv.actions.close();
|
||||
WorkflowPage.actions.executeNode('No Operation', { anchor: 'topLeft' });
|
||||
WorkflowPage.actions.executeNode('No Operation, do nothing', { anchor: 'topLeft' });
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openInlineExpressionEditor();
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import {
|
|||
CODE_NODE_NAME,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
IF_NODE_NAME,
|
||||
HTTP_REQUEST_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { getCanvasPane } from '../composables/workflow';
|
||||
import { successToast } from '../pages/notifications';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
|
@ -16,64 +16,12 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.actions.visit();
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Missing execute button if no nodes
|
||||
it('should render canvas', () => {
|
||||
WorkflowPage.getters.nodeViewRoot().should('be.visible');
|
||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||
WorkflowPage.getters.zoomToFitButton().should('be.visible');
|
||||
WorkflowPage.getters.zoomInButton().should('be.visible');
|
||||
WorkflowPage.getters.zoomOutButton().should('be.visible');
|
||||
WorkflowPage.getters.executeWorkflowButton().should('be.visible');
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix changing of connection
|
||||
it('should connect and disconnect a simple node', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
|
||||
WorkflowPage.getters.nodeViewBackground().click(600, 400, { force: true });
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
|
||||
// Change connection from Set to Set1
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('input', EDIT_FIELDS_SET_NODE_NAME),
|
||||
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
);
|
||||
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, `${EDIT_FIELDS_SET_NODE_NAME}1`)
|
||||
.should('be.visible');
|
||||
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
// Disconnect Set1
|
||||
cy.drag(
|
||||
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
[-200, 100],
|
||||
);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
it('should add first step', () => {
|
||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should add a node via plus endpoint drag', () => {
|
||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, true);
|
||||
|
||||
cy.drag(
|
||||
WorkflowPage.getters.getEndpointSelector('plus', SCHEDULE_TRIGGER_NODE_NAME),
|
||||
[100, 100],
|
||||
);
|
||||
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
WorkflowPage.actions.addNodeToCanvas(IF_NODE_NAME, false);
|
||||
WorkflowPage.getters.nodeViewBackground().click({ force: true });
|
||||
});
|
||||
|
||||
it('should add a connected node using plus endpoint', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
|
@ -116,7 +64,7 @@ describe('Canvas Actions', () => {
|
|||
it('should add disconnected node if nothing is selected', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
// Deselect nodes
|
||||
WorkflowPage.getters.nodeView().click({ force: true });
|
||||
getCanvasPane().click({ force: true });
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
|
@ -166,15 +114,6 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix disconnecting of connection by dragging it
|
||||
it('should delete a connection by moving it away from endpoint', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
cy.drag(WorkflowPage.getters.getEndpointSelector('input', CODE_NODE_NAME), [0, -100]);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
describe('Node hover actions', () => {
|
||||
it('should execute node', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
|
@ -184,7 +123,11 @@ describe('Canvas Actions', () => {
|
|||
.last()
|
||||
.findChildByTestId('execute-node-button')
|
||||
.click({ force: true });
|
||||
|
||||
successToast().should('have.length', 1);
|
||||
|
||||
WorkflowPage.actions.executeNode(CODE_NODE_NAME);
|
||||
|
||||
successToast().should('have.length', 2);
|
||||
successToast().should('contain.text', 'Node executed successfully');
|
||||
});
|
||||
|
@ -235,7 +178,6 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters.selectedNodes().should('have.length', 0);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Selection via arrow keys is broken
|
||||
it('should select nodes using arrow keys', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
|
@ -259,7 +201,6 @@ describe('Canvas Actions', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Selection via shift and arrow keys is broken
|
||||
it('should select nodes using shift and arrow keys', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
|
@ -268,31 +209,4 @@ describe('Canvas Actions', () => {
|
|||
cy.get('body').type('{shift}', { release: false }).type('{leftArrow}');
|
||||
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix select & deselect
|
||||
it('should not break lasso selection when dragging node action buttons', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.last()
|
||||
.findChildByTestId('execute-node-button')
|
||||
.as('executeNodeButton');
|
||||
cy.drag('@executeNodeButton', [200, 200]);
|
||||
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix select & deselect
|
||||
it('should not break lasso selection with multiple clicks on node action buttons', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
|
||||
WorkflowPage.getters.canvasNodes().last().as('lastNode');
|
||||
cy.get('@lastNode').findChildByTestId('execute-node-button').as('executeNodeButton');
|
||||
for (let i = 0; i < 20; i++) {
|
||||
cy.get('@lastNode').realHover();
|
||||
cy.get('@executeNodeButton').should('be.visible');
|
||||
cy.get('@executeNodeButton').realTouch();
|
||||
cy.getByTestId('execute-workflow-button').realHover();
|
||||
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,9 +7,17 @@ import {
|
|||
SWITCH_NODE_NAME,
|
||||
MERGE_NODE_NAME,
|
||||
} from './../constants';
|
||||
import {
|
||||
clickContextMenuAction,
|
||||
getCanvasNodeByName,
|
||||
getCanvasNodes,
|
||||
getConnectionBySourceAndTarget,
|
||||
getConnectionLabelBySourceAndTarget,
|
||||
getOutputPlusHandle,
|
||||
openContextMenu,
|
||||
} from '../composables/workflow';
|
||||
import { NDV, WorkflowExecutionsTab } from '../pages';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { isCanvasV2 } from '../utils/workflowUtils';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
const ExecutionsTab = new WorkflowExecutionsTab();
|
||||
|
@ -20,8 +28,6 @@ const ZOOM_IN_X2_FACTOR = 1.5625; // Zoom in factor after two clicks
|
|||
const ZOOM_OUT_X1_FACTOR = 0.8;
|
||||
const ZOOM_OUT_X2_FACTOR = 0.64;
|
||||
|
||||
const PINCH_ZOOM_IN_FACTOR = 1.05702;
|
||||
const PINCH_ZOOM_OUT_FACTOR = 0.946058;
|
||||
const RENAME_NODE_NAME = 'Something else';
|
||||
const RENAME_NODE_NAME2 = 'Something different';
|
||||
|
||||
|
@ -41,27 +47,52 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
|
||||
NDVDialog.actions.close();
|
||||
for (let i = 0; i < desiredOutputs; i++) {
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName(SWITCH_NODE_NAME, i).click({ force: true });
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
WorkflowPage.getters
|
||||
.canvasNodePlusEndpointByName(SWITCH_NODE_NAME, i)
|
||||
.click({ force: true });
|
||||
},
|
||||
() => {
|
||||
getOutputPlusHandle(SWITCH_NODE_NAME).eq(0).click();
|
||||
},
|
||||
);
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
}
|
||||
WorkflowPage.getters.nodeViewBackground().click({ force: true });
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName(`${EDIT_FIELDS_SET_NODE_NAME}3`).click();
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName(`${EDIT_FIELDS_SET_NODE_NAME}3`).click();
|
||||
},
|
||||
() => {
|
||||
getOutputPlusHandle(`${EDIT_FIELDS_SET_NODE_NAME}3`).click();
|
||||
},
|
||||
);
|
||||
WorkflowPage.actions.addNodeToCanvas(SWITCH_NODE_NAME, false);
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
// Make sure outputless switch was connected correctly
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}3`, `${SWITCH_NODE_NAME}1`)
|
||||
.should('exist');
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}3`, `${SWITCH_NODE_NAME}1`)
|
||||
.should('exist');
|
||||
},
|
||||
() => {
|
||||
getConnectionBySourceAndTarget(
|
||||
`${EDIT_FIELDS_SET_NODE_NAME}3`,
|
||||
`${SWITCH_NODE_NAME}1`,
|
||||
).should('exist');
|
||||
},
|
||||
);
|
||||
// Make sure all connections are there after reload
|
||||
for (let i = 0; i < desiredOutputs; i++) {
|
||||
const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`;
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(`${SWITCH_NODE_NAME}`, setName)
|
||||
.should('exist');
|
||||
|
||||
getConnectionBySourceAndTarget(`${SWITCH_NODE_NAME}`, setName).should('exist');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -84,14 +115,29 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
);
|
||||
|
||||
// Connect Set1 and Set2 to merge
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME),
|
||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 0),
|
||||
);
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME),
|
||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 0),
|
||||
);
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
|
||||
);
|
||||
},
|
||||
() => {
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('output', EDIT_FIELDS_SET_NODE_NAME),
|
||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 0),
|
||||
);
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('output', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const checkConnections = () => {
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(
|
||||
|
@ -117,10 +163,22 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.actions.executeWorkflow();
|
||||
WorkflowPage.getters.stopExecutionButton().should('not.exist');
|
||||
|
||||
// Make sure all connections are there after save & reload
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
checkConnections();
|
||||
|
||||
WorkflowPage.actions.executeWorkflow();
|
||||
WorkflowPage.getters.stopExecutionButton().should('not.exist');
|
||||
|
||||
// If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('[data-label="2 items"]').should('be.visible'),
|
||||
() => cy.getByTestId('canvas-node-output-handle').contains('2 items').should('be.visible'),
|
||||
() =>
|
||||
getConnectionLabelBySourceAndTarget(`${EDIT_FIELDS_SET_NODE_NAME}1`, MERGE_NODE_NAME)
|
||||
.contains('2 items')
|
||||
.should('be.visible'),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -144,7 +202,10 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'),
|
||||
() => cy.getByTestId('canvas-handle-plus').should('have.attr', 'data-plus-type', 'success'),
|
||||
() =>
|
||||
cy
|
||||
.getByTestId('canvas-handle-plus-wrapper')
|
||||
.should('have.attr', 'data-plus-type', 'success'),
|
||||
);
|
||||
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
@ -212,8 +273,8 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
cy.wait(500);
|
||||
WorkflowPage.actions.selectAllFromContextMenu();
|
||||
WorkflowPage.actions.openContextMenu();
|
||||
WorkflowPage.actions.contextMenuAction('delete');
|
||||
openContextMenu();
|
||||
clickContextMenuAction('delete');
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 0);
|
||||
});
|
||||
|
||||
|
@ -228,41 +289,43 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
cy.wait(500);
|
||||
WorkflowPage.actions.selectAllFromContextMenu();
|
||||
WorkflowPage.actions.openContextMenu();
|
||||
WorkflowPage.actions.contextMenuAction('delete');
|
||||
openContextMenu();
|
||||
clickContextMenuAction('delete');
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 0);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Figure out how to test moving of the node
|
||||
it('should move node', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
|
||||
getCanvasNodes()
|
||||
.last()
|
||||
.then(($node) => {
|
||||
const { left, top } = $node.position();
|
||||
const { x: x1, y: y1 } = $node[0].getBoundingClientRect();
|
||||
|
||||
if (isCanvasV2()) {
|
||||
cy.drag('.vue-flow__node', [300, 300], {
|
||||
realMouse: true,
|
||||
});
|
||||
} else {
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
||||
clickToFinish: true,
|
||||
});
|
||||
}
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
||||
clickToFinish: true,
|
||||
});
|
||||
},
|
||||
() => {
|
||||
cy.drag(getCanvasNodes().last(), [50, 150], {
|
||||
realMouse: true,
|
||||
abs: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
getCanvasNodes()
|
||||
.last()
|
||||
.then(($node) => {
|
||||
const { left: newLeft, top: newTop } = $node.position();
|
||||
expect(newLeft).to.be.greaterThan(left);
|
||||
expect(newTop).to.be.greaterThan(top);
|
||||
const { x: x2, y: y2 } = $node[0].getBoundingClientRect();
|
||||
expect(x2).to.be.greaterThan(x1);
|
||||
expect(y2).to.be.greaterThan(y1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -304,26 +367,6 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
zoomAndCheck('zoomOut', ZOOM_OUT_X2_FACTOR);
|
||||
});
|
||||
|
||||
it('should zoom using scroll or pinch gesture', () => {
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
|
||||
|
||||
// V2 Canvas is using the same zoom factor for both pinch and scroll
|
||||
cy.ifCanvasVersion(
|
||||
() => checkZoomLevel(PINCH_ZOOM_IN_FACTOR),
|
||||
() => checkZoomLevel(ZOOM_IN_X1_FACTOR),
|
||||
);
|
||||
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||
checkZoomLevel(1); // Zoom in 1x + Zoom out 1x should reset to default (=1)
|
||||
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => checkZoomLevel(PINCH_ZOOM_OUT_FACTOR),
|
||||
() => checkZoomLevel(ZOOM_OUT_X1_FACTOR),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset zoom', () => {
|
||||
WorkflowPage.getters.resetZoomButton().should('not.exist');
|
||||
WorkflowPage.getters.zoomInButton().click();
|
||||
|
@ -369,7 +412,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 0);
|
||||
WorkflowPage.actions.deselectAll();
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
getCanvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
|
@ -378,19 +421,19 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
|
||||
// Context menu
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
WorkflowPage.actions.openContextMenu();
|
||||
openContextMenu();
|
||||
WorkflowPage.actions.contextMenuAction('toggle_activation');
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 0);
|
||||
WorkflowPage.actions.openContextMenu();
|
||||
openContextMenu();
|
||||
WorkflowPage.actions.contextMenuAction('toggle_activation');
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
||||
WorkflowPage.actions.deselectAll();
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.openContextMenu();
|
||||
openContextMenu();
|
||||
WorkflowPage.actions.contextMenuAction('toggle_activation');
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
WorkflowPage.actions.hitSelectAll();
|
||||
WorkflowPage.actions.openContextMenu();
|
||||
openContextMenu();
|
||||
WorkflowPage.actions.contextMenuAction('toggle_activation');
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
||||
});
|
||||
|
@ -466,7 +509,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
});
|
||||
// FIXME: Canvas V2: Credentials should show issue on the first open
|
||||
|
||||
it('should remove unknown credentials on pasting workflow', () => {
|
||||
cy.fixture('workflow-with-unknown-credentials.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
|
@ -478,35 +521,4 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
NDVDialog.actions.close();
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Unknown nodes should still render connection endpoints
|
||||
it('should render connections correctly if unkown nodes are present', () => {
|
||||
const unknownNodeName = 'Unknown node';
|
||||
cy.createFixtureWorkflow('workflow-with-unknown-nodes.json', 'Unknown nodes');
|
||||
|
||||
WorkflowPage.getters.canvasNodeByName(`${unknownNodeName} 1`).should('exist');
|
||||
WorkflowPage.getters.canvasNodeByName(`${unknownNodeName} 2`).should('exist');
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('plus', `${unknownNodeName} 1`),
|
||||
WorkflowPage.getters.getEndpointSelector('input', EDIT_FIELDS_SET_NODE_NAME),
|
||||
);
|
||||
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('plus', `${unknownNodeName} 2`),
|
||||
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
);
|
||||
|
||||
WorkflowPage.actions.executeWorkflow();
|
||||
cy.contains('Unrecognized node type').should('be.visible');
|
||||
|
||||
WorkflowPage.actions.deselectAll();
|
||||
WorkflowPage.actions.deleteNodeFromContextMenu(`${unknownNodeName} 1`);
|
||||
WorkflowPage.actions.deleteNodeFromContextMenu(`${unknownNodeName} 2`);
|
||||
|
||||
WorkflowPage.actions.executeWorkflow();
|
||||
|
||||
cy.contains('Unrecognized node type').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { simpleWebhookCall, waitForWebhook } from './16-webhook-node.cy';
|
||||
import {
|
||||
HTTP_REQUEST_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
|
@ -109,36 +106,6 @@ describe('Data pinning', () => {
|
|||
ndv.getters.outputTbodyCell(1, 0).should('include.text', 1);
|
||||
});
|
||||
|
||||
it('Should be able to pin data from canvas (context menu or shortcut)', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
|
||||
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
workflowPage.actions.openContextMenu(EDIT_FIELDS_SET_NODE_NAME, { method: 'overflow-button' });
|
||||
workflowPage.getters
|
||||
.contextMenuAction('toggle_pin')
|
||||
.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 }]);
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.pinNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||
ndv.getters.nodeOutputHint().should('exist');
|
||||
ndv.actions.close();
|
||||
|
||||
// Unpin using shortcut
|
||||
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||
ndv.actions.setPinnedData([{ test: 1 }]);
|
||||
ndv.actions.close();
|
||||
workflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click();
|
||||
workflowPage.actions.hitPinNodeShortcut();
|
||||
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||
ndv.getters.nodeOutputHint().should('exist');
|
||||
});
|
||||
|
||||
it('Should show an error when maximum pin data size is exceeded', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
|
||||
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
|
||||
|
@ -217,32 +184,6 @@ describe('Data pinning', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should show pinned data tooltip', () => {
|
||||
const { callEndpoint } = simpleWebhookCall({
|
||||
method: 'GET',
|
||||
webhookPath: nanoid(),
|
||||
executeNow: false,
|
||||
});
|
||||
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.executeWorkflow();
|
||||
cy.wait(waitForWebhook);
|
||||
|
||||
// hide other visible popper on workflow execute button
|
||||
workflowPage.getters.canvasNodes().eq(0).click();
|
||||
|
||||
callEndpoint((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
getVisiblePopper().should('have.length', 1);
|
||||
getVisiblePopper()
|
||||
.eq(0)
|
||||
.should(
|
||||
'have.text',
|
||||
'You can pin this output instead of waiting for a test event. Open node to do so.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show pinned data tooltip', () => {
|
||||
cy.createFixtureWorkflow('Pinned_webhook_node.json', 'Test');
|
||||
workflowPage.actions.executeWorkflow();
|
||||
|
|
|
@ -62,12 +62,14 @@ describe('n8n Form Trigger', () => {
|
|||
getVisibleSelect().contains('Dropdown').click();
|
||||
cy.contains('button', 'Add Field Option').click();
|
||||
cy.contains('label', 'Field Options')
|
||||
.parent()
|
||||
.parent()
|
||||
.nextAll()
|
||||
.find('[data-test-id="parameter-input-field"]')
|
||||
.eq(0)
|
||||
.type('Option 1');
|
||||
cy.contains('label', 'Field Options')
|
||||
.parent()
|
||||
.parent()
|
||||
.nextAll()
|
||||
.find('[data-test-id="parameter-input-field"]')
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { simpleWebhookCall, waitForWebhook } from '../composables/webhooks';
|
||||
import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
|
||||
import { cowBase64 } from '../support/binaryTestFiles';
|
||||
|
@ -9,81 +10,6 @@ const workflowPage = new WorkflowPage();
|
|||
const ndv = new NDV();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
|
||||
export const waitForWebhook = 500;
|
||||
|
||||
interface SimpleWebhookCallOptions {
|
||||
method: string;
|
||||
webhookPath: string;
|
||||
responseCode?: number;
|
||||
respondWith?: string;
|
||||
executeNow?: boolean;
|
||||
responseData?: string;
|
||||
authentication?: string;
|
||||
}
|
||||
|
||||
export const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||
const {
|
||||
authentication,
|
||||
method,
|
||||
webhookPath,
|
||||
responseCode,
|
||||
respondWith,
|
||||
responseData,
|
||||
executeNow = true,
|
||||
} = options;
|
||||
|
||||
workflowPage.actions.addInitialNodeToCanvas('Webhook');
|
||||
workflowPage.actions.openNode('Webhook');
|
||||
|
||||
cy.getByTestId('parameter-input-httpMethod').click();
|
||||
getVisibleSelect().find('.option-headline').contains(method).click();
|
||||
cy.getByTestId('parameter-input-path')
|
||||
.find('.parameter-input')
|
||||
.find('input')
|
||||
.clear()
|
||||
.type(webhookPath);
|
||||
|
||||
if (authentication) {
|
||||
cy.getByTestId('parameter-input-authentication').click();
|
||||
getVisibleSelect().find('.option-headline').contains(authentication).click();
|
||||
}
|
||||
|
||||
if (responseCode) {
|
||||
cy.get('.param-options').click();
|
||||
getVisibleSelect().contains('Response Code').click();
|
||||
cy.get('.parameter-item-wrapper > .parameter-input-list-wrapper').children().click();
|
||||
getVisibleSelect().contains('201').click();
|
||||
}
|
||||
|
||||
if (respondWith) {
|
||||
cy.getByTestId('parameter-input-responseMode').click();
|
||||
getVisibleSelect().find('.option-headline').contains(respondWith).click();
|
||||
}
|
||||
|
||||
if (responseData) {
|
||||
cy.getByTestId('parameter-input-responseData').click();
|
||||
getVisibleSelect().find('.option-headline').contains(responseData).click();
|
||||
}
|
||||
|
||||
const callEndpoint = (cb: (response: Cypress.Response<unknown>) => void) => {
|
||||
cy.request(method, `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(cb);
|
||||
};
|
||||
|
||||
if (executeNow) {
|
||||
ndv.actions.execute();
|
||||
cy.wait(waitForWebhook);
|
||||
|
||||
callEndpoint((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
ndv.getters.outputPanel().contains('headers');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
callEndpoint,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Webhook Trigger node', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
|
@ -250,7 +176,7 @@ describe('Webhook Trigger node', () => {
|
|||
});
|
||||
// add credentials
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
|
||||
|
@ -293,7 +219,7 @@ describe('Webhook Trigger node', () => {
|
|||
});
|
||||
// add credentials
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
|
||||
|
|
|
@ -297,10 +297,9 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
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
|
||||
// Only the credential in this project should be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 2);
|
||||
getVisibleSelect().find('li').should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should only show credentials in their personal project for members', () => {
|
||||
|
@ -325,10 +324,9 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the own credential the shared one (+ the 'Create new' option)
|
||||
// should be in the dropdown
|
||||
// Only the own credential the shared one should be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 3);
|
||||
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', () => {
|
||||
|
@ -355,10 +353,9 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
workflowsPage.getters.workflowCardContent(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
|
||||
// Only the own credential the shared one should be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 2);
|
||||
getVisibleSelect().find('li').should('have.length', 1);
|
||||
});
|
||||
|
||||
it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
|
||||
|
@ -400,10 +397,9 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
workflowsPage.getters.workflowCardContent(workflowName).click();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the personal credentials of the workflow owner and the global owner
|
||||
// should show up.
|
||||
// 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);
|
||||
getVisibleSelect().find('li').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should show all personal credentials if the global owner owns the workflow', () => {
|
||||
|
@ -421,6 +417,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
|
||||
// Show all personal credentials
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.have.length', 2);
|
||||
getVisibleSelect().find('li').should('have.have.length', 1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
||||
describe('PAY-1858 context menu', () => {
|
||||
it('can use context menu on saved workflow', () => {
|
||||
WorkflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test_workflow_filter.json', 'test');
|
||||
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 5);
|
||||
WorkflowPage.actions.deleteNodeFromContextMenu('Then');
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||
|
||||
WorkflowPage.actions.hitSaveWorkflow();
|
||||
|
||||
cy.reload();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||
WorkflowPage.actions.deleteNodeFromContextMenu('Code');
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
});
|
||||
});
|
|
@ -1,3 +1,11 @@
|
|||
import { clickGetBackToCanvas, getNdvContainer, getOutputTableRow } from '../composables/ndv';
|
||||
import {
|
||||
clickExecuteWorkflowButton,
|
||||
getExecuteWorkflowButton,
|
||||
getNodeByName,
|
||||
getZoomToFitButton,
|
||||
openNode,
|
||||
} from '../composables/workflow';
|
||||
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
import { clearNotifications, errorToast, successToast } from '../pages/notifications';
|
||||
|
@ -214,89 +222,37 @@ describe('Execution', () => {
|
|||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Webhook should show waiting state but it doesn't
|
||||
it('should test webhook workflow stop', () => {
|
||||
cy.createFixtureWorkflow('Webhook_wait_set.json');
|
||||
it('should test workflow with specific trigger node', () => {
|
||||
cy.createFixtureWorkflow('Two_schedule_triggers.json');
|
||||
|
||||
// Check workflow buttons
|
||||
workflowPage.getters.executeWorkflowButton().should('be.visible');
|
||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||
workflowPage.getters.stopExecutionButton().should('not.exist');
|
||||
workflowPage.getters.stopExecutionWaitingForWebhookButton().should('not.exist');
|
||||
getZoomToFitButton().click();
|
||||
getExecuteWorkflowButton('Trigger A').should('not.be.visible');
|
||||
getExecuteWorkflowButton('Trigger B').should('not.be.visible');
|
||||
|
||||
// Execute the workflow
|
||||
workflowPage.getters.zoomToFitButton().click();
|
||||
workflowPage.getters.executeWorkflowButton().click();
|
||||
// Execute the workflow from trigger A
|
||||
getNodeByName('Trigger A').realHover();
|
||||
getExecuteWorkflowButton('Trigger A').should('be.visible');
|
||||
getExecuteWorkflowButton('Trigger B').should('not.be.visible');
|
||||
clickExecuteWorkflowButton('Trigger A');
|
||||
|
||||
// Check workflow buttons
|
||||
workflowPage.getters.executeWorkflowButton().get('.n8n-spinner').should('be.visible');
|
||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||
workflowPage.getters.stopExecutionButton().should('not.exist');
|
||||
workflowPage.getters.stopExecutionWaitingForWebhookButton().should('be.visible');
|
||||
// Check the output
|
||||
successToast().contains('Workflow executed successfully');
|
||||
openNode('Edit Fields');
|
||||
getOutputTableRow(1).should('include.text', 'Trigger A');
|
||||
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
clickGetBackToCanvas();
|
||||
getNdvContainer().should('not.be.visible');
|
||||
|
||||
ndv.getters.copyInput().click();
|
||||
// Execute the workflow from trigger B
|
||||
getNodeByName('Trigger B').realHover();
|
||||
getExecuteWorkflowButton('Trigger A').should('not.be.visible');
|
||||
getExecuteWorkflowButton('Trigger B').should('be.visible');
|
||||
clickExecuteWorkflowButton('Trigger B');
|
||||
|
||||
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
|
||||
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
||||
cy.readClipboard().then((url) => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url,
|
||||
}).then((resp) => {
|
||||
expect(resp.status).to.eq(200);
|
||||
});
|
||||
});
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
||||
workflowPage.getters.stopExecutionButton().click();
|
||||
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Webhook')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||
|
||||
// Check canvas nodes after workflow stopped
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Webhook')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
|
||||
if (isCanvasV2()) {
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
|
||||
} else {
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
|
||||
}
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||
|
||||
successToast().should('be.visible');
|
||||
|
||||
// Clear execution data
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
workflowPage.getters.clearExecutionDataButton().click();
|
||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||
// Check the output
|
||||
successToast().contains('Workflow executed successfully');
|
||||
openNode('Edit Fields');
|
||||
getOutputTableRow(1).should('include.text', 'Trigger B');
|
||||
});
|
||||
|
||||
describe('execution preview', () => {
|
||||
|
@ -312,8 +268,11 @@ describe('Execution', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Missing pinned states for `edge-label-wrapper`
|
||||
describe('connections should be colored differently for pinned data', () => {
|
||||
/**
|
||||
* @TODO New Canvas: Different classes for pinned states on edges and nodes
|
||||
*/
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
describe.skip('connections should be colored differently for pinned data', () => {
|
||||
beforeEach(() => {
|
||||
cy.createFixtureWorkflow('Schedule_pinned.json');
|
||||
workflowPage.actions.deselectAll();
|
||||
|
@ -634,45 +593,4 @@ describe('Execution', () => {
|
|||
|
||||
errorToast().should('contain', 'Problem in node ‘Telegram‘');
|
||||
});
|
||||
|
||||
it('should not show pinned data in production execution', () => {
|
||||
cy.createFixtureWorkflow('Execution-pinned-data-check.json');
|
||||
|
||||
workflowPage.getters.zoomToFitButton().click();
|
||||
cy.intercept('PATCH', '/rest/workflows/*').as('workflowActivate');
|
||||
workflowPage.getters.activatorSwitch().click();
|
||||
|
||||
cy.wait('@workflowActivate');
|
||||
cy.get('body').type('{esc}');
|
||||
workflowPage.actions.openNode('Webhook');
|
||||
|
||||
cy.contains('label', 'Production URL').should('be.visible').click();
|
||||
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
|
||||
cy.get('.webhook-url').click();
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
||||
cy.readClipboard().then((url) => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url,
|
||||
}).then((resp) => {
|
||||
expect(resp.status).to.eq(200);
|
||||
});
|
||||
});
|
||||
|
||||
cy.intercept('GET', '/rest/executions/*').as('getExecution');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
|
||||
cy.wait('@getExecution');
|
||||
executionsTab.getters
|
||||
.workflowExecutionPreviewIframe()
|
||||
.should('be.visible')
|
||||
.its('0.contentDocument.body')
|
||||
.should('not.be.empty')
|
||||
|
||||
.then(cy.wrap)
|
||||
.find('.connection-run-items-label')
|
||||
.filter(':contains("5 items")')
|
||||
.should('have.length', 2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,7 +31,7 @@ function createNotionCredential() {
|
|||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
|
||||
workflowPage.actions.openNode(NOTION_NODE_NAME);
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
cy.get('body').type('{esc}');
|
||||
workflowPage.actions.deleteNode(NOTION_NODE_NAME);
|
||||
|
@ -79,7 +79,7 @@ describe('Credentials', () => {
|
|||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
|
@ -99,7 +99,7 @@ describe('Credentials', () => {
|
|||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
// Add oAuth credentials
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
|
@ -107,14 +107,13 @@ describe('Credentials', () => {
|
|||
cy.get('.el-message-box').find('button').contains('Close').click();
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
// Add Service account credentials
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().last().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
// Both (+ the 'Create new' option) should be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length.greaterThan', 2);
|
||||
getVisibleSelect().find('li').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should correctly render required and optional credentials', () => {
|
||||
|
@ -130,13 +129,13 @@ describe('Credentials', () => {
|
|||
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
|
||||
|
||||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
getVisibleSelect().find('li').contains('Create New Credential').click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().first().click();
|
||||
// This one should show auth type selector
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
workflowPage.getters.nodeCredentialsSelect().last().click();
|
||||
getVisibleSelect().find('li').contains('Create New Credential').click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().last().click();
|
||||
// This one should not show auth type selector
|
||||
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
|
||||
});
|
||||
|
@ -148,7 +147,7 @@ describe('Credentials', () => {
|
|||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters
|
||||
|
@ -164,7 +163,7 @@ describe('Credentials', () => {
|
|||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
|
@ -189,7 +188,7 @@ describe('Credentials', () => {
|
|||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
|
@ -232,7 +231,7 @@ describe('Credentials', () => {
|
|||
cy.getByTestId('credential-select').click();
|
||||
cy.contains('Adalo API').click();
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
|
@ -296,7 +295,7 @@ describe('Credentials', () => {
|
|||
|
||||
workflowPage.getters.nodeCredentialsSelect().should('exist');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
|
@ -325,7 +324,7 @@ describe('Credentials', () => {
|
|||
workflowPage.actions.addNodeToCanvas('Slack', true, true, 'Get a channel');
|
||||
workflowPage.getters.nodeCredentialsSelect().should('exist');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
nodeDetailsView.getters.copyInput().should('not.exist');
|
||||
});
|
||||
|
|
|
@ -89,7 +89,7 @@ describe('Community and custom nodes in canvas', () => {
|
|||
workflowPage.actions.addNodeToCanvas('Manual');
|
||||
workflowPage.actions.addNodeToCanvas('E2E Node with native n8n credential', true, true);
|
||||
workflowPage.getters.nodeCredentialsLabel().click();
|
||||
cy.contains('Create New Credential').click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Notion API');
|
||||
});
|
||||
|
@ -98,7 +98,7 @@ describe('Community and custom nodes in canvas', () => {
|
|||
workflowPage.actions.addNodeToCanvas('Manual');
|
||||
workflowPage.actions.addNodeToCanvas('E2E Node with custom credential', true, true);
|
||||
workflowPage.getters.nodeCredentialsLabel().click();
|
||||
cy.contains('Create New Credential').click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
|
||||
});
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
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');
|
||||
});
|
||||
});
|
|
@ -1,135 +0,0 @@
|
|||
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]',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -65,8 +65,11 @@ describe('Variables', () => {
|
|||
const editingRow = variablesPage.getters.variablesEditableRows().eq(0);
|
||||
variablesPage.actions.setRowValue(editingRow, 'key', key);
|
||||
variablesPage.actions.setRowValue(editingRow, 'value', value);
|
||||
editingRow.should('contain', 'This field may contain only letters');
|
||||
variablesPage.getters.editableRowSaveButton(editingRow).should('be.disabled');
|
||||
variablesPage.actions.saveRowEditing(editingRow);
|
||||
variablesPage.getters
|
||||
.variablesEditableRows()
|
||||
.eq(0)
|
||||
.should('contain', 'This field may contain only letters');
|
||||
variablesPage.actions.cancelRowEditing(editingRow);
|
||||
|
||||
variablesPage.getters.variablesRows().should('have.length', 3);
|
||||
|
|
|
@ -118,6 +118,15 @@ describe('NDV', () => {
|
|||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.switchOutputMode('Table');
|
||||
|
||||
// Start from linked state
|
||||
ndv.getters.outputLinkRun().then(($el) => {
|
||||
const classList = Array.from($el[0].classList);
|
||||
if (!classList.includes('linked')) {
|
||||
ndv.actions.toggleOutputRunLinking();
|
||||
ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip
|
||||
}
|
||||
});
|
||||
|
||||
ndv.getters
|
||||
.inputRunSelector()
|
||||
.should('exist')
|
||||
|
@ -243,38 +252,38 @@ describe('NDV', () => {
|
|||
// biome-ignore format:
|
||||
const PINNED_DATA = [
|
||||
{
|
||||
"id": "abc",
|
||||
"historyId": "def",
|
||||
"messages": [
|
||||
id: 'abc',
|
||||
historyId: 'def',
|
||||
messages: [
|
||||
{
|
||||
"id": "abc"
|
||||
}
|
||||
]
|
||||
id: 'abc',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "abc",
|
||||
"historyId": "def",
|
||||
"messages": [
|
||||
id: 'abc',
|
||||
historyId: 'def',
|
||||
messages: [
|
||||
{
|
||||
"id": "abc"
|
||||
id: 'abc',
|
||||
},
|
||||
{
|
||||
"id": "abc"
|
||||
id: 'abc',
|
||||
},
|
||||
{
|
||||
"id": "abc"
|
||||
}
|
||||
]
|
||||
id: 'abc',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "abc",
|
||||
"historyId": "def",
|
||||
"messages": [
|
||||
id: 'abc',
|
||||
historyId: 'def',
|
||||
messages: [
|
||||
{
|
||||
"id": "abc"
|
||||
}
|
||||
]
|
||||
}
|
||||
id: 'abc',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
workflowPage.actions.openNode('Get thread details1');
|
||||
ndv.actions.pastePinnedData(PINNED_DATA);
|
||||
|
|
|
@ -3,24 +3,6 @@ import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
|||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
|
||||
function checkStickiesStyle(
|
||||
top: number,
|
||||
left: number,
|
||||
height: number,
|
||||
width: number,
|
||||
zIndex?: number,
|
||||
) {
|
||||
workflowPage.getters.stickies().should(($el) => {
|
||||
expect($el).to.have.css('top', `${top}px`);
|
||||
expect($el).to.have.css('left', `${left}px`);
|
||||
expect($el).to.have.css('height', `${height}px`);
|
||||
expect($el).to.have.css('width', `${width}px`);
|
||||
if (zIndex) {
|
||||
expect($el).to.have.css('z-index', `${zIndex}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('Canvas Actions', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
|
@ -51,191 +33,8 @@ describe('Canvas Actions', () => {
|
|||
.contains('Guide')
|
||||
.should('have.attr', 'href');
|
||||
});
|
||||
|
||||
it('drags sticky around to top left corner', () => {
|
||||
// used to caliberate move sticky function
|
||||
addDefaultSticky();
|
||||
moveSticky({ top: 0, left: 0 });
|
||||
});
|
||||
|
||||
it('drags sticky around and position/size are saved correctly', () => {
|
||||
addDefaultSticky();
|
||||
moveSticky({ top: 500, left: 500 });
|
||||
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.wait('@createWorkflow');
|
||||
|
||||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
|
||||
stickyShouldBePositionedCorrectly({ top: 500, left: 500 });
|
||||
});
|
||||
|
||||
it('deletes sticky', () => {
|
||||
workflowPage.actions.addSticky();
|
||||
workflowPage.getters.stickies().should('have.length', 1);
|
||||
|
||||
workflowPage.actions.deleteSticky();
|
||||
|
||||
workflowPage.getters.stickies().should('have.length', 0);
|
||||
});
|
||||
|
||||
it('edits sticky and updates content as markdown', () => {
|
||||
workflowPage.actions.addSticky();
|
||||
|
||||
workflowPage.getters
|
||||
.stickies()
|
||||
.should('have.text', 'I’m a note\nDouble click to edit me. Guide\n');
|
||||
|
||||
workflowPage.getters.stickies().dblclick();
|
||||
workflowPage.actions.editSticky('# hello world \n ## text text');
|
||||
workflowPage.getters.stickies().find('h1').should('have.text', 'hello world');
|
||||
workflowPage.getters.stickies().find('h2').should('have.text', 'text text');
|
||||
});
|
||||
|
||||
it('expands/shrinks sticky from the right edge', () => {
|
||||
addDefaultSticky();
|
||||
|
||||
moveSticky({ top: 200, left: 200 });
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="right"]', [100, 100]);
|
||||
checkStickiesStyle(100, 20, 160, 346);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="right"]', [-50, -50]);
|
||||
checkStickiesStyle(100, 20, 160, 302);
|
||||
});
|
||||
|
||||
it('expands/shrinks sticky from the left edge', () => {
|
||||
addDefaultSticky();
|
||||
|
||||
moveSticky({ left: 600, top: 200 });
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="left"]', [100, 100]);
|
||||
checkStickiesStyle(100, 510, 160, 150);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="left"]', [-50, -50]);
|
||||
checkStickiesStyle(100, 466, 160, 194);
|
||||
});
|
||||
|
||||
it('expands/shrinks sticky from the top edge', () => {
|
||||
workflowPage.actions.addSticky();
|
||||
cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button
|
||||
checkStickiesStyle(300, 620, 160, 240);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="top"]', [100, 100]);
|
||||
checkStickiesStyle(380, 620, 80, 240);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="top"]', [-50, -50]);
|
||||
checkStickiesStyle(324, 620, 136, 240);
|
||||
});
|
||||
|
||||
it('expands/shrinks sticky from the bottom edge', () => {
|
||||
workflowPage.actions.addSticky();
|
||||
cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button
|
||||
checkStickiesStyle(300, 620, 160, 240);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [100, 100]);
|
||||
checkStickiesStyle(300, 620, 254, 240);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [-50, -50]);
|
||||
checkStickiesStyle(300, 620, 198, 240);
|
||||
});
|
||||
|
||||
it('expands/shrinks sticky from the bottom right edge', () => {
|
||||
workflowPage.actions.addSticky();
|
||||
cy.drag('[data-test-id="sticky"]', [-100, -100]); // move away from canvas button
|
||||
checkStickiesStyle(100, 420, 160, 240);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [100, 100]);
|
||||
checkStickiesStyle(100, 420, 254, 346);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [-50, -50]);
|
||||
checkStickiesStyle(100, 420, 198, 302);
|
||||
});
|
||||
|
||||
it('expands/shrinks sticky from the top right edge', () => {
|
||||
addDefaultSticky();
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [100, 100]);
|
||||
checkStickiesStyle(360, 400, 80, 346);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [-50, -50]);
|
||||
checkStickiesStyle(304, 400, 136, 302);
|
||||
});
|
||||
|
||||
it('expands/shrinks sticky from the top left edge, and reach min height/width', () => {
|
||||
addDefaultSticky();
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [100, 100]);
|
||||
checkStickiesStyle(360, 490, 80, 150);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]);
|
||||
checkStickiesStyle(204, 346, 236, 294);
|
||||
});
|
||||
|
||||
it('sets sticky behind node', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
|
||||
addDefaultSticky();
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]);
|
||||
checkStickiesStyle(124, 256, 316, 384, -121);
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodes()
|
||||
.eq(0)
|
||||
.should(($el) => {
|
||||
expect($el).to.have.css('z-index', 'auto');
|
||||
});
|
||||
|
||||
workflowPage.actions.addSticky();
|
||||
workflowPage.getters
|
||||
.stickies()
|
||||
.eq(0)
|
||||
.should(($el) => {
|
||||
expect($el).to.have.css('z-index', '-121');
|
||||
});
|
||||
workflowPage.getters
|
||||
.stickies()
|
||||
.eq(1)
|
||||
.should(($el) => {
|
||||
expect($el).to.have.css('z-index', '-38');
|
||||
});
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-200, -200], { index: 1 });
|
||||
workflowPage.getters
|
||||
.stickies()
|
||||
.eq(0)
|
||||
.should(($el) => {
|
||||
expect($el).to.have.css('z-index', '-121');
|
||||
});
|
||||
|
||||
workflowPage.getters
|
||||
.stickies()
|
||||
.eq(1)
|
||||
.should(($el) => {
|
||||
expect($el).to.have.css('z-index', '-158');
|
||||
});
|
||||
});
|
||||
|
||||
it('Empty sticky should not error when activating workflow', () => {
|
||||
workflowPage.actions.addSticky();
|
||||
|
||||
workflowPage.getters.stickies().should('have.length', 1);
|
||||
|
||||
workflowPage.getters.stickies().dblclick();
|
||||
|
||||
workflowPage.actions.clearSticky();
|
||||
|
||||
workflowPage.actions.addNodeToCanvas('Schedule Trigger');
|
||||
|
||||
workflowPage.actions.activateWorkflow();
|
||||
});
|
||||
});
|
||||
|
||||
type Position = {
|
||||
top: number;
|
||||
left: number;
|
||||
};
|
||||
|
||||
function shouldHaveOneSticky() {
|
||||
workflowPage.getters.stickies().should('have.length', 1);
|
||||
}
|
||||
|
@ -263,17 +62,3 @@ function addDefaultSticky() {
|
|||
shouldHaveDefaultSize();
|
||||
shouldBeInDefaultLocation();
|
||||
}
|
||||
|
||||
function stickyShouldBePositionedCorrectly(position: Position) {
|
||||
const yOffset = -100;
|
||||
const xOffset = -180;
|
||||
workflowPage.getters.stickies().should(($el) => {
|
||||
expect($el).to.have.css('top', `${yOffset + position.top}px`);
|
||||
expect($el).to.have.css('left', `${xOffset + position.left}px`);
|
||||
});
|
||||
}
|
||||
|
||||
function moveSticky(target: Position) {
|
||||
cy.drag('[data-test-id="sticky"]', [target.left, target.top], { abs: true });
|
||||
stickyShouldBePositionedCorrectly(target);
|
||||
}
|
||||
|
|
|
@ -1,86 +1,7 @@
|
|||
import { getWorkflowHistoryCloseButton } from '../composables/workflow';
|
||||
import {
|
||||
CODE_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
IF_NODE_NAME,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
const executionsTab = new WorkflowExecutionsTab();
|
||||
|
||||
const createNewWorkflowAndActivate = () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
workflowPage.actions.activateWorkflow();
|
||||
cy.get('.el-notification .el-notification--error').should('not.exist');
|
||||
};
|
||||
|
||||
const editWorkflowAndDeactivate = () => {
|
||||
workflowPage.getters.canvasNodePlusEndpointByName(SCHEDULE_TRIGGER_NODE_NAME).click();
|
||||
workflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
|
||||
cy.get('.jtk-connector').should('have.length', 1);
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
workflowPage.getters.activatorSwitch().click();
|
||||
workflowPage.actions.zoomToFit();
|
||||
cy.get('.el-notification .el-notification--error').should('not.exist');
|
||||
};
|
||||
|
||||
const editWorkflowMoreAndActivate = () => {
|
||||
cy.drag(workflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME), [200, 200], {
|
||||
realMouse: true,
|
||||
});
|
||||
workflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
|
||||
workflowPage.actions.addNodeToCanvas(CODE_NODE_NAME, false);
|
||||
workflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
|
||||
cy.get('.jtk-connector').should('have.length', 2);
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
|
||||
workflowPage.actions.addNodeToCanvas(IF_NODE_NAME);
|
||||
workflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
|
||||
cy.get('.jtk-connector').should('have.length', 2);
|
||||
|
||||
const position = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
};
|
||||
workflowPage.getters
|
||||
.canvasNodeByName(IF_NODE_NAME)
|
||||
.click()
|
||||
.then(($element) => {
|
||||
position.top = $element.position().top;
|
||||
position.left = $element.position().left;
|
||||
});
|
||||
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 200], { clickToFinish: true });
|
||||
workflowPage.getters
|
||||
.canvasNodes()
|
||||
.last()
|
||||
.then(($element) => {
|
||||
const finalPosition = {
|
||||
top: $element.position().top,
|
||||
left: $element.position().left,
|
||||
};
|
||||
|
||||
expect(finalPosition.top).to.be.greaterThan(position.top);
|
||||
expect(finalPosition.left).to.be.greaterThan(position.left);
|
||||
});
|
||||
|
||||
cy.draganddrop(
|
||||
workflowPage.getters.getEndpointSelector('output', CODE_NODE_NAME),
|
||||
workflowPage.getters.getEndpointSelector('input', IF_NODE_NAME),
|
||||
);
|
||||
cy.get('.jtk-connector').should('have.length', 3);
|
||||
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
workflowPage.getters.activatorSwitch().click();
|
||||
cy.get('.el-notification .el-notification--error').should('not.exist');
|
||||
};
|
||||
|
||||
const switchBetweenEditorAndHistory = () => {
|
||||
workflowPage.getters.workflowHistoryButton().click();
|
||||
|
@ -116,62 +37,6 @@ const zoomInAndCheckNodes = () => {
|
|||
workflowPage.getters.canvasNodes().last().should('not.be.visible');
|
||||
};
|
||||
|
||||
describe('Editor actions should work', () => {
|
||||
beforeEach(() => {
|
||||
cy.enableFeature('debugInEditor');
|
||||
cy.enableFeature('workflowHistory');
|
||||
cy.signinAsOwner();
|
||||
createNewWorkflowAndActivate();
|
||||
});
|
||||
|
||||
it('after switching between Editor and Executions', () => {
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(['@getExecutions']);
|
||||
cy.wait(500);
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
editWorkflowAndDeactivate();
|
||||
editWorkflowMoreAndActivate();
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
editWorkflowAndDeactivate();
|
||||
workflowPage.actions.executeWorkflow();
|
||||
cy.wait(['@postWorkflowRun']);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(['@getExecutions']);
|
||||
|
||||
executionsTab.getters.executionListItems().should('have.length', 1).first().click();
|
||||
cy.wait(['@getExecution']);
|
||||
|
||||
executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click();
|
||||
editWorkflowMoreAndActivate();
|
||||
});
|
||||
|
||||
it('after switching between Editor and Workflow history', () => {
|
||||
cy.intercept('GET', '/rest/workflow-history/workflow/*/version/*').as('getVersion');
|
||||
cy.intercept('GET', '/rest/workflow-history/workflow/*').as('getHistory');
|
||||
|
||||
editWorkflowAndDeactivate();
|
||||
workflowPage.getters.workflowHistoryButton().click();
|
||||
cy.wait(['@getHistory']);
|
||||
cy.wait(['@getVersion']);
|
||||
|
||||
cy.intercept('GET', '/rest/workflows/*').as('workflowGet');
|
||||
getWorkflowHistoryCloseButton().click();
|
||||
cy.wait(['@workflowGet']);
|
||||
cy.wait(1000);
|
||||
|
||||
editWorkflowMoreAndActivate();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Editor zoom should work after route changes', () => {
|
||||
beforeEach(() => {
|
||||
cy.enableFeature('debugInEditor');
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
clickGetBackToCanvas,
|
||||
getRunDataInfoCallout,
|
||||
getOutputPanelTable,
|
||||
toggleParameterCheckboxInputByName,
|
||||
checkParameterCheckboxInputByName,
|
||||
} from '../composables/ndv';
|
||||
import {
|
||||
addLanguageModelNodeToParent,
|
||||
|
@ -38,8 +38,6 @@ import {
|
|||
addToolNodeToParent,
|
||||
clickExecuteWorkflowButton,
|
||||
clickManualChatButton,
|
||||
disableNode,
|
||||
getExecuteWorkflowButton,
|
||||
navigateToNewWorkflowPage,
|
||||
getNodes,
|
||||
openNode,
|
||||
|
@ -73,31 +71,10 @@ describe('Langchain Integration', () => {
|
|||
getManualChatModal().should('not.exist');
|
||||
});
|
||||
|
||||
it('should disable test workflow button', () => {
|
||||
addNodeToCanvas('Schedule Trigger', true);
|
||||
addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
|
||||
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true, true);
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addLanguageModelNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AGENT_NODE_NAME,
|
||||
true,
|
||||
);
|
||||
clickGetBackToCanvas();
|
||||
|
||||
disableNode('Schedule Trigger');
|
||||
|
||||
getExecuteWorkflowButton().should('be.disabled');
|
||||
});
|
||||
|
||||
it('should add nodes to all Agent node input types', () => {
|
||||
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true, true);
|
||||
toggleParameterCheckboxInputByName('hasOutputParser');
|
||||
checkParameterCheckboxInputByName('hasOutputParser');
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addLanguageModelNodeToParent(
|
||||
|
@ -368,58 +345,6 @@ describe('Langchain Integration', () => {
|
|||
getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist');
|
||||
getNodes().should('have.length', 3);
|
||||
});
|
||||
it('should render runItems for sub-nodes and allow switching between them', () => {
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
cy.createFixtureWorkflow('In_memory_vector_store_fake_embeddings.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.executeNode('Populate VS');
|
||||
cy.get('[data-label="25 items"]').should('exist');
|
||||
|
||||
const assertInputOutputText = (text: string, assertion: 'exist' | 'not.exist') => {
|
||||
ndv.getters.outputPanel().contains(text).should(assertion);
|
||||
ndv.getters.inputPanel().contains(text).should(assertion);
|
||||
};
|
||||
|
||||
workflowPage.actions.openNode('Character Text Splitter');
|
||||
ndv.getters.outputRunSelector().should('exist');
|
||||
ndv.getters.inputRunSelector().should('exist');
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '3 of 3');
|
||||
ndv.getters.outputRunSelector().find('input').should('include.value', '3 of 3');
|
||||
assertInputOutputText('Kyiv', 'exist');
|
||||
assertInputOutputText('Berlin', 'not.exist');
|
||||
assertInputOutputText('Prague', 'not.exist');
|
||||
|
||||
ndv.actions.changeOutputRunSelector('2 of 3');
|
||||
assertInputOutputText('Berlin', 'exist');
|
||||
assertInputOutputText('Kyiv', 'not.exist');
|
||||
assertInputOutputText('Prague', 'not.exist');
|
||||
|
||||
ndv.actions.changeOutputRunSelector('1 of 3');
|
||||
assertInputOutputText('Prague', 'exist');
|
||||
assertInputOutputText('Berlin', 'not.exist');
|
||||
assertInputOutputText('Kyiv', 'not.exist');
|
||||
|
||||
ndv.actions.toggleInputRunLinking();
|
||||
ndv.actions.changeOutputRunSelector('2 of 3');
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3');
|
||||
ndv.getters.outputRunSelector().find('input').should('include.value', '2 of 3');
|
||||
ndv.getters.inputPanel().contains('Prague').should('exist');
|
||||
ndv.getters.inputPanel().contains('Berlin').should('not.exist');
|
||||
|
||||
ndv.getters.outputPanel().contains('Berlin').should('exist');
|
||||
ndv.getters.outputPanel().contains('Prague').should('not.exist');
|
||||
|
||||
ndv.actions.toggleInputRunLinking();
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3');
|
||||
ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 3');
|
||||
assertInputOutputText('Prague', 'exist');
|
||||
assertInputOutputText('Berlin', 'not.exist');
|
||||
assertInputOutputText('Kyiv', 'not.exist');
|
||||
});
|
||||
|
||||
it('should show tool info notice if no existing tools were used during execution', () => {
|
||||
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
|
||||
|
@ -518,4 +443,29 @@ describe('Langchain Integration', () => {
|
|||
|
||||
getRunDataInfoCallout().should('not.exist');
|
||||
});
|
||||
|
||||
it('should execute up to Node 1 when using partial execution', () => {
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
cy.createFixtureWorkflow('Test_workflow_chat_partial_execution.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
getManualChatModal().should('not.exist');
|
||||
openNode('Node 1');
|
||||
ndv.actions.execute();
|
||||
|
||||
getManualChatModal().should('exist');
|
||||
sendManualChatMessage('Test');
|
||||
|
||||
getManualChatMessages().should('contain', 'this_my_field_1');
|
||||
cy.getByTestId('refresh-session-button').click();
|
||||
cy.get('button').contains('Reset').click();
|
||||
getManualChatMessages().should('not.exist');
|
||||
|
||||
sendManualChatMessage('Another test');
|
||||
getManualChatMessages().should('contain', 'this_my_field_3');
|
||||
getManualChatMessages().should('contain', 'this_my_field_4');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { setCredentialValues } from '../composables/modals/credential-modal';
|
||||
import { clickCreateNewCredential, selectResourceLocatorItem } from '../composables/ndv';
|
||||
import * as projects from '../composables/projects';
|
||||
import {
|
||||
INSTANCE_ADMIN,
|
||||
|
@ -11,18 +13,16 @@ import {
|
|||
WorkflowPage,
|
||||
CredentialsModal,
|
||||
CredentialsPage,
|
||||
WorkflowExecutionsTab,
|
||||
NDV,
|
||||
MainSidebar,
|
||||
} from '../pages';
|
||||
import { clearNotifications, successToast } from '../pages/notifications';
|
||||
import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';
|
||||
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();
|
||||
const mainSidebar = new MainSidebar();
|
||||
|
||||
|
@ -36,207 +36,6 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
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', 3);
|
||||
|
||||
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', 4);
|
||||
|
||||
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("Overview")[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().findChildByTestId('card-content').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("Overview")[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("Overview")[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);
|
||||
|
@ -245,26 +44,6 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
projects.getMenuItems().should('not.exist');
|
||||
});
|
||||
|
||||
it('should not show viewer role if not licensed', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabSettings().click();
|
||||
|
||||
cy.get(
|
||||
`[data-test-id="user-list-item-${INSTANCE_MEMBERS[0].email}"] [data-test-id="projects-settings-user-role-select"]`,
|
||||
).click();
|
||||
|
||||
cy.get('.el-select-dropdown__item.is-disabled')
|
||||
.should('contain.text', 'Viewer')
|
||||
.get('span:contains("Upgrade")')
|
||||
.filter(':visible')
|
||||
.click();
|
||||
|
||||
getVisibleModalOverlay().should('contain.text', 'Upgrade to unlock additional roles');
|
||||
});
|
||||
|
||||
describe('when starting from scratch', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetDatabase();
|
||||
|
@ -275,7 +54,11 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
cy.changeQuota('maxTeamProjects', -1);
|
||||
});
|
||||
|
||||
it('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', () => {
|
||||
/**
|
||||
* @TODO: New Canvas - Fix this test
|
||||
*/
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
|
@ -367,7 +150,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 2)
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('contain.text', 'Notion account project 1');
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
@ -382,7 +165,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 2)
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('contain.text', 'Notion account project 1');
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
@ -396,7 +179,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 2)
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('contain.text', 'Notion account project 2');
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
@ -407,7 +190,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 2)
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('contain.text', 'Notion account project 2');
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
@ -425,7 +208,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 2)
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('contain.text', 'Notion account personal project');
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
@ -436,7 +219,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 2)
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('contain.text', 'Notion account personal project');
|
||||
});
|
||||
|
@ -754,80 +537,64 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
ndv.getters.credentialInput().find('input').should('be.enabled');
|
||||
});
|
||||
|
||||
it('should handle viewer role', () => {
|
||||
cy.enableFeature('projectRole:viewer');
|
||||
it('should create sub-workflow and credential in the sub-workflow in the same project', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
projects.createProject('Development');
|
||||
projects.addProjectMember(INSTANCE_MEMBERS[0].email, 'Viewer');
|
||||
projects.getProjectSettingsSaveButton().click();
|
||||
|
||||
projects.createProject('Dev');
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_4_executions_view.json', 'WF with random error');
|
||||
executionsTab.actions.createManualExecutions(2);
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
executionsTab.actions.createManualExecutions(2);
|
||||
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
|
||||
workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
workflowPage.actions.addNodeToCanvas('Execute Workflow', true, true);
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Notion API');
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').callsFake((url) => {
|
||||
cy.visit(url);
|
||||
});
|
||||
});
|
||||
|
||||
mainSidebar.actions.openUserMenu();
|
||||
cy.getByTestId('user-menu-item-logout').click();
|
||||
selectResourceLocatorItem('workflowId', 0, 'Create a');
|
||||
|
||||
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();
|
||||
cy.get('body').type('{esc}');
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
apiKey: 'abc123',
|
||||
});
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
|
||||
projects.getMenuItems().last().click();
|
||||
projects.getProjectTabExecutions().click();
|
||||
cy.getByTestId('global-execution-list-item').first().find('td:last button').click();
|
||||
getVisibleDropdown()
|
||||
.find('li')
|
||||
.filter(':contains("Retry")')
|
||||
.should('have.class', 'is-disabled');
|
||||
getVisibleDropdown()
|
||||
.find('li')
|
||||
.filter(':contains("Delete")')
|
||||
.should('have.class', 'is-disabled');
|
||||
workflowsPage.getters.workflowCards().should('have.length', 2);
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
cy.getByTestId('workflow-card-name').should('be.visible').first().click();
|
||||
workflowPage.getters.nodeViewRoot().should('be.visible');
|
||||
workflowPage.getters.executeWorkflowButton().should('not.exist');
|
||||
workflowPage.getters.nodeCreatorPlusButton().should('not.exist');
|
||||
workflowPage.getters.canvasNodes().should('have.length', 3).last().click();
|
||||
cy.get('body').type('{backspace}');
|
||||
workflowPage.getters.canvasNodes().should('have.length', 3).last().rightclick();
|
||||
getVisibleDropdown()
|
||||
.find('li')
|
||||
.should('be.visible')
|
||||
.filter(
|
||||
':contains("Open"), :contains("Copy"), :contains("Select all"), :contains("Clear selection")',
|
||||
)
|
||||
.should('not.have.class', 'is-disabled');
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.getByTestId('retry-execution-button')
|
||||
.should('be.visible')
|
||||
.find('.is-disabled')
|
||||
.should('exist');
|
||||
cy.get('button:contains("Debug")').should('be.disabled');
|
||||
cy.get('button[title="Retry execution"]').should('be.disabled');
|
||||
cy.get('button[title="Delete this execution"]').should('be.disabled');
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.credentialCards().filter(':contains("Notion")').click();
|
||||
cy.getByTestId('node-credentials-config-container')
|
||||
.should('be.visible')
|
||||
.find('input')
|
||||
.should('not.have.length');
|
||||
credentialsPage.getters.credentialCards().should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should create credential from workflow in the correct project after editor page refresh', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
projects.createProject('Dev');
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
|
||||
cy.reload();
|
||||
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
apiKey: 'abc123',
|
||||
});
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
|
||||
projects.getMenuItems().last().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.credentialCards().should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -76,11 +76,21 @@ describe('Node Creator', () => {
|
|||
nodeCreatorFeature.getters.canvasAddButton().click();
|
||||
WorkflowPage.actions.addNodeToCanvas('Manual', false);
|
||||
|
||||
nodeCreatorFeature.getters.canvasAddButton().should('not.be.visible');
|
||||
nodeCreatorFeature.getters.nodeCreator().should('not.exist');
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
nodeCreatorFeature.getters.canvasAddButton().should('not.be.visible');
|
||||
nodeCreatorFeature.getters.nodeCreator().should('not.exist');
|
||||
// TODO: Replace once we have canvas feature utils
|
||||
cy.get('div').contains('Add first step').should('be.hidden');
|
||||
},
|
||||
() => {
|
||||
nodeCreatorFeature.getters.canvasAddButton().should('not.exist');
|
||||
nodeCreatorFeature.getters.nodeCreator().should('not.exist');
|
||||
// TODO: Replace once we have canvas feature utils
|
||||
cy.get('div').contains('Add first step').should('not.exist');
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: Replace once we have canvas feature utils
|
||||
cy.get('div').contains('Add first step').should('be.hidden');
|
||||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
nodeCreatorFeature.getters.nodeCreator().contains('What happens next?').should('be.visible');
|
||||
|
||||
|
@ -125,7 +135,6 @@ describe('Node Creator', () => {
|
|||
'OpenThesaurus',
|
||||
'Spontit',
|
||||
'Vonage',
|
||||
'Send Email',
|
||||
'Toggl Trigger',
|
||||
];
|
||||
const doubleActionNode = 'OpenWeatherMap';
|
||||
|
@ -346,7 +355,15 @@ describe('Node Creator', () => {
|
|||
|
||||
it('should correctly append a No Op node when Loop Over Items node is added (from connection)', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas('Manual');
|
||||
cy.get('.plus-endpoint').should('be.visible').click();
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
cy.get('.plus-endpoint').click();
|
||||
},
|
||||
() => {
|
||||
cy.getByTestId('canvas-handle-plus').click();
|
||||
},
|
||||
);
|
||||
|
||||
nodeCreatorFeature.getters.searchBar().find('input').type('Loop Over Items');
|
||||
nodeCreatorFeature.getters.getCreatorItem('Loop Over Items').click();
|
||||
|
@ -531,7 +548,7 @@ describe('Node Creator', () => {
|
|||
vectorStores.each((_i, vectorStore) => {
|
||||
nodeCreatorFeature.getters.getCreatorItem(vectorStore).click();
|
||||
actions.forEach((action) => {
|
||||
nodeCreatorFeature.getters.getCreatorItem(action).should('be.visible');
|
||||
nodeCreatorFeature.getters.getCreatorItem(action).should('be.visible').realHover();
|
||||
});
|
||||
cy.realPress('ArrowLeft');
|
||||
});
|
||||
|
@ -554,4 +571,13 @@ describe('Node Creator', () => {
|
|||
|
||||
addVectorStoreToolToParent('In-Memory Vector Store', AGENT_NODE_NAME);
|
||||
});
|
||||
|
||||
it('should insert node to canvas with sendAndWait operation selected', () => {
|
||||
nodeCreatorFeature.getters.canvasAddButton().click();
|
||||
WorkflowPage.actions.addNodeToCanvas('Manual', false);
|
||||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
cy.contains('Human in the loop').click();
|
||||
nodeCreatorFeature.getters.getCreatorItem('Slack').click();
|
||||
cy.contains('Send and Wait for Response').should('exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@ import { GMAIL_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
|
|||
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
|
||||
import { AIAssistant } from '../pages/features/ai-assistant';
|
||||
import { NodeCreator } from '../pages/features/node-creator';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const wf = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
@ -434,7 +433,7 @@ describe('AI Assistant Credential Help', () => {
|
|||
wf.actions.addNodeToCanvas('Slack', true, true, 'Get a channel');
|
||||
wf.getters.nodeCredentialsSelect().should('exist');
|
||||
wf.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
wf.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
ndv.getters.copyInput().should('not.exist');
|
||||
credentialsModal.getters.oauthConnectButton().should('have.length', 1);
|
||||
|
@ -467,7 +466,7 @@ describe('AI Assistant Credential Help', () => {
|
|||
wf.actions.addNodeToCanvas('Microsoft Outlook', true, true, 'Get a calendar');
|
||||
wf.getters.nodeCredentialsSelect().should('exist');
|
||||
wf.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
wf.getters.nodeCredentialsCreateOption().click();
|
||||
ndv.getters.copyInput().should('not.exist');
|
||||
credentialsModal.getters.oauthConnectButton().should('have.length', 1);
|
||||
credentialsModal.getters.credentialInputs().should('have.length', 1);
|
||||
|
|
|
@ -17,7 +17,7 @@ describe('Workflow Selector Parameter', () => {
|
|||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addInitialNodeToCanvas(EXECUTE_WORKFLOW_NODE_NAME, {
|
||||
keepNdvOpen: true,
|
||||
action: 'Call Another Workflow',
|
||||
action: 'Execute A Sub Workflow',
|
||||
});
|
||||
});
|
||||
it('should render sub-workflows list', () => {
|
||||
|
@ -86,6 +86,8 @@ describe('Workflow Selector Parameter', () => {
|
|||
cy.stub(win, 'open').as('windowOpen');
|
||||
});
|
||||
|
||||
cy.intercept('POST', '/rest/workflows*').as('createSubworkflow');
|
||||
|
||||
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||
|
||||
|
@ -94,14 +96,24 @@ describe('Workflow Selector Parameter', () => {
|
|||
.findChildByTestId('rlc-item')
|
||||
.eq(0)
|
||||
.find('span')
|
||||
.should('have.text', 'Create a new sub-workflow');
|
||||
.should('contain.text', 'Create a'); // Due to some inconsistency we're sometimes in a project and sometimes not, this covers both cases
|
||||
|
||||
getVisiblePopper().findChildByTestId('rlc-item').eq(0).click();
|
||||
|
||||
const SAMPLE_SUBWORKFLOW_TEMPLATE_ID = 'VMiAxXa3lCAizGB5f7dVZQSFfg3FtHkdTKvLuupqBls=';
|
||||
cy.get('@windowOpen').should(
|
||||
'be.calledWith',
|
||||
`/workflows/onboarding/${SAMPLE_SUBWORKFLOW_TEMPLATE_ID}?sampleSubWorkflows=0`,
|
||||
);
|
||||
cy.wait('@createSubworkflow').then((interception) => {
|
||||
expect(interception.request.body).to.have.property('name').that.includes('Sub-Workflow');
|
||||
expect(interception.request.body.nodes).to.be.an('array');
|
||||
expect(interception.request.body.nodes).to.have.length(2);
|
||||
expect(interception.request.body.nodes[0]).to.have.property(
|
||||
'name',
|
||||
'When Executed by Another Workflow',
|
||||
);
|
||||
expect(interception.request.body.nodes[1]).to.have.property(
|
||||
'name',
|
||||
'Replace me with your logic',
|
||||
);
|
||||
});
|
||||
|
||||
cy.get('@windowOpen').should('be.calledWithMatch', /\/workflow\/.+/);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
import {
|
||||
getExecutionPreviewOutputPanelRelatedExecutionLink,
|
||||
getExecutionsSidebar,
|
||||
getWorkflowExecutionPreviewIframe,
|
||||
openExecutionPreviewNode,
|
||||
} from '../composables/executions';
|
||||
import {
|
||||
changeOutputRunSelector,
|
||||
getOutputPanelItemsCount,
|
||||
|
@ -103,38 +97,4 @@ describe('Subworkflow debugging', () => {
|
|||
getOutputTbodyCell(1, 2).should('include.text', 'Terry.Dach@hotmail.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('can inspect parent executions', () => {
|
||||
cy.url().then((workflowUrl) => {
|
||||
openNode('Execute Workflow with param');
|
||||
|
||||
getOutputPanelItemsCount().should('contain.text', '2 items, 1 sub-execution');
|
||||
getOutputPanelRelatedExecutionLink().should('contain.text', 'View sub-execution');
|
||||
getOutputPanelRelatedExecutionLink().should('have.attr', 'href');
|
||||
|
||||
// ensure workflow executed and waited on output
|
||||
getOutputTableHeaders().should('have.length', 2);
|
||||
getOutputTbodyCell(1, 0).should('have.text', 'world Natalie Moore');
|
||||
|
||||
// cypress cannot handle new tabs so removing it
|
||||
getOutputPanelRelatedExecutionLink().invoke('removeAttr', 'target').click();
|
||||
|
||||
getExecutionsSidebar().should('be.visible');
|
||||
getWorkflowExecutionPreviewIframe().should('be.visible');
|
||||
openExecutionPreviewNode('Execute Workflow Trigger');
|
||||
|
||||
getExecutionPreviewOutputPanelRelatedExecutionLink().should(
|
||||
'include.text',
|
||||
'View parent execution',
|
||||
);
|
||||
|
||||
getExecutionPreviewOutputPanelRelatedExecutionLink()
|
||||
.invoke('removeAttr', 'target')
|
||||
.click({ force: true });
|
||||
|
||||
cy.url().then((currentUrl) => {
|
||||
expect(currentUrl === workflowUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,60 +1,227 @@
|
|||
import { clickGetBackToCanvas, getOutputTableHeaders } from '../composables/ndv';
|
||||
import {
|
||||
addItemToFixedCollection,
|
||||
assertNodeOutputHintExists,
|
||||
clickExecuteNode,
|
||||
clickGetBackToCanvas,
|
||||
getExecuteNodeButton,
|
||||
getOutputTableHeaders,
|
||||
getParameterInputByName,
|
||||
populateFixedCollection,
|
||||
selectResourceLocatorItem,
|
||||
typeIntoFixedCollectionItem,
|
||||
clickWorkflowCardContent,
|
||||
assertOutputTableContent,
|
||||
populateMapperFields,
|
||||
getNodeRunInfoStale,
|
||||
assertNodeOutputErrorMessageExists,
|
||||
checkParameterCheckboxInputByName,
|
||||
uncheckParameterCheckboxInputByName,
|
||||
} from '../composables/ndv';
|
||||
import {
|
||||
clickExecuteWorkflowButton,
|
||||
clickZoomToFit,
|
||||
navigateToNewWorkflowPage,
|
||||
openNode,
|
||||
pasteWorkflow,
|
||||
saveWorkflowOnButtonClick,
|
||||
} from '../composables/workflow';
|
||||
import { visitWorkflowsPage } from '../composables/workflowsPage';
|
||||
import SUB_WORKFLOW_INPUTS from '../fixtures/Test_Subworkflow-Inputs.json';
|
||||
import { NDV, WorkflowsPage, WorkflowPage } from '../pages';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
import { getVisiblePopper } from '../utils';
|
||||
|
||||
const ndv = new NDV();
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
const workflow = new WorkflowPage();
|
||||
|
||||
const DEFAULT_WORKFLOW_NAME = 'My workflow';
|
||||
const DEFAULT_SUBWORKFLOW_NAME_1 = 'My Sub-Workflow 1';
|
||||
const DEFAULT_SUBWORKFLOW_NAME_2 = 'My Sub-Workflow 2';
|
||||
|
||||
type FieldRow = readonly string[];
|
||||
|
||||
const exampleFields = [
|
||||
const EXAMPLE_FIELDS = [
|
||||
['aNumber', 'Number'],
|
||||
['aString', 'String'],
|
||||
['aArray', 'Array'],
|
||||
['aObject', 'Object'],
|
||||
['aAny', 'Allow Any Type'],
|
||||
// bool last since it's not an inputField so we'll skip it for some cases
|
||||
// bool last because it's a switch instead of a normal inputField so we'll skip it for some cases
|
||||
['aBool', 'Boolean'],
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Populate multiValue fixedCollections. Only supports fixedCollections for which all fields can be defined via keyboard typing
|
||||
*
|
||||
* @param items - 2D array of items to populate, i.e. [["myField1", "String"], [""]
|
||||
* @param collectionName - name of the fixedCollection to populate
|
||||
* @param offset - amount of 'parameter-input's before the fixedCollection under test
|
||||
* @returns
|
||||
*/
|
||||
function populateFixedCollection(
|
||||
items: readonly FieldRow[],
|
||||
collectionName: string,
|
||||
offset: number,
|
||||
) {
|
||||
if (items.length === 0) return;
|
||||
const n = items[0].length;
|
||||
for (const [i, params] of items.entries()) {
|
||||
ndv.actions.addItemToFixedCollection(collectionName);
|
||||
for (const [j, param] of params.entries()) {
|
||||
ndv.getters
|
||||
.fixedCollectionParameter(collectionName)
|
||||
.getByTestId('parameter-input')
|
||||
.eq(offset + i * n + j)
|
||||
.type(`${param}{downArrow}{enter}`);
|
||||
}
|
||||
type TypeField = 'Allow Any Type' | 'String' | 'Number' | 'Boolean' | 'Array' | 'Object';
|
||||
|
||||
describe('Sub-workflow creation and typed usage', () => {
|
||||
beforeEach(() => {
|
||||
navigateToNewWorkflowPage();
|
||||
pasteWorkflow(SUB_WORKFLOW_INPUTS);
|
||||
saveWorkflowOnButtonClick();
|
||||
clickZoomToFit();
|
||||
|
||||
openNode('Execute Workflow');
|
||||
|
||||
// Prevent sub-workflow from opening in new window
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').callsFake((url) => {
|
||||
cy.visit(url);
|
||||
});
|
||||
});
|
||||
selectResourceLocatorItem('workflowId', 0, 'Create a');
|
||||
// **************************
|
||||
// NAVIGATE TO CHILD WORKFLOW
|
||||
// **************************
|
||||
// Close NDV before opening the node creator
|
||||
cy.get('body').type('{esc}');
|
||||
openNode('When Executed by Another Workflow');
|
||||
});
|
||||
|
||||
it('works with type-checked values', () => {
|
||||
populateFixedCollection(EXAMPLE_FIELDS, 'workflowInputs', 1);
|
||||
|
||||
validateAndReturnToParent(
|
||||
DEFAULT_SUBWORKFLOW_NAME_1,
|
||||
1,
|
||||
EXAMPLE_FIELDS.map((f) => f[0]),
|
||||
);
|
||||
|
||||
const values = [
|
||||
'-1', // number fields don't support `=` switch to expression, so let's test the Fixed case with it
|
||||
...EXAMPLE_FIELDS.slice(1).map((x) => `={{}{{} $json.a${x[0]}`), // the `}}` at the end are added automatically
|
||||
];
|
||||
|
||||
// this matches with the pinned data provided in the fixture
|
||||
populateMapperFields(values.map((x, i) => [EXAMPLE_FIELDS[i][0], x]));
|
||||
|
||||
clickExecuteNode();
|
||||
|
||||
const expected = [
|
||||
['-1', 'A String', '0:11:true2:3', 'aKey:-1', '[empty object]', 'true'],
|
||||
['-1', 'Another String', '[empty array]', 'aDifferentKey:-1', '[empty array]', 'true'],
|
||||
];
|
||||
assertOutputTableContent(expected);
|
||||
|
||||
// Test the type-checking options
|
||||
populateMapperFields([['aString', '{selectAll}{backspace}{{}{{} 5']]);
|
||||
|
||||
getNodeRunInfoStale().should('exist');
|
||||
clickExecuteNode();
|
||||
|
||||
assertNodeOutputErrorMessageExists();
|
||||
|
||||
// attemptToConvertTypes enabled
|
||||
checkParameterCheckboxInputByName('attemptToConvertTypes');
|
||||
|
||||
getNodeRunInfoStale().should('exist');
|
||||
clickExecuteNode();
|
||||
|
||||
const expected2 = [
|
||||
['-1', '5', '0:11:true2:3', 'aKey:-1', '[empty object]', 'true'],
|
||||
['-1', '5', '[empty array]', 'aDifferentKey:-1', '[empty array]', 'true'],
|
||||
];
|
||||
|
||||
assertOutputTableContent(expected2);
|
||||
|
||||
// disabled again
|
||||
uncheckParameterCheckboxInputByName('attemptToConvertTypes');
|
||||
|
||||
getNodeRunInfoStale().should('exist');
|
||||
clickExecuteNode();
|
||||
|
||||
assertNodeOutputErrorMessageExists();
|
||||
});
|
||||
|
||||
it('works with Fields input source, and can then be changed to JSON input source', () => {
|
||||
assertNodeOutputHintExists();
|
||||
|
||||
populateFixedCollection(EXAMPLE_FIELDS, 'workflowInputs', 1);
|
||||
|
||||
validateAndReturnToParent(
|
||||
DEFAULT_SUBWORKFLOW_NAME_1,
|
||||
1,
|
||||
EXAMPLE_FIELDS.map((f) => f[0]),
|
||||
);
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').callsFake((url) => {
|
||||
cy.visit(url);
|
||||
selectResourceLocatorItem('workflowId', 0, 'Create a');
|
||||
|
||||
openNode('When Executed by Another Workflow');
|
||||
|
||||
getParameterInputByName('inputSource').click();
|
||||
|
||||
getVisiblePopper()
|
||||
.getByTestId('parameter-input')
|
||||
.eq(0)
|
||||
.type('Using JSON Example{downArrow}{enter}');
|
||||
|
||||
const exampleJson =
|
||||
'{{}' + EXAMPLE_FIELDS.map((x) => `"${x[0]}": ${makeExample(x[1])}`).join(',') + '}';
|
||||
getParameterInputByName('jsonExample')
|
||||
.find('.cm-line')
|
||||
.eq(0)
|
||||
.type(`{selectAll}{backspace}${exampleJson}{enter}`);
|
||||
|
||||
// first one doesn't work for some reason, might need to wait for something?
|
||||
clickExecuteNode();
|
||||
|
||||
validateAndReturnToParent(
|
||||
DEFAULT_SUBWORKFLOW_NAME_2,
|
||||
2,
|
||||
EXAMPLE_FIELDS.map((f) => f[0]),
|
||||
);
|
||||
|
||||
assertOutputTableContent([
|
||||
['[null]', '[null]', '[null]', '[null]', '[null]', 'true'],
|
||||
['[null]', '[null]', '[null]', '[null]', '[null]', 'true'],
|
||||
]);
|
||||
|
||||
clickExecuteNode();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show node issue when no fields are defined in manual mode', () => {
|
||||
getExecuteNodeButton().should('be.disabled');
|
||||
clickGetBackToCanvas();
|
||||
// Executing the workflow should show an error toast
|
||||
clickExecuteWorkflowButton();
|
||||
errorToast().should('contain', 'The workflow has issues');
|
||||
openNode('When Executed by Another Workflow');
|
||||
// Add a field to the workflowInputs fixedCollection
|
||||
addItemToFixedCollection('workflowInputs');
|
||||
typeIntoFixedCollectionItem('workflowInputs', 0, 'test');
|
||||
// Executing the workflow should not show error now
|
||||
clickGetBackToCanvas();
|
||||
clickExecuteWorkflowButton();
|
||||
successToast().should('contain', 'Workflow executed successfully');
|
||||
});
|
||||
});
|
||||
|
||||
// This function starts off in the Child Workflow Input Trigger, assuming we just defined the input fields
|
||||
// It then navigates back to the parent and validates the outputPanel matches our changes
|
||||
function validateAndReturnToParent(targetChild: string, offset: number, fields: string[]) {
|
||||
clickExecuteNode();
|
||||
|
||||
// + 1 to account for formatting-only column
|
||||
getOutputTableHeaders().should('have.length', fields.length + 1);
|
||||
for (const [i, name] of fields.entries()) {
|
||||
getOutputTableHeaders().eq(i).should('have.text', name);
|
||||
}
|
||||
|
||||
clickGetBackToCanvas();
|
||||
saveWorkflowOnButtonClick();
|
||||
|
||||
visitWorkflowsPage();
|
||||
|
||||
clickWorkflowCardContent(DEFAULT_WORKFLOW_NAME);
|
||||
|
||||
openNode('Execute Workflow');
|
||||
|
||||
// Note that outside of e2e tests this will be pre-selected correctly.
|
||||
// Due to our workaround to remain in the same tab we need to select the correct tab manually
|
||||
selectResourceLocatorItem('workflowId', offset, targetChild);
|
||||
|
||||
clickExecuteNode();
|
||||
|
||||
getOutputTableHeaders().should('have.length', fields.length + 1);
|
||||
for (const [i, name] of fields.entries()) {
|
||||
getOutputTableHeaders().eq(i).should('have.text', name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,215 +241,3 @@ function makeExample(type: TypeField) {
|
|||
return 'null';
|
||||
}
|
||||
}
|
||||
|
||||
type TypeField = 'Allow Any Type' | 'String' | 'Number' | 'Boolean' | 'Array' | 'Object';
|
||||
function populateFields(items: ReadonlyArray<readonly [string, TypeField]>) {
|
||||
populateFixedCollection(items, 'workflowInputs', 1);
|
||||
}
|
||||
|
||||
function navigateWorkflowSelectionDropdown(index: number, expectedText: string) {
|
||||
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||
|
||||
getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist');
|
||||
getVisiblePopper()
|
||||
.findChildByTestId('rlc-item')
|
||||
.eq(index)
|
||||
.find('span')
|
||||
.should('have.text', expectedText)
|
||||
.click();
|
||||
}
|
||||
|
||||
function populateMapperFields(values: readonly string[], offset: number) {
|
||||
for (const [i, value] of values.entries()) {
|
||||
cy.getByTestId('parameter-input')
|
||||
.eq(offset + i)
|
||||
.type(value);
|
||||
|
||||
// Click on a parent to dismiss the pop up hiding the field below.
|
||||
cy.getByTestId('parameter-input')
|
||||
.eq(offset + i)
|
||||
.parent()
|
||||
.parent()
|
||||
.click('topLeft');
|
||||
}
|
||||
}
|
||||
|
||||
// This function starts off in the Child Workflow Input Trigger, assuming we just defined the input fields
|
||||
// It then navigates back to the parent and validates output
|
||||
function validateAndReturnToParent(targetChild: string, offset: number, fields: string[]) {
|
||||
ndv.actions.execute();
|
||||
|
||||
// + 1 to account for formatting-only column
|
||||
getOutputTableHeaders().should('have.length', fields.length + 1);
|
||||
for (const [i, name] of fields.entries()) {
|
||||
getOutputTableHeaders().eq(i).should('have.text', name);
|
||||
}
|
||||
|
||||
clickGetBackToCanvas();
|
||||
saveWorkflowOnButtonClick();
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
workflowsPage.getters.workflowCardContent(DEFAULT_WORKFLOW_NAME).click();
|
||||
|
||||
openNode('Execute Workflow');
|
||||
|
||||
// Note that outside of e2e tests this will be pre-selected correctly.
|
||||
// Due to our workaround to remain in the same tab we need to select the correct tab manually
|
||||
navigateWorkflowSelectionDropdown(offset, targetChild);
|
||||
|
||||
// This fails, pointing to `usePushConnection` `const triggerNode = subWorkflow?.nodes.find` being `undefined.find()`I <think>
|
||||
ndv.actions.execute();
|
||||
|
||||
getOutputTableHeaders().should('have.length', fields.length + 1);
|
||||
for (const [i, name] of fields.entries()) {
|
||||
getOutputTableHeaders().eq(i).should('have.text', name);
|
||||
}
|
||||
|
||||
// todo: verify the fields appear and show the correct types
|
||||
|
||||
// todo: fill in the input fields (and mock previous node data in the json fixture to match)
|
||||
|
||||
// todo: validate the actual output data
|
||||
}
|
||||
|
||||
function setWorkflowInputFieldValue(index: number, value: string) {
|
||||
ndv.actions.addItemToFixedCollection('workflowInputs');
|
||||
ndv.actions.typeIntoFixedCollectionItem('workflowInputs', index, value);
|
||||
}
|
||||
|
||||
describe('Sub-workflow creation and typed usage', () => {
|
||||
beforeEach(() => {
|
||||
navigateToNewWorkflowPage();
|
||||
pasteWorkflow(SUB_WORKFLOW_INPUTS);
|
||||
saveWorkflowOnButtonClick();
|
||||
clickZoomToFit();
|
||||
|
||||
openNode('Execute Workflow');
|
||||
|
||||
// Prevent sub-workflow from opening in new window
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').callsFake((url) => {
|
||||
cy.visit(url);
|
||||
});
|
||||
});
|
||||
navigateWorkflowSelectionDropdown(0, 'Create a new sub-workflow');
|
||||
// **************************
|
||||
// NAVIGATE TO CHILD WORKFLOW
|
||||
// **************************
|
||||
|
||||
openNode('Workflow Input Trigger');
|
||||
});
|
||||
|
||||
it('works with type-checked values', () => {
|
||||
populateFields(exampleFields);
|
||||
|
||||
validateAndReturnToParent(
|
||||
DEFAULT_SUBWORKFLOW_NAME_1,
|
||||
1,
|
||||
exampleFields.map((f) => f[0]),
|
||||
);
|
||||
|
||||
const values = [
|
||||
'-1', // number fields don't support `=` switch to expression, so let's test the Fixed case with it
|
||||
...exampleFields.slice(1).map((x) => `={{}{{} $json.a${x[0]}`), // }} are added automatically
|
||||
];
|
||||
|
||||
// this matches with the pinned data provided in the fixture
|
||||
populateMapperFields(values, 2);
|
||||
|
||||
ndv.actions.execute();
|
||||
|
||||
// todo:
|
||||
// - validate output lines up
|
||||
// - change input to need casts
|
||||
// - run
|
||||
// - confirm error
|
||||
// - switch `attemptToConvertTypes` flag
|
||||
// - confirm success and changed output
|
||||
// - change input to be invalid despite cast
|
||||
// - run
|
||||
// - confirm error
|
||||
// - switch type option flags
|
||||
// - run
|
||||
// - confirm success
|
||||
// - turn off attempt to cast flag
|
||||
// - confirm a value was not cast
|
||||
});
|
||||
|
||||
it('works with Fields input source into JSON input source', () => {
|
||||
ndv.getters.nodeOutputHint().should('exist');
|
||||
|
||||
populateFields(exampleFields);
|
||||
|
||||
validateAndReturnToParent(
|
||||
DEFAULT_SUBWORKFLOW_NAME_1,
|
||||
1,
|
||||
exampleFields.map((f) => f[0]),
|
||||
);
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').callsFake((url) => {
|
||||
cy.visit(url);
|
||||
});
|
||||
});
|
||||
navigateWorkflowSelectionDropdown(0, 'Create a new sub-workflow');
|
||||
|
||||
openNode('Workflow Input Trigger');
|
||||
|
||||
cy.getByTestId('parameter-input').eq(0).click();
|
||||
|
||||
// Todo: Check if there's a better way to interact with option dropdowns
|
||||
// This PR would add this child testId
|
||||
getVisiblePopper()
|
||||
.getByTestId('parameter-input')
|
||||
.eq(0)
|
||||
.type('Using JSON Example{downArrow}{enter}');
|
||||
|
||||
const exampleJson =
|
||||
'{{}' + exampleFields.map((x) => `"${x[0]}": ${makeExample(x[1])}`).join(',') + '}';
|
||||
cy.getByTestId('parameter-input-jsonExample')
|
||||
.find('.cm-line')
|
||||
.eq(0)
|
||||
.type(`{selectAll}{backspace}${exampleJson}{enter}`);
|
||||
|
||||
// first one doesn't work for some reason, might need to wait for something?
|
||||
ndv.actions.execute();
|
||||
|
||||
validateAndReturnToParent(
|
||||
DEFAULT_SUBWORKFLOW_NAME_2,
|
||||
2,
|
||||
exampleFields.map((f) => f[0]),
|
||||
);
|
||||
|
||||
// test for either InputSource mode and options combinations:
|
||||
// + we're showing the notice in the output panel
|
||||
// + we start with no fields
|
||||
// + Test Step works and we create the fields
|
||||
// + create field of each type (string, number, boolean, object, array, any)
|
||||
// + exit ndv
|
||||
// + save
|
||||
// + go back to parent workflow
|
||||
// - verify fields appear [needs Ivan's PR]
|
||||
// - link fields [needs Ivan's PR]
|
||||
// + run parent
|
||||
// - verify output with `null` defaults exists
|
||||
//
|
||||
});
|
||||
|
||||
it('should show node issue when no fields are defined in manual mode', () => {
|
||||
ndv.getters.nodeExecuteButton().should('be.disabled');
|
||||
ndv.actions.close();
|
||||
// Executing the workflow should show an error toast
|
||||
workflow.actions.executeWorkflow();
|
||||
errorToast().should('contain', 'The workflow has issues');
|
||||
openNode('Workflow Input Trigger');
|
||||
// Add a field to the workflowInputs fixedCollection
|
||||
setWorkflowInputFieldValue(0, 'test');
|
||||
// Executing the workflow should not show error now
|
||||
ndv.actions.close();
|
||||
workflow.actions.executeWorkflow();
|
||||
successToast().should('contain', 'Workflow executed successfully');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { setCredentialValues } from '../composables/modals/credential-modal';
|
||||
import { clickCreateNewCredential } from '../composables/ndv';
|
||||
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants';
|
||||
import { clickCreateNewCredential, setParameterSelectByContent } from '../composables/ndv';
|
||||
import {
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
NOTION_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
import { NodeCreator } from '../pages/features/node-creator';
|
||||
|
||||
|
@ -106,7 +110,10 @@ describe('NDV', () => {
|
|||
ndv.actions.execute();
|
||||
ndv.getters
|
||||
.nodeRunErrorMessage()
|
||||
.should('have.text', 'Info for expression missing from previous node');
|
||||
.should(
|
||||
'have.text',
|
||||
"Using the item method doesn't work with pinned data in this scenario. Please unpin 'Break pairedItem chain' and try again.",
|
||||
);
|
||||
ndv.getters
|
||||
.nodeRunErrorDescription()
|
||||
.should(
|
||||
|
@ -242,6 +249,15 @@ describe('NDV', () => {
|
|||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.switchOutputMode('Table');
|
||||
|
||||
// Start from linked state
|
||||
ndv.getters.outputLinkRun().then(($el) => {
|
||||
const classList = Array.from($el[0].classList);
|
||||
if (!classList.includes('linked')) {
|
||||
ndv.actions.toggleOutputRunLinking();
|
||||
ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip
|
||||
}
|
||||
});
|
||||
|
||||
ndv.getters
|
||||
.inputRunSelector()
|
||||
.should('exist')
|
||||
|
@ -359,15 +375,71 @@ describe('NDV', () => {
|
|||
ndv.getters.nodeExecuteButton().should('be.visible');
|
||||
});
|
||||
|
||||
it('should allow editing code in fullscreen in the Code node', () => {
|
||||
it('should allow editing code in fullscreen in the code editors', () => {
|
||||
// Code (JavaScript)
|
||||
workflowPage.actions.addInitialNodeToCanvas('Code', { keepNdvOpen: true });
|
||||
ndv.actions.openCodeEditorFullscreen();
|
||||
|
||||
ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('foo()');
|
||||
ndv.getters.codeEditorFullscreen().should('contain.text', 'foo()');
|
||||
cy.wait(200);
|
||||
cy.wait(200); // allow change to emit before closing modal
|
||||
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
|
||||
ndv.getters.parameterInput('jsCode').get('.cm-content').should('contain.text', 'foo()');
|
||||
ndv.actions.close();
|
||||
|
||||
// SQL
|
||||
workflowPage.actions.addNodeToCanvas('Postgres', true, true, 'Execute a SQL query');
|
||||
ndv.actions.openCodeEditorFullscreen();
|
||||
|
||||
ndv.getters
|
||||
.codeEditorFullscreen()
|
||||
.type('{selectall}')
|
||||
.type('{backspace}')
|
||||
.type('SELECT * FROM workflows');
|
||||
ndv.getters.codeEditorFullscreen().should('contain.text', 'SELECT * FROM workflows');
|
||||
cy.wait(200);
|
||||
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
|
||||
ndv.getters
|
||||
.parameterInput('query')
|
||||
.get('.cm-content')
|
||||
.should('contain.text', 'SELECT * FROM workflows');
|
||||
ndv.actions.close();
|
||||
|
||||
// HTML
|
||||
workflowPage.actions.addNodeToCanvas('HTML', true, true, 'Generate HTML template');
|
||||
ndv.actions.openCodeEditorFullscreen();
|
||||
|
||||
ndv.getters
|
||||
.codeEditorFullscreen()
|
||||
.type('{selectall}')
|
||||
.type('{backspace}')
|
||||
.type('<div>Hello World');
|
||||
ndv.getters.codeEditorFullscreen().should('contain.text', '<div>Hello World</div>');
|
||||
cy.wait(200);
|
||||
|
||||
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
|
||||
ndv.getters
|
||||
.parameterInput('html')
|
||||
.get('.cm-content')
|
||||
.should('contain.text', '<div>Hello World</div>');
|
||||
ndv.actions.close();
|
||||
|
||||
// JSON
|
||||
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
|
||||
setParameterSelectByContent('mode', 'JSON');
|
||||
ndv.actions.openCodeEditorFullscreen();
|
||||
ndv.getters
|
||||
.codeEditorFullscreen()
|
||||
.type('{selectall}')
|
||||
.type('{backspace}')
|
||||
.type('{ "key": "value" }', { parseSpecialCharSequences: false });
|
||||
ndv.getters.codeEditorFullscreen().should('contain.text', '{ "key": "value" }');
|
||||
cy.wait(200);
|
||||
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
|
||||
ndv.getters
|
||||
.parameterInput('jsonOutput')
|
||||
.get('.cm-content')
|
||||
.should('contain.text', '{ "key": "value" }');
|
||||
});
|
||||
|
||||
it('should not retrieve remote options when a parameter value changes', () => {
|
||||
|
|
|
@ -7,6 +7,9 @@ import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
|||
const WorkflowPage = new WorkflowPageClass();
|
||||
const ndv = new NDV();
|
||||
|
||||
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
|
||||
const getEditor = () => getParameter().find('.cm-content').should('exist');
|
||||
|
||||
describe('Code node', () => {
|
||||
describe('Code editor', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -40,10 +43,23 @@ describe('Code node', () => {
|
|||
successToast().contains('Node executed successfully');
|
||||
});
|
||||
|
||||
it('should show lint errors in `runOnceForAllItems` mode', () => {
|
||||
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
|
||||
const getEditor = () => getParameter().find('.cm-content').should('exist');
|
||||
it('should allow switching between sibling code nodes', () => {
|
||||
// Setup
|
||||
getEditor().type('{selectall}').paste("console.log('code node 1')");
|
||||
ndv.actions.close();
|
||||
WorkflowPage.actions.addNodeToCanvas('Code', true, true);
|
||||
getEditor().type('{selectall}').paste("console.log('code node 2')");
|
||||
ndv.actions.close();
|
||||
|
||||
WorkflowPage.actions.openNode('Code');
|
||||
ndv.actions.clickFloatingNode('Code1');
|
||||
getEditor().should('have.text', "console.log('code node 2')");
|
||||
|
||||
ndv.actions.clickFloatingNode('Code');
|
||||
getEditor().should('have.text', "console.log('code node 1')");
|
||||
});
|
||||
|
||||
it('should show lint errors in `runOnceForAllItems` mode', () => {
|
||||
getEditor()
|
||||
.type('{selectall}')
|
||||
.paste(`$input.itemMatching()
|
||||
|
@ -66,9 +82,6 @@ return
|
|||
});
|
||||
|
||||
it('should show lint errors in `runOnceForEachItem` mode', () => {
|
||||
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
|
||||
const getEditor = () => getParameter().find('.cm-content').should('exist');
|
||||
|
||||
ndv.getters.parameterInput('mode').click();
|
||||
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
|
||||
getEditor()
|
||||
|
|
|
@ -200,7 +200,14 @@ describe('Workflow Actions', () => {
|
|||
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');
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
cy.wrap(node).should('have.attr', 'data-name');
|
||||
},
|
||||
() => {
|
||||
cy.wrap(node).should('have.attr', 'data-node-name');
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -105,7 +105,7 @@ describe('Expression editor modal', () => {
|
|||
// Run workflow
|
||||
cy.get('body').type('{esc}');
|
||||
ndv.actions.close();
|
||||
WorkflowPage.actions.executeNode('No Operation');
|
||||
WorkflowPage.actions.executeNode('No Operation, do nothing', { anchor: 'topLeft' });
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openExpressionEditorModal();
|
||||
|
||||
|
|
77
cypress/fixtures/Test_workflow_chat_partial_execution.json
Normal file
77
cypress/fixtures/Test_workflow_chat_partial_execution.json
Normal file
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "535fd3dd-e78f-4ffa-a085-79723fc81b38",
|
||||
"name": "When chat message received",
|
||||
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [
|
||||
320,
|
||||
-380
|
||||
],
|
||||
"webhookId": "4fb58136-3481-494a-a30f-d9e064dac186"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "raw",
|
||||
"jsonOutput": "{\n \"this_my_field_1\": \"value\",\n \"this_my_field_2\": 1\n}\n",
|
||||
"options": {}
|
||||
},
|
||||
"id": "78201ec2-6def-40b7-85e5-97b580d7f642",
|
||||
"name": "Node 1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
580,
|
||||
-380
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "raw",
|
||||
"jsonOutput": "{\n \"this_my_field_3\": \"value\",\n \"this_my_field_4\": 1\n}\n",
|
||||
"options": {}
|
||||
},
|
||||
"id": "1cfca06d-3ec3-427f-89f7-1ef321e025ff",
|
||||
"name": "Node 2",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
780,
|
||||
-380
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When chat message received": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Node 1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Node 1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Node 2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "178ef8a5109fc76c716d40bcadb720c455319f7b7a3fd5a39e4f336a091f524a"
|
||||
}
|
||||
}
|
76
cypress/fixtures/Two_schedule_triggers.json
Normal file
76
cypress/fixtures/Two_schedule_triggers.json
Normal file
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "6a8c3d85-26f8-4f28-ace9-55a196a23d37",
|
||||
"name": "prevNode",
|
||||
"value": "={{ $prevNode.name }}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [200, -100],
|
||||
"id": "351ce967-0399-4a78-848a-9cc69b831796",
|
||||
"name": "Edit Fields"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [{}]
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [0, -100],
|
||||
"id": "cf2f58a8-1fbb-4c70-b2b1-9e06bee7ec47",
|
||||
"name": "Trigger A"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [{}]
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [0, 100],
|
||||
"id": "4fade34e-2bfc-4a2e-a8ed-03ab2ed9c690",
|
||||
"name": "Trigger B"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Trigger A": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Trigger B": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "0dd4627b77a5a795ab9bf073e5812be94dd8d1a5f012248ef2a4acac09be12cb"
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
"cypress:install": "cypress install",
|
||||
"test:e2e:ui": "scripts/run-e2e.js ui",
|
||||
"test:e2e:dev": "scripts/run-e2e.js dev",
|
||||
"test:e2e:dev:v2": "scripts/run-e2e.js dev:v2",
|
||||
"test:e2e:dev:v1": "scripts/run-e2e.js dev:v1",
|
||||
"test:e2e:all": "scripts/run-e2e.js all",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
|
|
|
@ -151,6 +151,9 @@ export class NDV extends BasePage {
|
|||
schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'),
|
||||
expressionExpanders: () => cy.getByTestId('expander'),
|
||||
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
||||
floatingNodes: () => cy.getByTestId('floating-node'),
|
||||
floatingNodeByName: (name: string) =>
|
||||
cy.getByTestId('floating-node').filter(`[data-node-name="${name}"]`),
|
||||
};
|
||||
|
||||
actions = {
|
||||
|
@ -339,6 +342,9 @@ export class NDV extends BasePage {
|
|||
dragMainPanelToRight: () => {
|
||||
cy.drag('[data-test-id=panel-drag-button]', [1000, 0], { moveTwice: true });
|
||||
},
|
||||
clickFloatingNode: (name: string) => {
|
||||
this.getters.floatingNodeByName(name).realHover().click({ force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,10 @@ export class VariablesPage extends BasePage {
|
|||
},
|
||||
setRowValue: (row: Chainable<JQuery<HTMLElement>>, field: 'key' | 'value', value: string) => {
|
||||
row.within(() => {
|
||||
cy.getByTestId(`variable-row-${field}-input`).type('{selectAll}{del}').type(value);
|
||||
cy.getByTestId(`variable-row-${field}-input`)
|
||||
.find('input, textarea')
|
||||
.type('{selectAll}{del}')
|
||||
.type(value);
|
||||
});
|
||||
},
|
||||
cancelRowEditing: (row: Chainable<JQuery<HTMLElement>>) => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { BasePage } from './base';
|
||||
import { NodeCreator } from './features/node-creator';
|
||||
import { clickContextMenuAction, getCanvasPane, openContextMenu } from '../composables/workflow';
|
||||
import { META_KEY } from '../constants';
|
||||
import type { OpenContextMenuOptions } from '../types';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
@ -38,15 +39,7 @@ export class WorkflowPage extends BasePage {
|
|||
nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'),
|
||||
nodeCreatorPlusButton: () => cy.getByTestId('node-creator-plus-button'),
|
||||
canvasPlusButton: () => cy.getByTestId('canvas-plus-button'),
|
||||
canvasNodes: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('canvas-node'),
|
||||
() =>
|
||||
cy
|
||||
.getByTestId('canvas-node')
|
||||
.not('[data-node-type="n8n-nodes-internal.addNodes"]')
|
||||
.not('[data-node-type="n8n-nodes-base.stickyNote"]'),
|
||||
),
|
||||
canvasNodes: () => cy.getByTestId('canvas-node'),
|
||||
canvasNodeByName: (nodeName: string) =>
|
||||
this.getters.canvasNodes().filter(`:contains(${nodeName})`),
|
||||
nodeIssuesByName: (nodeName: string) =>
|
||||
|
@ -58,13 +51,13 @@ export class WorkflowPage extends BasePage {
|
|||
getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => {
|
||||
if (isCanvasV2()) {
|
||||
if (type === 'input') {
|
||||
return `[data-test-id="canvas-node-input-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`;
|
||||
return `[data-test-id="canvas-node-input-handle"][data-node-name="${nodeName}"][data-index="${index}"]`;
|
||||
}
|
||||
if (type === 'output') {
|
||||
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`;
|
||||
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-index="${index}"]`;
|
||||
}
|
||||
if (type === 'plus') {
|
||||
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"] [data-test-id="canvas-handle-plus"] .clickable`;
|
||||
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-index="${index}"] [data-test-id="canvas-handle-plus"]`;
|
||||
}
|
||||
}
|
||||
return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`;
|
||||
|
@ -81,7 +74,7 @@ export class WorkflowPage extends BasePage {
|
|||
() =>
|
||||
cy
|
||||
.get(
|
||||
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"] .clickable`,
|
||||
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`,
|
||||
)
|
||||
.eq(index),
|
||||
);
|
||||
|
@ -103,14 +96,14 @@ export class WorkflowPage extends BasePage {
|
|||
nodeConnections: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.jtk-connector'),
|
||||
() => cy.getByTestId('edge-label-wrapper'),
|
||||
() => cy.getByTestId('edge'),
|
||||
),
|
||||
zoomToFitButton: () => cy.getByTestId('zoom-to-fit'),
|
||||
nodeEndpoints: () => cy.get('.jtk-endpoint-connected'),
|
||||
disabledNodes: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.node-box.disabled'),
|
||||
() => cy.get('[data-test-id*="node"][class*="disabled"]'),
|
||||
() => cy.get('[data-canvas-node-render-type][class*="disabled"]'),
|
||||
),
|
||||
selectedNodes: () =>
|
||||
cy.ifCanvasVersion(
|
||||
|
@ -189,7 +182,7 @@ export class WorkflowPage extends BasePage {
|
|||
),
|
||||
() =>
|
||||
cy.get(
|
||||
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`,
|
||||
`[data-test-id="edge"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`,
|
||||
),
|
||||
),
|
||||
getConnectionActionsBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
|
||||
|
@ -200,7 +193,7 @@ export class WorkflowPage extends BasePage {
|
|||
),
|
||||
() =>
|
||||
cy.get(
|
||||
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`,
|
||||
`[data-test-id="edge-label"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`,
|
||||
),
|
||||
),
|
||||
addStickyButton: () => cy.getByTestId('add-sticky-button'),
|
||||
|
@ -288,71 +281,77 @@ export class WorkflowPage extends BasePage {
|
|||
nodeTypeName?: string,
|
||||
{ method = 'right-click', anchor = 'center' }: OpenContextMenuOptions = {},
|
||||
) => {
|
||||
const target = nodeTypeName
|
||||
? this.getters.canvasNodeByName(nodeTypeName)
|
||||
: this.getters.nodeViewBackground();
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
const target = nodeTypeName
|
||||
? this.getters.canvasNodeByName(nodeTypeName)
|
||||
: this.getters.nodeViewBackground();
|
||||
|
||||
if (method === 'right-click') {
|
||||
target.rightclick(nodeTypeName ? anchor : 'topLeft', { force: true });
|
||||
} else {
|
||||
target.realHover();
|
||||
target.find('[data-test-id="overflow-node-button"]').click({ force: true });
|
||||
}
|
||||
if (method === 'right-click') {
|
||||
target.rightclick(nodeTypeName ? anchor : 'topLeft', { force: true });
|
||||
} else {
|
||||
target.realHover();
|
||||
target.find('[data-test-id="overflow-node-button"]').click({ force: true });
|
||||
}
|
||||
},
|
||||
() => {
|
||||
openContextMenu(nodeTypeName, { method, anchor });
|
||||
},
|
||||
);
|
||||
},
|
||||
openNode: (nodeTypeName: string) => {
|
||||
this.getters.canvasNodeByName(nodeTypeName).first().dblclick();
|
||||
},
|
||||
duplicateNode: (nodeTypeName: string) => {
|
||||
this.actions.openContextMenu(nodeTypeName);
|
||||
this.actions.contextMenuAction('duplicate');
|
||||
clickContextMenuAction('duplicate');
|
||||
},
|
||||
deleteNodeFromContextMenu: (nodeTypeName: string) => {
|
||||
this.actions.openContextMenu(nodeTypeName);
|
||||
this.actions.contextMenuAction('delete');
|
||||
clickContextMenuAction('delete');
|
||||
},
|
||||
executeNode: (nodeTypeName: string, options?: OpenContextMenuOptions) => {
|
||||
this.actions.openContextMenu(nodeTypeName, options);
|
||||
this.actions.contextMenuAction('execute');
|
||||
clickContextMenuAction('execute');
|
||||
},
|
||||
addStickyFromContextMenu: () => {
|
||||
this.actions.openContextMenu();
|
||||
this.actions.contextMenuAction('add_sticky');
|
||||
clickContextMenuAction('add_sticky');
|
||||
},
|
||||
renameNode: (nodeTypeName: string) => {
|
||||
this.actions.openContextMenu(nodeTypeName);
|
||||
this.actions.contextMenuAction('rename');
|
||||
clickContextMenuAction('rename');
|
||||
},
|
||||
copyNode: (nodeTypeName: string) => {
|
||||
this.actions.openContextMenu(nodeTypeName);
|
||||
this.actions.contextMenuAction('copy');
|
||||
clickContextMenuAction('copy');
|
||||
},
|
||||
contextMenuAction: (action: string) => {
|
||||
this.getters.contextMenuAction(action).click();
|
||||
},
|
||||
disableNode: (nodeTypeName: string) => {
|
||||
this.actions.openContextMenu(nodeTypeName);
|
||||
this.actions.contextMenuAction('toggle_activation');
|
||||
clickContextMenuAction('toggle_activation');
|
||||
},
|
||||
pinNode: (nodeTypeName: string) => {
|
||||
this.actions.openContextMenu(nodeTypeName);
|
||||
this.actions.contextMenuAction('toggle_pin');
|
||||
clickContextMenuAction('toggle_pin');
|
||||
},
|
||||
openNodeFromContextMenu: (nodeTypeName: string) => {
|
||||
this.actions.openContextMenu(nodeTypeName, { method: 'overflow-button' });
|
||||
this.actions.contextMenuAction('open');
|
||||
clickContextMenuAction('open');
|
||||
},
|
||||
selectAllFromContextMenu: () => {
|
||||
this.actions.openContextMenu();
|
||||
this.actions.contextMenuAction('select_all');
|
||||
clickContextMenuAction('select_all');
|
||||
},
|
||||
deselectAll: () => {
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
this.actions.openContextMenu();
|
||||
this.actions.contextMenuAction('deselect_all');
|
||||
clickContextMenuAction('deselect_all');
|
||||
},
|
||||
// rightclick doesn't work with vueFlow canvas
|
||||
() => this.getters.nodeViewBackground().click('topLeft'),
|
||||
() => getCanvasPane().click('topLeft'),
|
||||
);
|
||||
},
|
||||
openExpressionEditorModal: () => {
|
||||
|
@ -431,7 +430,7 @@ export class WorkflowPage extends BasePage {
|
|||
pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => {
|
||||
cy.window().then((win) => {
|
||||
// Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling)
|
||||
this.getters.nodeView().trigger('wheel', {
|
||||
getCanvasPane().trigger('wheel', {
|
||||
force: true,
|
||||
bubbles: true,
|
||||
ctrlKey: true,
|
||||
|
|
|
@ -45,19 +45,23 @@ switch (scenario) {
|
|||
startCommand: 'start',
|
||||
url: 'http://localhost:5678/favicon.ico',
|
||||
testCommand: 'cypress open',
|
||||
customEnv: {
|
||||
CYPRESS_NODE_VIEW_VERSION: 2,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'dev':
|
||||
case 'dev:v1':
|
||||
runTests({
|
||||
startCommand: 'develop',
|
||||
url: 'http://localhost:8080/favicon.ico',
|
||||
testCommand: 'cypress open',
|
||||
customEnv: {
|
||||
CYPRESS_NODE_VIEW_VERSION: 1,
|
||||
CYPRESS_BASE_URL: 'http://localhost:8080',
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'dev:v2':
|
||||
case 'dev':
|
||||
runTests({
|
||||
startCommand: 'develop',
|
||||
url: 'http://localhost:8080/favicon.ico',
|
||||
|
@ -76,6 +80,9 @@ switch (scenario) {
|
|||
startCommand: 'start',
|
||||
url: 'http://localhost:5678/favicon.ico',
|
||||
testCommand: `cypress run --headless ${specParam}`,
|
||||
customEnv: {
|
||||
CYPRESS_NODE_VIEW_VERSION: 2,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -77,7 +77,7 @@ Cypress.Commands.add('signin', ({ email, password }) => {
|
|||
|
||||
// @TODO Remove this once the switcher is removed
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('NodeView.migrated', 'true');
|
||||
win.localStorage.setItem('NodeView.migrated.release', 'true');
|
||||
win.localStorage.setItem('NodeView.switcher.discovered.beta', 'true');
|
||||
|
||||
const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION');
|
||||
|
@ -172,6 +172,7 @@ Cypress.Commands.add('drag', (selector, pos, options) => {
|
|||
};
|
||||
if (options?.realMouse) {
|
||||
element.realMouseDown();
|
||||
element.realMouseMove(0, 0);
|
||||
element.realMouseMove(newPosition.x, newPosition.y);
|
||||
element.realMouseUp();
|
||||
} else {
|
||||
|
@ -218,8 +219,15 @@ Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector, optio
|
|||
const pageY = coords.top + coords.height / 2;
|
||||
|
||||
if (draggableSelector) {
|
||||
// We can't use realMouseDown here because it hangs headless run
|
||||
cy.get(draggableSelector).trigger('mousedown');
|
||||
cy.ifCanvasVersion(
|
||||
() => {
|
||||
// We can't use realMouseDown here because it hangs headless run
|
||||
cy.get(draggableSelector).trigger('mousedown');
|
||||
},
|
||||
() => {
|
||||
cy.get(draggableSelector).realMouseDown();
|
||||
},
|
||||
);
|
||||
}
|
||||
// We don't chain these commands to make sure cy.get is re-trying correctly
|
||||
cy.get(droppableSelector).realMouseMove(0, 0);
|
||||
|
|
|
@ -38,7 +38,21 @@ beforeEach(() => {
|
|||
data: { status: 'success', message: 'Tested successfully' },
|
||||
}).as('credentialTest');
|
||||
|
||||
cy.intercept('POST', '/rest/license/renew', {});
|
||||
cy.intercept('POST', '/rest/license/renew', {
|
||||
data: {
|
||||
usage: {
|
||||
activeWorkflowTriggers: {
|
||||
limit: -1,
|
||||
value: 0,
|
||||
warningThreshold: 0.8,
|
||||
},
|
||||
},
|
||||
license: {
|
||||
planId: '',
|
||||
planName: 'Community',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cy.intercept({ pathname: '/api/health' }, { status: 'OK' }).as('healthCheck');
|
||||
cy.intercept({ pathname: '/api/versions/*' }, [
|
||||
|
|
|
@ -16,7 +16,7 @@ RUN apk add --update git openssh graphicsmagick tini tzdata ca-certificates libc
|
|||
|
||||
# Update npm and install full-uci
|
||||
COPY .npmrc /usr/local/etc/npmrc
|
||||
RUN npm install -g npm@9.9.2 full-icu@1.5.0
|
||||
RUN npm install -g npm@9.9.2 corepack@0.31 full-icu@1.5.0
|
||||
|
||||
# Activate corepack, and install pnpm
|
||||
WORKDIR /tmp
|
||||
|
|
|
@ -73,7 +73,7 @@ docker run -it --rm \
|
|||
-p 5678:5678 \
|
||||
-v ~/.n8n:/home/node/.n8n \
|
||||
docker.n8n.io/n8nio/n8n \
|
||||
n8n start --tunnel
|
||||
start --tunnel
|
||||
```
|
||||
|
||||
## Persist data
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
"runner-type": "javascript",
|
||||
"workdir": "/home/node",
|
||||
"command": "/usr/local/bin/node",
|
||||
"args": ["/usr/local/lib/node_modules/n8n/node_modules/@n8n/task-runner/dist/start.js"],
|
||||
"args": [
|
||||
"--disallow-code-generation-from-strings",
|
||||
"--disable-proto=delete",
|
||||
"/usr/local/lib/node_modules/n8n/node_modules/@n8n/task-runner/dist/start.js"
|
||||
],
|
||||
"allowed-env": [
|
||||
"PATH",
|
||||
"GENERIC_TIMEZONE",
|
||||
|
|
|
@ -2,14 +2,14 @@ pre-commit:
|
|||
commands:
|
||||
biome_check:
|
||||
glob: 'packages/**/*.{js,ts,json}'
|
||||
run: ./node_modules/.bin/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
||||
run: pnpm biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
||||
stage_fixed: true
|
||||
skip:
|
||||
- merge
|
||||
- rebase
|
||||
prettier_check:
|
||||
glob: 'packages/**/*.{vue,yml,md,css,scss}'
|
||||
run: ./node_modules/.bin/prettier --write --ignore-unknown --no-error-on-unmatched-pattern {staged_files}
|
||||
run: pnpm prettier --write --ignore-unknown --no-error-on-unmatched-pattern {staged_files}
|
||||
stage_fixed: true
|
||||
skip:
|
||||
- merge
|
||||
|
|
10
package.json
10
package.json
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.74.1",
|
||||
"version": "1.78.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.15",
|
||||
"pnpm": ">=9.15"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.1",
|
||||
"packageManager": "pnpm@9.15.5",
|
||||
"scripts": {
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
"preinstall": "node scripts/block-npm-install.js",
|
||||
|
@ -21,7 +21,7 @@
|
|||
"dev:fe": "run-p start \"dev:fe:editor --filter=n8n-design-system\"",
|
||||
"dev:fe:editor": "turbo run dev --parallel --env-mode=loose --filter=n8n-editor-ui",
|
||||
"dev:e2e": "cd cypress && pnpm run test:e2e:dev",
|
||||
"dev:e2e:v2": "cd cypress && pnpm run test:e2e:dev:v2",
|
||||
"dev:e2e:v1": "cd cypress && pnpm run test:e2e:dev:v1",
|
||||
"dev:e2e:server": "run-p start dev:fe:editor",
|
||||
"clean": "turbo run clean --parallel",
|
||||
"reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs",
|
||||
|
@ -58,8 +58,7 @@
|
|||
"jest-mock": "^29.6.2",
|
||||
"jest-mock-extended": "^3.0.4",
|
||||
"lefthook": "^1.7.15",
|
||||
"loader": "^2.1.1",
|
||||
"nock": "^13.3.2",
|
||||
"nock": "^14.0.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"p-limit": "^3.1.0",
|
||||
|
@ -91,6 +90,7 @@
|
|||
"ws": ">=8.17.1"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"bull@4.12.1": "patches/bull@4.12.1.patch",
|
||||
"pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch",
|
||||
"pyodide@0.23.4": "patches/pyodide@0.23.4.patch",
|
||||
"@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/api-types",
|
||||
"version": "0.12.0",
|
||||
"version": "0.14.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
14
packages/@n8n/api-types/src/api-keys.ts
Normal file
14
packages/@n8n/api-types/src/api-keys.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/** Unix timestamp. Seconds since epoch */
|
||||
export type UnixTimestamp = number | null;
|
||||
|
||||
export type ApiKey = {
|
||||
id: string;
|
||||
label: string;
|
||||
apiKey: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
/** Null if API key never expires */
|
||||
expiresAt: UnixTimestamp | null;
|
||||
};
|
||||
|
||||
export type ApiKeyWithRawValue = ApiKey & { rawApiKey: string };
|
|
@ -0,0 +1,53 @@
|
|||
import { CreateApiKeyRequestDto } from '../create-api-key-request.dto';
|
||||
|
||||
describe('CreateApiKeyRequestDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'expiresAt in the future',
|
||||
expiresAt: Date.now() / 1000 + 1000,
|
||||
},
|
||||
{
|
||||
name: 'expiresAt null',
|
||||
expiresAt: null,
|
||||
},
|
||||
])('should succeed validation for $name', ({ expiresAt }) => {
|
||||
const result = CreateApiKeyRequestDto.safeParse({ label: 'valid', expiresAt });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'expiresAt in the past',
|
||||
expiresAt: Date.now() / 1000 - 1000,
|
||||
expectedErrorPath: ['expiresAt'],
|
||||
},
|
||||
{
|
||||
name: 'expiresAt with string',
|
||||
expiresAt: 'invalid',
|
||||
expectedErrorPath: ['expiresAt'],
|
||||
},
|
||||
{
|
||||
name: 'expiresAt with []',
|
||||
expiresAt: [],
|
||||
expectedErrorPath: ['expiresAt'],
|
||||
},
|
||||
{
|
||||
name: 'expiresAt with {}',
|
||||
expiresAt: {},
|
||||
expectedErrorPath: ['expiresAt'],
|
||||
},
|
||||
])('should fail validation for $name', ({ expiresAt, expectedErrorPath }) => {
|
||||
const result = CreateApiKeyRequestDto.safeParse({ label: 'valid', expiresAt });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import { UpdateApiKeyRequestDto } from '../update-api-key-request.dto';
|
||||
|
||||
describe('UpdateApiKeyRequestDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test('should allow valid label', () => {
|
||||
const result = UpdateApiKeyRequestDto.safeParse({
|
||||
label: 'valid label',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'empty label',
|
||||
label: '',
|
||||
expectedErrorPath: ['label'],
|
||||
},
|
||||
{
|
||||
name: 'label exceeding 50 characters',
|
||||
label: '2mWMfsrvAmneWluS8IbezaIHZOu2mWMfsrvAmneWluS8IbezaIa',
|
||||
expectedErrorPath: ['label'],
|
||||
},
|
||||
{
|
||||
name: 'label with xss injection',
|
||||
label: '<script>alert("xss");new label</script>',
|
||||
expectedErrorPath: ['label'],
|
||||
},
|
||||
])('should fail validation for $name', ({ label, expectedErrorPath }) => {
|
||||
const result = UpdateApiKeyRequestDto.safeParse({ label });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { UpdateApiKeyRequestDto } from './update-api-key-request.dto';
|
||||
|
||||
const isTimeNullOrInFuture = (value: number | null) => {
|
||||
if (!value) return true;
|
||||
return value > Date.now() / 1000;
|
||||
};
|
||||
|
||||
export class CreateApiKeyRequestDto extends UpdateApiKeyRequestDto.extend({
|
||||
expiresAt: z
|
||||
.number()
|
||||
.nullable()
|
||||
.refine(isTimeNullOrInFuture, { message: 'Expiration date must be in the future or null' }),
|
||||
}) {}
|
|
@ -0,0 +1,13 @@
|
|||
import xss from 'xss';
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
const xssCheck = (value: string) =>
|
||||
value ===
|
||||
xss(value, {
|
||||
whiteList: {},
|
||||
});
|
||||
|
||||
export class UpdateApiKeyRequestDto extends Z.class({
|
||||
label: z.string().max(50).min(1).refine(xssCheck),
|
||||
}) {}
|
|
@ -21,6 +21,10 @@ export { ForgotPasswordRequestDto } from './password-reset/forgot-password-reque
|
|||
export { ResolvePasswordTokenQueryDto } from './password-reset/resolve-password-token-query.dto';
|
||||
export { ChangePasswordRequestDto } from './password-reset/change-password-request.dto';
|
||||
|
||||
export { CreateProjectDto } from './project/create-project.dto';
|
||||
export { UpdateProjectDto } from './project/update-project.dto';
|
||||
export { DeleteProjectDto } from './project/delete-project.dto';
|
||||
|
||||
export { SamlAcsDto } from './saml/saml-acs.dto';
|
||||
export { SamlPreferences } from './saml/saml-preferences.dto';
|
||||
export { SamlToggleDto } from './saml/saml-toggle.dto';
|
||||
|
@ -32,8 +36,18 @@ export { UserUpdateRequestDto } from './user/user-update-request.dto';
|
|||
|
||||
export { CommunityRegisteredRequestDto } from './license/community-registered-request.dto';
|
||||
|
||||
export { PullWorkFolderRequestDto } from './source-control/pull-work-folder-request.dto';
|
||||
export { PushWorkFolderRequestDto } from './source-control/push-work-folder-request.dto';
|
||||
|
||||
export { VariableListRequestDto } from './variables/variables-list-request.dto';
|
||||
export { CredentialsGetOneRequestQuery } from './credentials/credentials-get-one-request.dto';
|
||||
export { CredentialsGetManyRequestQuery } from './credentials/credentials-get-many-request.dto';
|
||||
|
||||
export { ImportWorkflowFromUrlDto } from './workflows/import-workflow-from-url.dto';
|
||||
export { ManualRunQueryDto } from './workflows/manual-run-query.dto';
|
||||
|
||||
export { CreateOrUpdateTagRequestDto } from './tag/create-or-update-tag-request.dto';
|
||||
export { RetrieveTagQueryDto } from './tag/retrieve-tag-query.dto';
|
||||
|
||||
export { UpdateApiKeyRequestDto } from './api-keys/update-api-key-request.dto';
|
||||
export { CreateApiKeyRequestDto } from './api-keys/create-api-key-request.dto';
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import { CreateProjectDto } from '../create-project.dto';
|
||||
|
||||
describe('CreateProjectDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'with just the name',
|
||||
request: {
|
||||
name: 'My Awesome Project',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with name and emoji icon',
|
||||
request: {
|
||||
name: 'My Awesome Project',
|
||||
icon: {
|
||||
type: 'emoji',
|
||||
value: '🚀',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with name and regular icon',
|
||||
request: {
|
||||
name: 'My Awesome Project',
|
||||
icon: {
|
||||
type: 'icon',
|
||||
value: 'blah',
|
||||
},
|
||||
},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = CreateProjectDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'missing name',
|
||||
request: { icon: { type: 'emoji', value: '🚀' } },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'empty name',
|
||||
request: { name: '', icon: { type: 'emoji', value: '🚀' } },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'name too long',
|
||||
request: { name: 'a'.repeat(256), icon: { type: 'emoji', value: '🚀' } },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'invalid icon type',
|
||||
request: { name: 'My Awesome Project', icon: { type: 'invalid', value: '🚀' } },
|
||||
expectedErrorPath: ['icon', 'type'],
|
||||
},
|
||||
{
|
||||
name: 'invalid icon value',
|
||||
request: { name: 'My Awesome Project', icon: { type: 'emoji', value: '' } },
|
||||
expectedErrorPath: ['icon', 'value'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = CreateProjectDto.safeParse(request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,121 @@
|
|||
import { UpdateProjectDto } from '../update-project.dto';
|
||||
|
||||
describe('UpdateProjectDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'with just the name',
|
||||
request: {
|
||||
name: 'My Updated Project',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with name and emoji icon',
|
||||
request: {
|
||||
name: 'My Updated Project',
|
||||
icon: {
|
||||
type: 'emoji',
|
||||
value: '🚀',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with name and regular icon',
|
||||
request: {
|
||||
name: 'My Updated Project',
|
||||
icon: {
|
||||
type: 'icon',
|
||||
value: 'blah',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with relations',
|
||||
request: {
|
||||
relations: [
|
||||
{
|
||||
userId: 'user-123',
|
||||
role: 'project:admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with all fields',
|
||||
request: {
|
||||
name: 'My Updated Project',
|
||||
icon: {
|
||||
type: 'emoji',
|
||||
value: '🚀',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
userId: 'user-123',
|
||||
role: 'project:admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = UpdateProjectDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'invalid name type',
|
||||
request: { name: 123 },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'name too long',
|
||||
request: { name: 'a'.repeat(256) },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'invalid icon type',
|
||||
request: { icon: { type: 'invalid', value: '🚀' } },
|
||||
expectedErrorPath: ['icon', 'type'],
|
||||
},
|
||||
{
|
||||
name: 'invalid icon value',
|
||||
request: { icon: { type: 'emoji', value: '' } },
|
||||
expectedErrorPath: ['icon', 'value'],
|
||||
},
|
||||
{
|
||||
name: 'invalid relations userId',
|
||||
request: {
|
||||
relations: [
|
||||
{
|
||||
userId: 123,
|
||||
role: 'project:admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedErrorPath: ['relations', 0, 'userId'],
|
||||
},
|
||||
{
|
||||
name: 'invalid relations role',
|
||||
request: {
|
||||
relations: [
|
||||
{
|
||||
userId: 'user-123',
|
||||
role: 'invalid-role',
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedErrorPath: ['relations', 0, 'role'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = UpdateProjectDto.safeParse(request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import { Z } from 'zod-class';
|
||||
|
||||
import { projectIconSchema, projectNameSchema } from '../../schemas/project.schema';
|
||||
|
||||
export class CreateProjectDto extends Z.class({
|
||||
name: projectNameSchema,
|
||||
icon: projectIconSchema.optional(),
|
||||
}) {}
|
|
@ -0,0 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class DeleteProjectDto extends Z.class({
|
||||
transferId: z.string().optional(),
|
||||
}) {}
|
|
@ -0,0 +1,14 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import {
|
||||
projectIconSchema,
|
||||
projectNameSchema,
|
||||
projectRelationSchema,
|
||||
} from '../../schemas/project.schema';
|
||||
|
||||
export class UpdateProjectDto extends Z.class({
|
||||
name: projectNameSchema.optional(),
|
||||
icon: projectIconSchema.optional(),
|
||||
relations: z.array(projectRelationSchema).optional(),
|
||||
}) {}
|
|
@ -0,0 +1,38 @@
|
|||
import { PullWorkFolderRequestDto } from '../pull-work-folder-request.dto';
|
||||
|
||||
describe('PullWorkFolderRequestDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'with force',
|
||||
request: { force: true },
|
||||
},
|
||||
{
|
||||
name: 'without force',
|
||||
request: {},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = PullWorkFolderRequestDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'invalid force type',
|
||||
request: {
|
||||
force: 'true', // Should be boolean
|
||||
},
|
||||
expectedErrorPath: ['force'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = PullWorkFolderRequestDto.safeParse(request);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,112 @@
|
|||
import { PushWorkFolderRequestDto } from '../push-work-folder-request.dto';
|
||||
|
||||
describe('PushWorkFolderRequestDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'complete valid push request with all fields',
|
||||
request: {
|
||||
force: true,
|
||||
fileNames: [
|
||||
{
|
||||
file: 'file1.json',
|
||||
id: '1',
|
||||
name: 'File 1',
|
||||
type: 'workflow',
|
||||
status: 'modified',
|
||||
location: 'local',
|
||||
conflict: false,
|
||||
updatedAt: '2023-10-01T12:00:00Z',
|
||||
pushed: true,
|
||||
},
|
||||
],
|
||||
message: 'Initial commit',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'push request with only required fields',
|
||||
request: {
|
||||
fileNames: [
|
||||
{
|
||||
file: 'file2.json',
|
||||
id: '2',
|
||||
name: 'File 2',
|
||||
type: 'credential',
|
||||
status: 'new',
|
||||
location: 'remote',
|
||||
conflict: true,
|
||||
updatedAt: '2023-10-02T12:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = PushWorkFolderRequestDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'missing required fileNames field',
|
||||
request: {
|
||||
force: true,
|
||||
message: 'Initial commit',
|
||||
},
|
||||
expectedErrorPath: ['fileNames'],
|
||||
},
|
||||
{
|
||||
name: 'invalid fileNames type',
|
||||
request: {
|
||||
fileNames: 'not-an-array', // Should be an array
|
||||
},
|
||||
expectedErrorPath: ['fileNames'],
|
||||
},
|
||||
{
|
||||
name: 'invalid fileNames array element',
|
||||
request: {
|
||||
fileNames: [
|
||||
{
|
||||
file: 'file4.json',
|
||||
id: '4',
|
||||
name: 'File 4',
|
||||
type: 'invalid-type', // Invalid type
|
||||
status: 'modified',
|
||||
location: 'local',
|
||||
conflict: false,
|
||||
updatedAt: '2023-10-04T12:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedErrorPath: ['fileNames', 0, 'type'],
|
||||
},
|
||||
{
|
||||
name: 'invalid force type',
|
||||
request: {
|
||||
force: 'true', // Should be boolean
|
||||
fileNames: [
|
||||
{
|
||||
file: 'file5.json',
|
||||
id: '5',
|
||||
name: 'File 5',
|
||||
type: 'workflow',
|
||||
status: 'modified',
|
||||
location: 'local',
|
||||
conflict: false,
|
||||
updatedAt: '2023-10-05T12:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedErrorPath: ['force'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = PushWorkFolderRequestDto.safeParse(request);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class PullWorkFolderRequestDto extends Z.class({
|
||||
force: z.boolean().optional(),
|
||||
}) {}
|
|
@ -0,0 +1,10 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { SourceControlledFileSchema } from '../../schemas/source-controlled-file.schema';
|
||||
|
||||
export class PushWorkFolderRequestDto extends Z.class({
|
||||
force: z.boolean().optional(),
|
||||
commitMessage: z.string().optional(),
|
||||
fileNames: z.array(SourceControlledFileSchema),
|
||||
}) {}
|
|
@ -0,0 +1,37 @@
|
|||
import { CreateOrUpdateTagRequestDto } from '../create-or-update-tag-request.dto';
|
||||
|
||||
describe('CreateOrUpdateTagRequestDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'valid name',
|
||||
request: {
|
||||
name: 'tag-name',
|
||||
},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = CreateOrUpdateTagRequestDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'empty tag name',
|
||||
request: {
|
||||
name: '',
|
||||
},
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = CreateOrUpdateTagRequestDto.safeParse(request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
import { RetrieveTagQueryDto } from '../retrieve-tag-query.dto';
|
||||
|
||||
describe('RetrieveTagQueryDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'with "true"',
|
||||
request: {
|
||||
withUsageCount: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with "false"',
|
||||
request: {
|
||||
withUsageCount: 'false',
|
||||
},
|
||||
},
|
||||
])('should pass validation for withUsageCount $name', ({ request }) => {
|
||||
const result = RetrieveTagQueryDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'with number',
|
||||
request: {
|
||||
withUsageCount: 1,
|
||||
},
|
||||
expectedErrorPath: ['withUsageCount'],
|
||||
},
|
||||
{
|
||||
name: 'with boolean (true) ',
|
||||
request: {
|
||||
withUsageCount: true,
|
||||
},
|
||||
expectedErrorPath: ['withUsageCount'],
|
||||
},
|
||||
{
|
||||
name: 'with boolean (false)',
|
||||
request: {
|
||||
withUsageCount: false,
|
||||
},
|
||||
expectedErrorPath: ['withUsageCount'],
|
||||
},
|
||||
{
|
||||
name: 'with invalid string',
|
||||
request: {
|
||||
withUsageCount: 'invalid',
|
||||
},
|
||||
expectedErrorPath: ['withUsageCount'],
|
||||
},
|
||||
])('should fail validation for withUsageCount $name', ({ request, expectedErrorPath }) => {
|
||||
const result = RetrieveTagQueryDto.safeParse(request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class CreateOrUpdateTagRequestDto extends Z.class({
|
||||
name: z.string().trim().min(1),
|
||||
}) {}
|
|
@ -0,0 +1,7 @@
|
|||
import { Z } from 'zod-class';
|
||||
|
||||
import { booleanFromString } from '../../schemas/booleanFromString';
|
||||
|
||||
export class RetrieveTagQueryDto extends Z.class({
|
||||
withUsageCount: booleanFromString.optional().default('false'),
|
||||
}) {}
|
|
@ -0,0 +1,47 @@
|
|||
import { ManualRunQueryDto } from '../manual-run-query.dto';
|
||||
|
||||
describe('ManualRunQueryDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{ name: 'version number 1', partialExecutionVersion: '1' },
|
||||
{ name: 'version number 2', partialExecutionVersion: '2' },
|
||||
{ name: 'missing version' },
|
||||
])('should validate $name', ({ partialExecutionVersion }) => {
|
||||
const result = ManualRunQueryDto.safeParse({ partialExecutionVersion });
|
||||
|
||||
if (!result.success) {
|
||||
return fail('expected validation to succeed');
|
||||
}
|
||||
expect(result.success).toBe(true);
|
||||
expect(typeof result.data.partialExecutionVersion).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'invalid version 0',
|
||||
partialExecutionVersion: '0',
|
||||
expectedErrorPath: ['partialExecutionVersion'],
|
||||
},
|
||||
{
|
||||
name: 'invalid type (boolean)',
|
||||
partialExecutionVersion: true,
|
||||
expectedErrorPath: ['partialExecutionVersion'],
|
||||
},
|
||||
{
|
||||
name: 'invalid type (number)',
|
||||
partialExecutionVersion: 1,
|
||||
expectedErrorPath: ['partialExecutionVersion'],
|
||||
},
|
||||
])('should fail validation for $name', ({ partialExecutionVersion, expectedErrorPath }) => {
|
||||
const result = ManualRunQueryDto.safeParse({ partialExecutionVersion });
|
||||
|
||||
if (result.success) {
|
||||
return fail('expected validation to fail');
|
||||
}
|
||||
|
||||
expect(result.error.issues[0].path).toEqual(expectedErrorPath);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class ManualRunQueryDto extends Z.class({
|
||||
partialExecutionVersion: z
|
||||
.enum(['1', '2'])
|
||||
.default('1')
|
||||
.transform((version) => Number.parseInt(version) as 1 | 2),
|
||||
}) {}
|
|
@ -87,6 +87,7 @@ export interface FrontendSettings {
|
|||
};
|
||||
};
|
||||
publicApi: {
|
||||
apiKeysPerUserLimit: number;
|
||||
enabled: boolean;
|
||||
latestVersion: number;
|
||||
path: string;
|
||||
|
@ -177,4 +178,8 @@ export interface FrontendSettings {
|
|||
};
|
||||
betaFeatures: FrontendBetaFeatures[];
|
||||
easyAIWorkflowOnboarded: boolean;
|
||||
partialExecution: {
|
||||
version: 1 | 2;
|
||||
enforce: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,9 +4,24 @@ export type * from './push';
|
|||
export type * from './scaling';
|
||||
export type * from './frontend-settings';
|
||||
export type * from './user';
|
||||
export type * from './api-keys';
|
||||
|
||||
export type { Collaborator } from './push/collaboration';
|
||||
export type { SendWorkerStatusMessage } from './push/worker';
|
||||
|
||||
export type { BannerName } from './schemas/bannerName.schema';
|
||||
export { passwordSchema } from './schemas/password.schema';
|
||||
|
||||
export type {
|
||||
ProjectType,
|
||||
ProjectIcon,
|
||||
ProjectRole,
|
||||
ProjectRelation,
|
||||
} from './schemas/project.schema';
|
||||
|
||||
export {
|
||||
type SourceControlledFile,
|
||||
SOURCE_CONTROL_FILE_LOCATION,
|
||||
SOURCE_CONTROL_FILE_STATUS,
|
||||
SOURCE_CONTROL_FILE_TYPE,
|
||||
} from './schemas/source-controlled-file.schema';
|
||||
|
|
|
@ -52,6 +52,17 @@ type NodeExecuteAfter = {
|
|||
executionId: string;
|
||||
nodeName: string;
|
||||
data: ITaskData;
|
||||
|
||||
/**
|
||||
* When a worker relays updates about a manual execution to main, if the
|
||||
* payload size is above a limit, we send only a placeholder to the client.
|
||||
* Later we fetch the entire execution data and fill in any placeholders.
|
||||
*
|
||||
* When sending a placheolder, we also send the number of output items, so
|
||||
* the client knows ahead of time how many items are there, to prevent the
|
||||
* items count from jumping up when the execution finishes.
|
||||
*/
|
||||
itemCount?: number;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ export type RunningJobSummary = {
|
|||
workflowName: string;
|
||||
mode: WorkflowExecuteMode;
|
||||
startedAt: Date;
|
||||
retryOf: string;
|
||||
retryOf?: string;
|
||||
status: ExecutionStatus;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import {
|
||||
projectNameSchema,
|
||||
projectTypeSchema,
|
||||
projectIconSchema,
|
||||
projectRoleSchema,
|
||||
projectRelationSchema,
|
||||
} from '../project.schema';
|
||||
|
||||
describe('project.schema', () => {
|
||||
describe('projectNameSchema', () => {
|
||||
test.each([
|
||||
{ name: 'valid name', value: 'My Project', expected: true },
|
||||
{ name: 'empty name', value: '', expected: false },
|
||||
{ name: 'name too long', value: 'a'.repeat(256), expected: false },
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = projectNameSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectTypeSchema', () => {
|
||||
test.each([
|
||||
{ name: 'valid type: personal', value: 'personal', expected: true },
|
||||
{ name: 'valid type: team', value: 'team', expected: true },
|
||||
{ name: 'invalid type', value: 'invalid', expected: false },
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = projectTypeSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectIconSchema', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'valid emoji icon',
|
||||
value: { type: 'emoji', value: '🚀' },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'valid icon',
|
||||
value: { type: 'icon', value: 'blah' },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'invalid icon type',
|
||||
value: { type: 'invalid', value: '🚀' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'empty icon value',
|
||||
value: { type: 'emoji', value: '' },
|
||||
expected: false,
|
||||
},
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = projectIconSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectRoleSchema', () => {
|
||||
test.each([
|
||||
{ name: 'valid role: project:personalOwner', value: 'project:personalOwner', expected: true },
|
||||
{ name: 'valid role: project:admin', value: 'project:admin', expected: true },
|
||||
{ name: 'valid role: project:editor', value: 'project:editor', expected: true },
|
||||
{ name: 'valid role: project:viewer', value: 'project:viewer', expected: true },
|
||||
{ name: 'invalid role', value: 'invalid-role', expected: false },
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = projectRoleSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectRelationSchema', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'valid relation',
|
||||
value: { userId: 'user-123', role: 'project:admin' },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'invalid userId type',
|
||||
value: { userId: 123, role: 'project:admin' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'invalid role',
|
||||
value: { userId: 'user-123', role: 'invalid-role' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'missing userId',
|
||||
value: { role: 'project:admin' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'missing role',
|
||||
value: { userId: 'user-123' },
|
||||
expected: false,
|
||||
},
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = projectRelationSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue