mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 16:44:07 -08:00
Merge remote-tracking branch 'origin/master' into export-import
This commit is contained in:
commit
5eb9413469
|
@ -11,3 +11,8 @@
|
|||
# refactor: Run lintfix (no-changelog) (#7537)
|
||||
|
||||
62c096710fab2f7e886518abdbded34b55e93f62
|
||||
|
||||
# refactor: Move test files alongside tested files (#11504)
|
||||
|
||||
7e58fc4fec468aca0b45d5bfe6150e1af632acbc
|
||||
f32b13c6ed078be042a735bc8621f27e00dc3116
|
||||
|
|
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
|
@ -11,6 +11,8 @@ Photos and videos are recommended.
|
|||
Include links to **Linear ticket** or Github issue or Community forum post.
|
||||
Important in order to close *automatically* and provide context to reviewers.
|
||||
-->
|
||||
<!-- Use "closes #<issue-number>", "fixes #<issue-number>", or "resolves #<issue-number>" to automatically close issues when the PR is merged. -->
|
||||
|
||||
|
||||
## Review / Merge checklist
|
||||
|
||||
|
|
1
.github/workflows/chromatic.yml
vendored
1
.github/workflows/chromatic.yml
vendored
|
@ -65,6 +65,7 @@ jobs:
|
|||
continue-on-error: true
|
||||
with:
|
||||
workingDir: packages/design-system
|
||||
onlyChanged: true
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
exitZeroOnChanges: false
|
||||
|
||||
|
|
3
.github/workflows/ci-pull-requests.yml
vendored
3
.github/workflows/ci-pull-requests.yml
vendored
|
@ -49,6 +49,9 @@ jobs:
|
|||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
cacheKey: ${{ github.sha }}-base:build
|
||||
collectCoverage: true
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
|
|
6
.github/workflows/e2e-reusable.yml
vendored
6
.github/workflows/e2e-reusable.yml
vendored
|
@ -41,6 +41,11 @@ 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.'
|
||||
|
@ -160,6 +165,7 @@ jobs:
|
|||
spec: '${{ inputs.spec }}'
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
CYPRESS_NODE_VIEW_VERSION: ${{ inputs.node_view_version }}
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
E2E_TESTS: true
|
||||
|
|
6
.github/workflows/e2e-tests.yml
vendored
6
.github/workflows/e2e-tests.yml
vendored
|
@ -27,6 +27,11 @@ 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:
|
||||
|
@ -46,6 +51,7 @@ 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 }}
|
||||
|
||||
|
|
40
.github/workflows/release-publish.yml
vendored
40
.github/workflows/release-publish.yml
vendored
|
@ -38,6 +38,12 @@ jobs:
|
|||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache/save@v4.0.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-release:build
|
||||
|
||||
- name: Dry-run publishing
|
||||
run: pnpm publish -r --no-git-checks --dry-run
|
||||
|
||||
|
@ -119,6 +125,40 @@ jobs:
|
|||
makeLatest: false
|
||||
body: ${{github.event.pull_request.body}}
|
||||
|
||||
create-sentry-release:
|
||||
name: Create a Sentry Release
|
||||
needs: [publish-to-npm, publish-to-docker-hub]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-release:build
|
||||
|
||||
- name: Create a frontend release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
projects: ${{ secrets.SENTRY_FRONTEND_PROJECT }}
|
||||
version: ${{ needs.publish-to-npm.outputs.release }}
|
||||
sourcemaps: packages/editor-ui/dist
|
||||
|
||||
- name: Create a backend release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
projects: ${{ secrets.SENTRY_BACKEND_PROJECT }}
|
||||
version: ${{ needs.publish-to-npm.outputs.release }}
|
||||
sourcemaps: packages/cli/dist packages/core/dist packages/nodes-base/dist packages/@n8n/n8n-nodes-langchain/dist
|
||||
|
||||
trigger-release-note:
|
||||
name: Trigger a release note
|
||||
needs: [publish-to-npm, create-github-release]
|
||||
|
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
@ -7,6 +7,7 @@
|
|||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"mjmlio.vscode-mjml",
|
||||
"Vue.volar"
|
||||
"Vue.volar",
|
||||
"vitest.explorer"
|
||||
]
|
||||
}
|
||||
|
|
171
CHANGELOG.md
171
CHANGELOG.md
|
@ -1,3 +1,174 @@
|
|||
# [1.67.0](https://github.com/n8n-io/n8n/compare/n8n@1.66.0...n8n@1.67.0) (2024-11-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Bring back nodes panel telemetry events ([#11456](https://github.com/n8n-io/n8n/issues/11456)) ([130c942](https://github.com/n8n-io/n8n/commit/130c942f633788d1b2f937d6fea342d4450c6e3d))
|
||||
* **core:** Account for double quotes in instance base URL ([#11495](https://github.com/n8n-io/n8n/issues/11495)) ([c5191e6](https://github.com/n8n-io/n8n/commit/c5191e697a9a9ebfa2b67587cd01b5835ebf6ea8))
|
||||
* **core:** Do not delete waiting executions when saving of successful executions is disabled ([#11458](https://github.com/n8n-io/n8n/issues/11458)) ([e8757e5](https://github.com/n8n-io/n8n/commit/e8757e58f69e091ac3d2a2f8e8c8e33ac57c1e47))
|
||||
* **core:** Don't send a `executionFinished` event to the browser with no run data if the execution has already been cleaned up ([#11502](https://github.com/n8n-io/n8n/issues/11502)) ([d1153f5](https://github.com/n8n-io/n8n/commit/d1153f51e80911cbc8f34ba5f038f349b75295c3))
|
||||
* **core:** Include `projectId` in range query middleware ([#11590](https://github.com/n8n-io/n8n/issues/11590)) ([a6070af](https://github.com/n8n-io/n8n/commit/a6070afdda29631fd36e5213f52bf815268bcda4))
|
||||
* **core:** Save exeution progress for waiting executions, even when progress saving is disabled ([#11535](https://github.com/n8n-io/n8n/issues/11535)) ([6b9353c](https://github.com/n8n-io/n8n/commit/6b9353c80f61ab36945fff434d98242dc1cab7b3))
|
||||
* **core:** Use the correct docs URL for regular nodes when used as tools ([#11529](https://github.com/n8n-io/n8n/issues/11529)) ([a092b8e](https://github.com/n8n-io/n8n/commit/a092b8e972e1253d92df416f19096a045858e7c1))
|
||||
* **Edit Image Node:** Fix Text operation by setting Arial as default font ([#11125](https://github.com/n8n-io/n8n/issues/11125)) ([60c1ace](https://github.com/n8n-io/n8n/commit/60c1ace64be29d651ce7b777fbb576598e38b9d7))
|
||||
* **editor:** Auto focus first fields on SignIn, SignUp and ForgotMyPassword views ([#11445](https://github.com/n8n-io/n8n/issues/11445)) ([5b5bd72](https://github.com/n8n-io/n8n/commit/5b5bd7291dde17880b7699f7e6832938599ffd8f))
|
||||
* **editor:** Do not overwrite the webhookId in the new canvas ([#11562](https://github.com/n8n-io/n8n/issues/11562)) ([dfd785b](https://github.com/n8n-io/n8n/commit/dfd785bc0894257eb6e62b0dd8f71248c27aae53))
|
||||
* **editor:** Ensure Enter key on Cancel button correctly cancels node rename ([#11563](https://github.com/n8n-io/n8n/issues/11563)) ([be05ae3](https://github.com/n8n-io/n8n/commit/be05ae36e7790156cb48b317fc254ae46a3b2d8c))
|
||||
* **editor:** Fix emitting `n8nReady` notification via `postmessage` on new canvas ([#11558](https://github.com/n8n-io/n8n/issues/11558)) ([463d101](https://github.com/n8n-io/n8n/commit/463d101f3592e6df4afd66c4d0fde0cb4aec34cc))
|
||||
* **editor:** Fix run index input for RunData view in sub-nodes ([#11538](https://github.com/n8n-io/n8n/issues/11538)) ([434d31c](https://github.com/n8n-io/n8n/commit/434d31ce928342d52b6ab8b78639afd7829216d4))
|
||||
* **editor:** Fix selected credential being overwritten in NDV ([#11496](https://github.com/n8n-io/n8n/issues/11496)) ([a26c0e2](https://github.com/n8n-io/n8n/commit/a26c0e2c3c7da87bfaba9737a967aa0070810d85))
|
||||
* **editor:** Keep workflow pristine after load on new canvas ([#11579](https://github.com/n8n-io/n8n/issues/11579)) ([7254359](https://github.com/n8n-io/n8n/commit/7254359855b89769613cd5cc24dbb4f45a7cc76f))
|
||||
* Show Pinned data in demo mode ([#11490](https://github.com/n8n-io/n8n/issues/11490)) ([ca2a583](https://github.com/n8n-io/n8n/commit/ca2a583b5cbb0cba3ecb694261806de16547aa91))
|
||||
* Toast not aligned to the bottom when AI assistant disable ([#11549](https://github.com/n8n-io/n8n/issues/11549)) ([e80f7e0](https://github.com/n8n-io/n8n/commit/e80f7e0a02a972379f73af6a44de11768081086e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Rapid7 InsightVm credentials ([#11462](https://github.com/n8n-io/n8n/issues/11462)) ([46eceab](https://github.com/n8n-io/n8n/commit/46eceabc27ac219b11b85c16c533a2cff848c5dd))
|
||||
* **AI Transform Node:** UX improvements ([#11280](https://github.com/n8n-io/n8n/issues/11280)) ([8a48407](https://github.com/n8n-io/n8n/commit/8a484077af3d3e1fe2d1b90b1ea9edf4ba41fcb6))
|
||||
* **Anthropic Chat Model Node:** Add support for Haiku 3.5 ([#11551](https://github.com/n8n-io/n8n/issues/11551)) ([8b39825](https://github.com/n8n-io/n8n/commit/8b398256a81594a52f20f8eb8adf8ff205209bc1))
|
||||
* **Convert to File Node:** Add delimiter convert to csv ([#11556](https://github.com/n8n-io/n8n/issues/11556)) ([63d454b](https://github.com/n8n-io/n8n/commit/63d454b776c092ff8c6c521a7e083774adb8f649))
|
||||
* **editor:** Update panning and selection keybindings on new canvas ([#11534](https://github.com/n8n-io/n8n/issues/11534)) ([5e2e205](https://github.com/n8n-io/n8n/commit/5e2e205394adf76faf02aee2d4f21df71848e1d4))
|
||||
* **Gmail Trigger Node:** Add filter option to include drafts ([#11441](https://github.com/n8n-io/n8n/issues/11441)) ([7a2be77](https://github.com/n8n-io/n8n/commit/7a2be77f384a32ede3acad8fe24fb89227c058bf))
|
||||
* **Intercom Node:** Update credential to new style ([#11485](https://github.com/n8n-io/n8n/issues/11485)) ([b137e13](https://github.com/n8n-io/n8n/commit/b137e13845f0714ebf7421c837f5ab104b66709b))
|
||||
|
||||
|
||||
|
||||
# [1.66.0](https://github.com/n8n-io/n8n/compare/n8n@1.65.0...n8n@1.66.0) (2024-10-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Asana Node:** Fix issue with pagination ([#11415](https://github.com/n8n-io/n8n/issues/11415)) ([04c075a](https://github.com/n8n-io/n8n/commit/04c075a46bcc7b1964397f0244b0fde99476212d))
|
||||
* **core:** Add 'user_id' to `license-community-plus-registered` telemetry event ([#11430](https://github.com/n8n-io/n8n/issues/11430)) ([7a8dafe](https://github.com/n8n-io/n8n/commit/7a8dafe9902fbc0d5001c50579c34959b95211ab))
|
||||
* **core:** Add safeguard for command publishing ([#11337](https://github.com/n8n-io/n8n/issues/11337)) ([656439e](https://github.com/n8n-io/n8n/commit/656439e87138f9f96dea5a683cfdac3f661ffefb))
|
||||
* **core:** Ensure `LoggerProxy` is not scoped ([#11379](https://github.com/n8n-io/n8n/issues/11379)) ([f4ea943](https://github.com/n8n-io/n8n/commit/f4ea943c9cb2321e41705de6c5c27535a0f5eae0))
|
||||
* **core:** Ensure `remove-triggers-and-pollers` command is not debounced ([#11486](https://github.com/n8n-io/n8n/issues/11486)) ([529d4fc](https://github.com/n8n-io/n8n/commit/529d4fc3ef5206bd1b02d27634342cc50b45997e))
|
||||
* **core:** Ensure job processor does not reprocess amended executions ([#11438](https://github.com/n8n-io/n8n/issues/11438)) ([c152a3a](https://github.com/n8n-io/n8n/commit/c152a3ac56f140a39eea4771a94f5a3082118df7))
|
||||
* **core:** Fix Message Event Bus Metrics not counting up for labeled metrics ([#11396](https://github.com/n8n-io/n8n/issues/11396)) ([7fc3b25](https://github.com/n8n-io/n8n/commit/7fc3b25d21c6c4f1802f34b1ae065a65cac3001b))
|
||||
* **core:** Fix resolving of $fromAI expression via `evaluateExpression` ([#11397](https://github.com/n8n-io/n8n/issues/11397)) ([2e64464](https://github.com/n8n-io/n8n/commit/2e6446454defbd3e5a47b66e6fd46d4f1b9fbd0f))
|
||||
* **core:** Make execution and its data creation atomic ([#11392](https://github.com/n8n-io/n8n/issues/11392)) ([ed30d43](https://github.com/n8n-io/n8n/commit/ed30d43236bf3c6b657022636a02a41be01aa152))
|
||||
* **core:** On unhandled rejections, extract the original exception correctly ([#11389](https://github.com/n8n-io/n8n/issues/11389)) ([8608bae](https://github.com/n8n-io/n8n/commit/8608baeb7ec302daddc8adca6e39778dcf7b2eda))
|
||||
* **editor:** Fix TypeError: Cannot read properties of undefined (reading '0') ([#11399](https://github.com/n8n-io/n8n/issues/11399)) ([ae37c52](https://github.com/n8n-io/n8n/commit/ae37c520a91c75e353e818944b36a3619c0d8b4a))
|
||||
* **editor:** Add Retry button for AI Assistant errors ([#11345](https://github.com/n8n-io/n8n/issues/11345)) ([7699240](https://github.com/n8n-io/n8n/commit/7699240073122cdef31cf109fd37fa66961f588a))
|
||||
* **editor:** Change tooltip for workflow with execute workflow trigger ([#11374](https://github.com/n8n-io/n8n/issues/11374)) ([dcd6038](https://github.com/n8n-io/n8n/commit/dcd6038c3085135803cdaa546a239359a6d449eb))
|
||||
* **editor:** Ensure toasts show above modal overlays ([#11410](https://github.com/n8n-io/n8n/issues/11410)) ([351134f](https://github.com/n8n-io/n8n/commit/351134f786af933f5f310bf8d9897269387635a0))
|
||||
* **editor:** Fit view consistently after nodes are initialized on new canvas ([#11457](https://github.com/n8n-io/n8n/issues/11457)) ([497d637](https://github.com/n8n-io/n8n/commit/497d637fc5308b9c4a06bc764152fde1f1a9c130))
|
||||
* **editor:** Fix adding connections when initializing workspace in templates view on new canvas ([#11451](https://github.com/n8n-io/n8n/issues/11451)) ([ea47b02](https://github.com/n8n-io/n8n/commit/ea47b025fb16c967d4fc73dcacc6e260d2aecd61))
|
||||
* **editor:** Fix rendering of AI logs ([#11450](https://github.com/n8n-io/n8n/issues/11450)) ([73b0a80](https://github.com/n8n-io/n8n/commit/73b0a80ac92b4f4b5a300d0ec1c833b4395a222a))
|
||||
* **editor:** Hide data mapping tooltip in credential edit modal ([#11356](https://github.com/n8n-io/n8n/issues/11356)) ([ff14dcb](https://github.com/n8n-io/n8n/commit/ff14dcb3a1ddaea4eca7c1ecd2e92c0abb0c413c))
|
||||
* **editor:** Prevent running workflow that has issues if listening to webhook ([#11402](https://github.com/n8n-io/n8n/issues/11402)) ([8b0a48f](https://github.com/n8n-io/n8n/commit/8b0a48f53010378e497e4cc362fda75a958cf363))
|
||||
* **editor:** Run external hooks after settings have been initialized ([#11423](https://github.com/n8n-io/n8n/issues/11423)) ([0ab24c8](https://github.com/n8n-io/n8n/commit/0ab24c814abd1787268750ba808993ab2735ac52))
|
||||
* **editor:** Support middle click to scroll when using a mouse on new canvas ([#11384](https://github.com/n8n-io/n8n/issues/11384)) ([46f3b4a](https://github.com/n8n-io/n8n/commit/46f3b4a258f89f02e0d2bd1eef25a22e3a721167))
|
||||
* **HTTP Request Tool Node:** Fix HTML response optimization issue ([#11439](https://github.com/n8n-io/n8n/issues/11439)) ([cf37e94](https://github.com/n8n-io/n8n/commit/cf37e94dd875e9f6ab1f189146fb34e7296af93c))
|
||||
* **n8n Form Node:** Form Trigger does not wait in multi-form mode ([#11404](https://github.com/n8n-io/n8n/issues/11404)) ([151f4dd](https://github.com/n8n-io/n8n/commit/151f4dd7b8f800af424f8ae64cb8238975fb3cb8))
|
||||
* Update required node js version in CONTRIBUTING.md ([#11437](https://github.com/n8n-io/n8n/issues/11437)) ([4f511aa](https://github.com/n8n-io/n8n/commit/4f511aab68651caa8fe47f70cd7cdb88bb06a3e2))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Anthropic Chat Model Node:** Add model claude-3-5-sonnet-20241022 ([#11465](https://github.com/n8n-io/n8n/issues/11465)) ([f6c8890](https://github.com/n8n-io/n8n/commit/f6c8890a8069de221b9b96e735418ecc9624cf7b))
|
||||
* **core:** Handle nodes with multiple inputs and connections during partial executions ([#11376](https://github.com/n8n-io/n8n/issues/11376)) ([cb7c4d2](https://github.com/n8n-io/n8n/commit/cb7c4d29a6f042b590822e5b9c67fff0a8f0863d))
|
||||
* **editor:** Add descriptive header to projects /workflow ([#11203](https://github.com/n8n-io/n8n/issues/11203)) ([5d19e8f](https://github.com/n8n-io/n8n/commit/5d19e8f2b45dc1abc5a8253f9e3a0fdacb1ebd91))
|
||||
* **editor:** Improve placeholder for vector store tool ([#11483](https://github.com/n8n-io/n8n/issues/11483)) ([629e092](https://github.com/n8n-io/n8n/commit/629e09240785bc648ff6575f97910fbb4e77cdab))
|
||||
* **editor:** Remove edge execution animation on new canvas ([#11446](https://github.com/n8n-io/n8n/issues/11446)) ([a701d87](https://github.com/n8n-io/n8n/commit/a701d87f5ba94ffc811e424b60e188b26ac6c1c5))
|
||||
* **editor:** Update ownership pills ([#11155](https://github.com/n8n-io/n8n/issues/11155)) ([8147038](https://github.com/n8n-io/n8n/commit/8147038cf87dca657602e617e49698065bf1a63f))
|
||||
|
||||
|
||||
|
||||
# [1.65.0](https://github.com/n8n-io/n8n/compare/n8n@1.64.0...n8n@1.65.0) (2024-10-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **AI Agent Node:** Preserve `intermediateSteps` when using output parser with non-tool agent ([#11363](https://github.com/n8n-io/n8n/issues/11363)) ([e61a853](https://github.com/n8n-io/n8n/commit/e61a8535aa39653b9a87575ea911a65318282167))
|
||||
* **API:** `PUT /credentials/:id` should move the specified credential, not the first one in the database ([#11365](https://github.com/n8n-io/n8n/issues/11365)) ([e6b2f8e](https://github.com/n8n-io/n8n/commit/e6b2f8e7e6ebbb6e3776a976297d519e99ac6c64))
|
||||
* **API:** Correct credential schema for response in `POST /credentials` ([#11340](https://github.com/n8n-io/n8n/issues/11340)) ([f495875](https://github.com/n8n-io/n8n/commit/f4958756b4976e0b608b9155dab84564f7e8804e))
|
||||
* **core:** Account for waiting jobs during shutdown ([#11338](https://github.com/n8n-io/n8n/issues/11338)) ([c863abd](https://github.com/n8n-io/n8n/commit/c863abd08300b53ea898fc4d06aae97dec7afa9b))
|
||||
* **core:** Add missing primary key to execution annotation tags table ([#11168](https://github.com/n8n-io/n8n/issues/11168)) ([b4b543d](https://github.com/n8n-io/n8n/commit/b4b543d41daa07753eca24ab93bf7445f672361d))
|
||||
* **core:** Change dedupe value column type from varchar(255) to text ([#11357](https://github.com/n8n-io/n8n/issues/11357)) ([7a71cff](https://github.com/n8n-io/n8n/commit/7a71cff4d75fe4e7282a398b4843428e0161ba8c))
|
||||
* **core:** Do not debounce webhooks, triggers and pollers activation ([#11306](https://github.com/n8n-io/n8n/issues/11306)) ([64bddf8](https://github.com/n8n-io/n8n/commit/64bddf86536ddd688638a643d24f80c947a12f31))
|
||||
* **core:** Enforce nodejs version consistently ([#11323](https://github.com/n8n-io/n8n/issues/11323)) ([0fa2e8c](https://github.com/n8n-io/n8n/commit/0fa2e8ca85005362d9043d82469f3c3525f4c4ef))
|
||||
* **core:** Fix memory issue with empty model response ([#11300](https://github.com/n8n-io/n8n/issues/11300)) ([216b119](https://github.com/n8n-io/n8n/commit/216b119350949de70f15cf2d61f474770803ad7a))
|
||||
* **core:** Fix race condition when resolving post-execute promise ([#11360](https://github.com/n8n-io/n8n/issues/11360)) ([4f1816e](https://github.com/n8n-io/n8n/commit/4f1816e03db00219bc2e723e3048848aef7f8fe1))
|
||||
* **core:** Sanitise IdP provided information in SAML test pages ([#11171](https://github.com/n8n-io/n8n/issues/11171)) ([74fc388](https://github.com/n8n-io/n8n/commit/74fc3889b946e8f224e65ef8d3d44125404aa4fc))
|
||||
* Don't show pin button in input panel when there's binary data ([#11267](https://github.com/n8n-io/n8n/issues/11267)) ([c0b5b92](https://github.com/n8n-io/n8n/commit/c0b5b92f62a2d7ba60492eb27daced268b654fe9))
|
||||
* **editor:** Add Personal project to main navigation ([#11161](https://github.com/n8n-io/n8n/issues/11161)) ([1f441f9](https://github.com/n8n-io/n8n/commit/1f441f97528f58e905eaf8930577bbcd08debf06))
|
||||
* **editor:** Fix Cannot read properties of undefined (reading 'finished') ([#11367](https://github.com/n8n-io/n8n/issues/11367)) ([475d72e](https://github.com/n8n-io/n8n/commit/475d72e0bc9e13c6dc56129902f6f89c67547f78))
|
||||
* **editor:** Fix delete all existing executions ([#11352](https://github.com/n8n-io/n8n/issues/11352)) ([3ec103f](https://github.com/n8n-io/n8n/commit/3ec103f8baaa89e579844947d945f00bec9e498e))
|
||||
* **editor:** Fix pin data button disappearing after reload ([#11198](https://github.com/n8n-io/n8n/issues/11198)) ([3b2f63e](https://github.com/n8n-io/n8n/commit/3b2f63e248cd0cba04087e2f40e13d670073707d))
|
||||
* **editor:** Fix RunData non-binary pagination when binary data is present ([#11309](https://github.com/n8n-io/n8n/issues/11309)) ([901888d](https://github.com/n8n-io/n8n/commit/901888d5b1027098653540c72f787f176941f35a))
|
||||
* **editor:** Fix sorting problem in older browsers that don't support `toSorted` ([#11204](https://github.com/n8n-io/n8n/issues/11204)) ([c728a2f](https://github.com/n8n-io/n8n/commit/c728a2ffe01f510a237979a54897c4680a407800))
|
||||
* **editor:** Follow-up fixes to projects side menu ([#11327](https://github.com/n8n-io/n8n/issues/11327)) ([4dde772](https://github.com/n8n-io/n8n/commit/4dde772814c55e66efcc9b369ae443328af21b14))
|
||||
* **editor:** Keep always focus on the first item on the node's search panel ([#11193](https://github.com/n8n-io/n8n/issues/11193)) ([c57cac9](https://github.com/n8n-io/n8n/commit/c57cac9e4d447c3a4240a565f9f2de8aa3b7c513))
|
||||
* **editor:** Open Community+ enrollment modal only for the instance owner ([#11292](https://github.com/n8n-io/n8n/issues/11292)) ([76724c3](https://github.com/n8n-io/n8n/commit/76724c3be6e001792433045c2b2aac0ef16d4b8a))
|
||||
* **editor:** Record sessionStarted telemetry event in Setting Store ([#11334](https://github.com/n8n-io/n8n/issues/11334)) ([1b734dd](https://github.com/n8n-io/n8n/commit/1b734dd9f42885594ce02400cfb395a4f5e7e088))
|
||||
* Ensure NDV params don't get cut off early and scrolled to the top ([#11252](https://github.com/n8n-io/n8n/issues/11252)) ([054fe97](https://github.com/n8n-io/n8n/commit/054fe9745ff6864f9088aa4cd66ed9e7869520d5))
|
||||
* **HTTP Request Tool Node:** Fix the undefined response issue when authentication is enabled ([#11343](https://github.com/n8n-io/n8n/issues/11343)) ([094ec68](https://github.com/n8n-io/n8n/commit/094ec68d4c00848013aa4eec4ac5efbd2c92afc5))
|
||||
* Include error in the message in JS task runner sandbox ([#11359](https://github.com/n8n-io/n8n/issues/11359)) ([0708b3a](https://github.com/n8n-io/n8n/commit/0708b3a1f8097af829c92fe106ea6ba375d6c500))
|
||||
* **Microsoft SQL Node:** Fix execute query to allow for non select query to run ([#11335](https://github.com/n8n-io/n8n/issues/11335)) ([ba158b4](https://github.com/n8n-io/n8n/commit/ba158b4f8533bd3430db8766d4921f75db5c1a11))
|
||||
* **OpenAI Chat Model Node, Ollama Chat Model Node:** Change default model to a more up-to-date option ([#11293](https://github.com/n8n-io/n8n/issues/11293)) ([0be04c6](https://github.com/n8n-io/n8n/commit/0be04c6348d8c059a96c3d37a6d6cd587bfb97f3))
|
||||
* **Pinecone Vector Store Node:** Prevent populating of vectors after manually stopping the execution ([#11288](https://github.com/n8n-io/n8n/issues/11288)) ([fbae17d](https://github.com/n8n-io/n8n/commit/fbae17d8fb35a5197fa183e3639bb36762dc73d2))
|
||||
* **Postgres Node:** Special datetime values cause errors ([#11225](https://github.com/n8n-io/n8n/issues/11225)) ([3c57f46](https://github.com/n8n-io/n8n/commit/3c57f46aaeb968d2974f2dc9790317a6a6fab624))
|
||||
* Resend invite operation on users list ([#11351](https://github.com/n8n-io/n8n/issues/11351)) ([e4218de](https://github.com/n8n-io/n8n/commit/e4218debd18812fa3aa508339afd3de03c4d69dc))
|
||||
* **SSH Node:** Cleanup temporary binary files as soon as possible ([#11305](https://github.com/n8n-io/n8n/issues/11305)) ([08a7b5b](https://github.com/n8n-io/n8n/commit/08a7b5b7425663ec6593114921c2e22ab37d039e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add report bug buttons ([#11304](https://github.com/n8n-io/n8n/issues/11304)) ([296f68f](https://github.com/n8n-io/n8n/commit/296f68f041b93fd32ac7be2b53c2b41d58c2998a))
|
||||
* **AI Agent Node:** Make tools optional when using OpenAI model with Tools agent ([#11212](https://github.com/n8n-io/n8n/issues/11212)) ([fed7c3e](https://github.com/n8n-io/n8n/commit/fed7c3ec1fb0553adaa9a933f91aabfd54fe83a3))
|
||||
* **core:** introduce JWT API keys for the public API ([#11005](https://github.com/n8n-io/n8n/issues/11005)) ([679fa4a](https://github.com/n8n-io/n8n/commit/679fa4a10a85fc96e12ca66fe12cdb32368bc12b))
|
||||
* **core:** Enforce config file permissions on startup ([#11328](https://github.com/n8n-io/n8n/issues/11328)) ([c078a51](https://github.com/n8n-io/n8n/commit/c078a516bec857831cc904ef807d0791b889f3a2))
|
||||
* **core:** Handle cycles in workflows when partially executing them ([#11187](https://github.com/n8n-io/n8n/issues/11187)) ([321d6de](https://github.com/n8n-io/n8n/commit/321d6deef18806d88d97afef2f2c6f29e739ccb4))
|
||||
* **editor:** Separate node output execution tooltip from status icon ([#11196](https://github.com/n8n-io/n8n/issues/11196)) ([cd15e95](https://github.com/n8n-io/n8n/commit/cd15e959c7af82a7d8c682e94add2b2640624a70))
|
||||
* **GitHub Node:** Add workflow resource operations ([#10744](https://github.com/n8n-io/n8n/issues/10744)) ([d309112](https://github.com/n8n-io/n8n/commit/d3091126472faa2c8f270650e54027d19dc56bb6))
|
||||
* **n8n Form Page Node:** New node ([#10390](https://github.com/n8n-io/n8n/issues/10390)) ([643d66c](https://github.com/n8n-io/n8n/commit/643d66c0ae084a0d93dac652703adc0a32cab8de))
|
||||
* **n8n Google My Business Node:** New node ([#10504](https://github.com/n8n-io/n8n/issues/10504)) ([bf28fbe](https://github.com/n8n-io/n8n/commit/bf28fbefe5e8ba648cba1555a2d396b75ee32bbb))
|
||||
* Run `mfa.beforeSetup` hook before enabling MFA ([#11116](https://github.com/n8n-io/n8n/issues/11116)) ([25c1c32](https://github.com/n8n-io/n8n/commit/25c1c3218cf1075ca3abd961236f3b2fbd9d6ba9))
|
||||
* **Structured Output Parser Node:** Refactor Output Parsers and Improve Error Handling ([#11148](https://github.com/n8n-io/n8n/issues/11148)) ([45274f2](https://github.com/n8n-io/n8n/commit/45274f2e7f081e194e330e1c9e6a5c26fca0b141))
|
||||
|
||||
|
||||
|
||||
# [1.64.0](https://github.com/n8n-io/n8n/compare/n8n@1.63.0...n8n@1.64.0) (2024-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Adjust arrow button colors in dark mode ([#11248](https://github.com/n8n-io/n8n/issues/11248)) ([439132c](https://github.com/n8n-io/n8n/commit/439132c291a812d57702c94eaa12878394ac4c69))
|
||||
* **core:** Ensure error reporter does not promote `info` to `error` messages ([#11245](https://github.com/n8n-io/n8n/issues/11245)) ([a7fc7fc](https://github.com/n8n-io/n8n/commit/a7fc7fc22997acec86dc94386c95349fd018f4ae))
|
||||
* **core:** Override executions mode if `regular` during worker startup ([#11250](https://github.com/n8n-io/n8n/issues/11250)) ([c0aa28c](https://github.com/n8n-io/n8n/commit/c0aa28c6cf3f77b04e04663217c9df8e3803ed3f))
|
||||
* **core:** Wrap nodes for tool use at a suitable time ([#11238](https://github.com/n8n-io/n8n/issues/11238)) ([c2fb881](https://github.com/n8n-io/n8n/commit/c2fb881d61291209802438d95892d052f5c82d43))
|
||||
* Don't show pinned data tooltip for pinned nodes ([#11249](https://github.com/n8n-io/n8n/issues/11249)) ([c2ad156](https://github.com/n8n-io/n8n/commit/c2ad15646d326a8f71e314d54efe202a5bcdd296))
|
||||
* **editor:** Bring back the "Forgot password" link on SigninView ([#11216](https://github.com/n8n-io/n8n/issues/11216)) ([4e78c46](https://github.com/n8n-io/n8n/commit/4e78c46a7450c7fc0694369944d4fb446cef2348))
|
||||
* **editor:** Fix chat crashing when rendering output-parsed content ([#11210](https://github.com/n8n-io/n8n/issues/11210)) ([4aaebfd](https://github.com/n8n-io/n8n/commit/4aaebfd4358f590e98c453ad4e65cc2c9d0f76f8))
|
||||
* **editor:** Make submit in ChangePasswordView work again ([#11227](https://github.com/n8n-io/n8n/issues/11227)) ([4f27b39](https://github.com/n8n-io/n8n/commit/4f27b39b45b58779d363980241e6e5e11b58f5da))
|
||||
* Expressions display actual result of evaluating expression inside string ([#11257](https://github.com/n8n-io/n8n/issues/11257)) ([7f5f0a9](https://github.com/n8n-io/n8n/commit/7f5f0a9df3b3fae6e2f9787443ac1cf9415d5932))
|
||||
* **Google Ads Node:** Update to use v17 api ([#11243](https://github.com/n8n-io/n8n/issues/11243)) ([3d97f02](https://github.com/n8n-io/n8n/commit/3d97f02a8d2b6e5bc7c97c5271bed97417ecacd2))
|
||||
* **Google Calendar Node:** Fix issue with conference data types not loading ([#11185](https://github.com/n8n-io/n8n/issues/11185)) ([4012758](https://github.com/n8n-io/n8n/commit/401275884e5db0287e4eeffb3c7497dd5e024880))
|
||||
* **Google Calendar Node:** Mode to add or replace attendees in event update ([#11132](https://github.com/n8n-io/n8n/issues/11132)) ([6c6a8ef](https://github.com/n8n-io/n8n/commit/6c6a8efdea83cf7194304ce089d7b72d8f6c1a9d))
|
||||
* **HTTP Request Tool Node:** Respond with an error when receive binary response ([#11219](https://github.com/n8n-io/n8n/issues/11219)) ([0d23a7f](https://github.com/n8n-io/n8n/commit/0d23a7fb5ba41545f70c4848d30b90af91b1e7e6))
|
||||
* **MySQL Node:** Fix "Maximum call stack size exceeded" error when handling a large number of rows ([#11242](https://github.com/n8n-io/n8n/issues/11242)) ([b7ee0c4](https://github.com/n8n-io/n8n/commit/b7ee0c4087eae346bc7e5360130d6c812dbe99db))
|
||||
* **n8n Trigger Node:** Merge with Workflow Trigger node ([#11174](https://github.com/n8n-io/n8n/issues/11174)) ([6ec6b51](https://github.com/n8n-io/n8n/commit/6ec6b5197ae97eb86496effd458fcc0b9b223ef3))
|
||||
* **OpenAI Node:** Fix tool parameter parsing issue ([#11201](https://github.com/n8n-io/n8n/issues/11201)) ([5a1d81a](https://github.com/n8n-io/n8n/commit/5a1d81ad917fde5cd6a387fe2d4ec6aab6b71349))
|
||||
* **Set Node:** Fix issue with UI properties not being hidden ([#11263](https://github.com/n8n-io/n8n/issues/11263)) ([1affc27](https://github.com/n8n-io/n8n/commit/1affc27b6bf9a559061a06f92bebe8167d938665))
|
||||
* **Strava Trigger Node:** Fix issue with webhook not being deleted ([#11226](https://github.com/n8n-io/n8n/issues/11226)) ([566529c](https://github.com/n8n-io/n8n/commit/566529ca1149988a54a58b3c34bbe4d9f1add6db))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add tracking for node errors and update node graph ([#11060](https://github.com/n8n-io/n8n/issues/11060)) ([d3b05f1](https://github.com/n8n-io/n8n/commit/d3b05f1c54e62440666297d8e484ccd22168da48))
|
||||
* **core:** Dedupe ([#10101](https://github.com/n8n-io/n8n/issues/10101)) ([52dd2c7](https://github.com/n8n-io/n8n/commit/52dd2c76196c6895b47145c2b85a6895ce2874d4))
|
||||
* **editor:** Send workflow context to assistant store ([#11135](https://github.com/n8n-io/n8n/issues/11135)) ([fade9e4](https://github.com/n8n-io/n8n/commit/fade9e43c84a0ae1fbc80f3ee546a418970e2380))
|
||||
* **Gong Node:** New node ([#10777](https://github.com/n8n-io/n8n/issues/10777)) ([785b47f](https://github.com/n8n-io/n8n/commit/785b47feb3b83cf36aaed57123f8baca2bbab307))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **Google Sheets Node:** Don't load whole spreadsheet dataset to determine columns when appending data ([#11235](https://github.com/n8n-io/n8n/issues/11235)) ([26ad091](https://github.com/n8n-io/n8n/commit/26ad091f473bca4e5d3bdc257e0818be02e52db5))
|
||||
|
||||
|
||||
|
||||
# [1.63.0](https://github.com/n8n-io/n8n/compare/n8n@1.62.1...n8n@1.63.0) (2024-10-09)
|
||||
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ If you already have VS Code and Docker installed, you can click [here](https://v
|
|||
|
||||
#### Node.js
|
||||
|
||||
[Node.js](https://nodejs.org/en/) version 18.10 or newer is required for development purposes.
|
||||
[Node.js](https://nodejs.org/en/) version 20.15 or newer is required for development purposes.
|
||||
|
||||
#### pnpm
|
||||
|
||||
|
|
|
@ -40,6 +40,14 @@ export function getOutputPanelDataContainer() {
|
|||
return getOutputPanel().getByTestId('ndv-data-container');
|
||||
}
|
||||
|
||||
export function getOutputTableRows() {
|
||||
return getOutputPanelDataContainer().find('table tr');
|
||||
}
|
||||
|
||||
export function getOutputTableRow(row: number) {
|
||||
return getOutputTableRows().eq(row);
|
||||
}
|
||||
|
||||
export function getOutputPanelTable() {
|
||||
return getOutputPanelDataContainer().get('table');
|
||||
}
|
||||
|
|
|
@ -69,6 +69,13 @@ export function getNodeCreatorPlusButton() {
|
|||
return cy.getByTestId('node-creator-plus-button');
|
||||
}
|
||||
|
||||
export function getCanvasNodes() {
|
||||
return cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('canvas-node'),
|
||||
() => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
|
|
@ -20,6 +20,7 @@ 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);
|
||||
|
@ -114,6 +115,7 @@ 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(CODE_NODE_NAME);
|
||||
|
@ -146,18 +148,14 @@ describe('Undo/Redo', () => {
|
|||
it('should undo/redo deleting a connection using context menu', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().realHover();
|
||||
cy.get('.connection-actions .delete')
|
||||
.filter(':visible')
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
WorkflowPage.actions.deleteNodeBetweenNodes(SCHEDULE_TRIGGER_NODE_NAME, CODE_NODE_NAME);
|
||||
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);
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
@ -206,6 +204,7 @@ 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);
|
||||
|
@ -244,6 +243,7 @@ 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);
|
||||
|
|
|
@ -16,6 +16,7 @@ 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');
|
||||
|
@ -25,10 +26,11 @@ describe('Canvas Actions', () => {
|
|||
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 });
|
||||
cy.get('.jtk-connector').should('have.length', 1);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
|
||||
WorkflowPage.getters.nodeViewBackground().click(600, 400, { force: true });
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
|
@ -40,16 +42,16 @@ describe('Canvas Actions', () => {
|
|||
);
|
||||
|
||||
WorkflowPage.getters
|
||||
.canvasNodeInputEndpointByName(`${EDIT_FIELDS_SET_NODE_NAME}1`)
|
||||
.should('have.class', 'jtk-endpoint-connected');
|
||||
.getConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, `${EDIT_FIELDS_SET_NODE_NAME}1`)
|
||||
.should('be.visible');
|
||||
|
||||
cy.get('.jtk-connector').should('have.length', 1);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
// Disconnect Set1
|
||||
cy.drag(
|
||||
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
[-200, 100],
|
||||
);
|
||||
cy.get('.jtk-connector').should('have.length', 0);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
it('should add first step', () => {
|
||||
|
@ -74,7 +76,7 @@ describe('Canvas Actions', () => {
|
|||
|
||||
it('should add a connected node using plus endpoint', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
cy.get('.plus-endpoint').should('be.visible').click();
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}');
|
||||
|
@ -85,7 +87,7 @@ describe('Canvas Actions', () => {
|
|||
|
||||
it('should add a connected node dragging from node creator', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
cy.get('.plus-endpoint').should('be.visible').click();
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
||||
cy.drag(WorkflowPage.getters.nodeCreatorNodeItems().first(), [100, 100], {
|
||||
|
@ -99,7 +101,7 @@ describe('Canvas Actions', () => {
|
|||
|
||||
it('should open a category when trying to drag and drop it on the canvas', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
cy.get('.plus-endpoint').should('be.visible').click();
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
||||
cy.drag(WorkflowPage.getters.nodeCreatorActionItems().first(), [100, 100], {
|
||||
|
@ -114,7 +116,7 @@ describe('Canvas Actions', () => {
|
|||
it('should add disconnected node if nothing is selected', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
// Deselect nodes
|
||||
WorkflowPage.getters.nodeViewBackground().click({ force: true });
|
||||
WorkflowPage.getters.nodeView().click({ force: true });
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
|
@ -136,10 +138,10 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
||||
|
||||
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
|
||||
const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left'));
|
||||
const editFieldsNodeLeft = WorkflowPage.getters.getNodeLeftPosition($editFieldsNode);
|
||||
|
||||
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
|
||||
const httpNodeLeft = parseFloat($httpNode.css('left'));
|
||||
const httpNodeLeft = WorkflowPage.getters.getNodeLeftPosition($httpNode);
|
||||
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
|
||||
});
|
||||
});
|
||||
|
@ -159,10 +161,12 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().first().realHover();
|
||||
cy.get('.connection-actions .delete').first().click({ force: true });
|
||||
WorkflowPage.actions.deleteNodeBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME);
|
||||
|
||||
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();
|
||||
|
@ -216,10 +220,10 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.actions.hitSelectAll();
|
||||
|
||||
WorkflowPage.actions.hitCopy();
|
||||
successToast().should('contain', 'Copied!');
|
||||
successToast().should('contain', 'Copied to clipboard');
|
||||
|
||||
WorkflowPage.actions.copyNode(CODE_NODE_NAME);
|
||||
successToast().should('contain', 'Copied!');
|
||||
successToast().should('contain', 'Copied to clipboard');
|
||||
});
|
||||
|
||||
it('should select/deselect all nodes', () => {
|
||||
|
@ -231,17 +235,31 @@ 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();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
cy.wait(500);
|
||||
cy.get('body').type('{leftArrow}');
|
||||
WorkflowPage.getters.canvasNodes().first().should('have.class', 'jtk-drag-selected');
|
||||
const selectedCanvasNodes = () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => WorkflowPage.getters.canvasNodes(),
|
||||
() => WorkflowPage.getters.canvasNodes().parent(),
|
||||
);
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => selectedCanvasNodes().first().should('have.class', 'jtk-drag-selected'),
|
||||
() => selectedCanvasNodes().first().should('have.class', 'selected'),
|
||||
);
|
||||
cy.get('body').type('{rightArrow}');
|
||||
WorkflowPage.getters.canvasNodes().last().should('have.class', 'jtk-drag-selected');
|
||||
cy.ifCanvasVersion(
|
||||
() => selectedCanvasNodes().last().should('have.class', 'jtk-drag-selected'),
|
||||
() => selectedCanvasNodes().last().should('have.class', 'selected'),
|
||||
);
|
||||
});
|
||||
|
||||
// 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();
|
||||
|
@ -251,6 +269,7 @@ describe('Canvas Actions', () => {
|
|||
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
|
||||
|
@ -262,6 +281,7 @@ describe('Canvas Actions', () => {
|
|||
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]);
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from './../constants';
|
||||
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();
|
||||
|
@ -52,15 +53,15 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
// Make sure outputless switch was connected correctly
|
||||
cy.get(
|
||||
`[data-target-node="${SWITCH_NODE_NAME}1"][data-source-node="${EDIT_FIELDS_SET_NODE_NAME}3"]`,
|
||||
).should('be.visible');
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(`${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
|
||||
.canvasNodeInputEndpointByName(setName)
|
||||
.should('have.class', 'jtk-endpoint-connected');
|
||||
.getConnectionBetweenNodes(`${SWITCH_NODE_NAME}`, setName)
|
||||
.should('exist');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -69,9 +70,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
|
||||
WorkflowPage.getters
|
||||
.nodeViewBackground()
|
||||
.click((i + 1) * 200, (i + 1) * 200, { force: true });
|
||||
WorkflowPage.getters.nodeView().click((i + 1) * 200, (i + 1) * 200, { force: true });
|
||||
}
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
|
||||
|
@ -84,8 +83,6 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
);
|
||||
|
||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 2);
|
||||
|
||||
// Connect Set1 and Set2 to merge
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME),
|
||||
|
@ -95,20 +92,36 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
|
||||
);
|
||||
|
||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
||||
const checkConnections = () => {
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
`${EDIT_FIELDS_SET_NODE_NAME}1`,
|
||||
)
|
||||
.should('exist');
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(EDIT_FIELDS_SET_NODE_NAME, MERGE_NODE_NAME)
|
||||
.should('exist');
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}1`, MERGE_NODE_NAME)
|
||||
.should('exist');
|
||||
};
|
||||
checkConnections();
|
||||
|
||||
// Make sure all connections are there after save & reload
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
|
||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
||||
checkConnections();
|
||||
// cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
||||
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.get('[data-label="2 items"]').should('be.visible');
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('[data-label="2 items"]').should('be.visible'),
|
||||
() => cy.getByTestId('canvas-node-output-handle').contains('2 items').should('be.visible'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add nodes and check execution success', () => {
|
||||
|
@ -120,16 +133,42 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.actions.executeWorkflow();
|
||||
|
||||
cy.get('.jtk-connector.success').should('have.length', 3);
|
||||
cy.get('.data-count').should('have.length', 4);
|
||||
cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success');
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.jtk-connector.success').should('have.length', 3),
|
||||
() => cy.get('[data-edge-status=success]').should('have.length', 3),
|
||||
);
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.data-count').should('have.length', 4),
|
||||
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
|
||||
);
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'),
|
||||
() => cy.getByTestId('canvas-handle-plus').should('have.attr', 'data-plus-type', 'success'),
|
||||
);
|
||||
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
|
||||
cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success');
|
||||
cy.get('.jtk-connector.success').should('have.length', 3);
|
||||
cy.get('.jtk-connector').should('have.length', 4);
|
||||
cy.ifCanvasVersion(
|
||||
() =>
|
||||
cy
|
||||
.get('.plus-draggable-endpoint')
|
||||
.filter(':visible')
|
||||
.should('not.have.class', 'ep-success'),
|
||||
() =>
|
||||
cy.getByTestId('canvas-handle-plus').should('not.have.attr', 'data-plus-type', 'success'),
|
||||
);
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.jtk-connector.success').should('have.length', 3),
|
||||
// The new version of the canvas correctly shows executed data being passed to the input of the next node
|
||||
() => cy.get('[data-edge-status=success]').should('have.length', 4),
|
||||
);
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.data-count').should('have.length', 4),
|
||||
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete node using context menu', () => {
|
||||
|
@ -194,19 +233,29 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
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()
|
||||
.last()
|
||||
.then(($node) => {
|
||||
const { left, top } = $node.position();
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.last()
|
||||
|
@ -218,80 +267,68 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Canvas Zoom Functionality', () => {
|
||||
const getContainer = () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => WorkflowPage.getters.nodeView(),
|
||||
() => WorkflowPage.getters.canvasViewport(),
|
||||
);
|
||||
const checkZoomLevel = (expectedFactor: number) => {
|
||||
return getContainer().should(($nodeView) => {
|
||||
const newTransform = $nodeView.css('transform');
|
||||
const newScale = parseFloat(newTransform.split(',')[0].slice(7));
|
||||
|
||||
expect(newScale).to.be.closeTo(expectedFactor, 0.2);
|
||||
});
|
||||
};
|
||||
|
||||
const zoomAndCheck = (action: 'zoomIn' | 'zoomOut', expectedFactor: number) => {
|
||||
WorkflowPage.getters[`${action}Button`]().click();
|
||||
checkZoomLevel(expectedFactor);
|
||||
};
|
||||
|
||||
it('should zoom in', () => {
|
||||
WorkflowPage.getters.zoomInButton().should('be.visible').click();
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${ZOOM_IN_X1_FACTOR}, 0, 0, ${ZOOM_IN_X1_FACTOR}, 0, 0)`,
|
||||
);
|
||||
WorkflowPage.getters.zoomInButton().click();
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${ZOOM_IN_X2_FACTOR}, 0, 0, ${ZOOM_IN_X2_FACTOR}, 0, 0)`,
|
||||
);
|
||||
WorkflowPage.getters.zoomInButton().should('be.visible');
|
||||
getContainer().then(($nodeView) => {
|
||||
const initialTransform = $nodeView.css('transform');
|
||||
const initialScale =
|
||||
initialTransform === 'none' ? 1 : parseFloat(initialTransform.split(',')[0].slice(7));
|
||||
|
||||
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X1_FACTOR);
|
||||
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X2_FACTOR);
|
||||
});
|
||||
});
|
||||
|
||||
it('should zoom out', () => {
|
||||
WorkflowPage.getters.zoomOutButton().should('be.visible').click();
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${ZOOM_OUT_X1_FACTOR}, 0, 0, ${ZOOM_OUT_X1_FACTOR}, 0, 0)`,
|
||||
);
|
||||
WorkflowPage.getters.zoomOutButton().click();
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${ZOOM_OUT_X2_FACTOR}, 0, 0, ${ZOOM_OUT_X2_FACTOR}, 0, 0)`,
|
||||
);
|
||||
zoomAndCheck('zoomOut', ZOOM_OUT_X1_FACTOR);
|
||||
zoomAndCheck('zoomOut', ZOOM_OUT_X2_FACTOR);
|
||||
});
|
||||
|
||||
it('should zoom using scroll or pinch gesture', () => {
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${PINCH_ZOOM_IN_FACTOR}, 0, 0, ${PINCH_ZOOM_IN_FACTOR}, 0, 0)`,
|
||||
|
||||
// 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');
|
||||
// Zoom in 1x + Zoom out 1x should reset to default (=1)
|
||||
WorkflowPage.getters.nodeView().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)');
|
||||
checkZoomLevel(1); // Zoom in 1x + Zoom out 1x should reset to default (=1)
|
||||
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${PINCH_ZOOM_OUT_FACTOR}, 0, 0, ${PINCH_ZOOM_OUT_FACTOR}, 0, 0)`,
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => checkZoomLevel(PINCH_ZOOM_OUT_FACTOR),
|
||||
() => checkZoomLevel(ZOOM_OUT_X1_FACTOR),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset zoom', () => {
|
||||
// Reset zoom should not appear until zoom level changed
|
||||
WorkflowPage.getters.resetZoomButton().should('not.exist');
|
||||
WorkflowPage.getters.zoomInButton().click();
|
||||
WorkflowPage.getters.resetZoomButton().should('be.visible').click();
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${DEFAULT_ZOOM_FACTOR}, 0, 0, ${DEFAULT_ZOOM_FACTOR}, 0, 0)`,
|
||||
);
|
||||
checkZoomLevel(DEFAULT_ZOOM_FACTOR);
|
||||
});
|
||||
|
||||
it('should zoom to fit', () => {
|
||||
|
@ -304,6 +341,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.zoomToFitButton().click();
|
||||
WorkflowPage.getters.canvasNodes().last().should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable node (context menu or shortcut)', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
|
@ -426,9 +464,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 1);
|
||||
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));
|
||||
|
@ -441,6 +479,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// 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');
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { simpleWebhookCall, waitForWebhook } from './16-webhook-node.cy';
|
||||
import {
|
||||
HTTP_REQUEST_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
|
@ -7,6 +10,7 @@ import {
|
|||
} from '../constants';
|
||||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { errorToast } from '../pages/notifications';
|
||||
import { getVisiblePopper } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
@ -212,6 +216,42 @@ 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();
|
||||
|
||||
// hide other visible popper on workflow execute button
|
||||
workflowPage.getters.canvasNodes().eq(0).click();
|
||||
|
||||
getVisiblePopper().should('have.length', 0);
|
||||
});
|
||||
});
|
||||
|
||||
function setExpressionOnStringValueInSet(expression: string) {
|
||||
|
|
|
@ -16,12 +16,14 @@ describe('n8n Form Trigger', () => {
|
|||
ndv.getters.parameterInput('formDescription').type('Test Form Description');
|
||||
ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
|
||||
ndv.getters.backToCanvas().click();
|
||||
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
|
||||
workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist');
|
||||
});
|
||||
|
||||
it('should fill up form fields', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger');
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger', {
|
||||
isTrigger: true,
|
||||
action: 'On new n8n Form event',
|
||||
});
|
||||
ndv.getters.parameterInput('formTitle').type('Test Form');
|
||||
ndv.getters.parameterInput('formDescription').type('Test Form Description');
|
||||
//fill up first field of type number
|
||||
|
@ -96,6 +98,6 @@ describe('n8n Form Trigger', () => {
|
|||
.type('Your test form was successfully submitted');
|
||||
|
||||
ndv.getters.backToCanvas().click();
|
||||
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
|
||||
workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ const workflowPage = new WorkflowPage();
|
|||
const ndv = new NDV();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
|
||||
const waitForWebhook = 500;
|
||||
export const waitForWebhook = 500;
|
||||
|
||||
interface SimpleWebhookCallOptions {
|
||||
method: string;
|
||||
|
@ -21,7 +21,7 @@ interface SimpleWebhookCallOptions {
|
|||
authentication?: string;
|
||||
}
|
||||
|
||||
const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||
export const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||
const {
|
||||
authentication,
|
||||
method,
|
||||
|
@ -65,15 +65,23 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
|||
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);
|
||||
|
||||
cy.request(method, `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
||||
callEndpoint((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
ndv.getters.outputPanel().contains('headers');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
callEndpoint,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Webhook Trigger node', () => {
|
||||
|
|
|
@ -226,6 +226,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
.should('have.length', 1)
|
||||
.click();
|
||||
credentialsModal.getters.saveButton().click();
|
||||
credentialsModal.getters.saveButton().should('have.text', 'Saved');
|
||||
credentialsModal.actions.close();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
|
@ -252,12 +253,13 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
credentialsModal.getters.usersSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 4).first().click();
|
||||
credentialsModal.getters.saveButton().click();
|
||||
credentialsModal.getters.saveButton().should('have.text', 'Saved');
|
||||
credentialsModal.actions.close();
|
||||
|
||||
credentialsPage.getters
|
||||
.credentialCards()
|
||||
.should('have.length', 2)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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';
|
||||
import { isCanvasV2 } from '../utils/workflowUtils';
|
||||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
const executionsTab = new WorkflowExecutionsTab();
|
||||
|
@ -117,15 +118,22 @@ describe('Execution', () => {
|
|||
.canvasNodeByName('Manual')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
|
||||
if (isCanvasV2()) {
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
|
||||
.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');
|
||||
clearNotifications();
|
||||
|
||||
// Clear execution data
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
@ -206,6 +214,7 @@ 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');
|
||||
|
||||
|
@ -267,9 +276,17 @@ describe('Execution', () => {
|
|||
.canvasNodeByName('Webhook')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
|
||||
if (isCanvasV2()) {
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
|
||||
.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'));
|
||||
|
@ -295,6 +312,7 @@ describe('Execution', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Missing pinned states for `edge-label-wrapper`
|
||||
describe('connections should be colored differently for pinned data', () => {
|
||||
beforeEach(() => {
|
||||
cy.createFixtureWorkflow('Schedule_pinned.json');
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
TRELLO_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
|
||||
import { successToast } from '../pages/notifications';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const credentialsPage = new CredentialsPage();
|
||||
|
@ -26,6 +26,22 @@ const nodeDetailsView = new NDV();
|
|||
const NEW_CREDENTIAL_NAME = 'Something else';
|
||||
const NEW_CREDENTIAL_NAME2 = 'Something else entirely';
|
||||
|
||||
function createNotionCredential() {
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
|
||||
workflowPage.actions.openNode(NOTION_NODE_NAME);
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
cy.get('body').type('{esc}');
|
||||
workflowPage.actions.deleteNode(NOTION_NODE_NAME);
|
||||
}
|
||||
|
||||
function deleteSelectedCredential() {
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.deleteButton().click();
|
||||
cy.get('.el-message-box').find('button').contains('Yes').click();
|
||||
}
|
||||
|
||||
describe('Credentials', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(credentialsPage.url);
|
||||
|
@ -229,6 +245,40 @@ describe('Credentials', () => {
|
|||
.should('have.value', NEW_CREDENTIAL_NAME);
|
||||
});
|
||||
|
||||
it('should set a default credential when adding nodes', () => {
|
||||
workflowPage.actions.visit();
|
||||
|
||||
createNotionCredential();
|
||||
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_NOTION_ACCOUNT_NAME);
|
||||
|
||||
deleteSelectedCredential();
|
||||
});
|
||||
|
||||
it('should set a default credential when editing a node', () => {
|
||||
workflowPage.actions.visit();
|
||||
|
||||
createNotionCredential();
|
||||
|
||||
workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true);
|
||||
nodeDetailsView.getters.parameterInput('authentication').click();
|
||||
getVisibleSelect().find('li').contains('Predefined').click();
|
||||
|
||||
nodeDetailsView.getters.parameterInput('nodeCredentialType').click();
|
||||
getVisibleSelect().find('li').contains('Notion API').click();
|
||||
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_NOTION_ACCOUNT_NAME);
|
||||
|
||||
deleteSelectedCredential();
|
||||
});
|
||||
|
||||
it('should setup generic authentication for HTTP node', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
|
@ -278,4 +328,25 @@ describe('Credentials', () => {
|
|||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
nodeDetailsView.getters.copyInput().should('not.exist');
|
||||
});
|
||||
|
||||
it('ADO-2583 should show notifications above credential modal overlay', () => {
|
||||
// check error notifications because they are sticky
|
||||
cy.intercept('POST', '/rest/credentials', { forceNetworkError: true });
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
|
||||
credentialsModal.getters.newCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||
|
||||
credentialsModal.getters.newCredentialTypeButton().click();
|
||||
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
||||
|
||||
credentialsModal.actions.setName('My awesome Notion account');
|
||||
credentialsModal.getters.saveButton().click({ force: true });
|
||||
errorToast().should('have.length', 1);
|
||||
errorToast().should('be.visible');
|
||||
|
||||
errorToast().should('have.css', 'z-index', '2100');
|
||||
cy.get('.el-overlay').should('have.css', 'z-index', '2001');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { WEBHOOK_NODE_NAME } from '../constants';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('ADO-2270 Save button resets on webhook node open', () => {
|
||||
it('should not reset the save button if webhook node is opened and closed', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addInitialNodeToCanvas(WEBHOOK_NODE_NAME);
|
||||
workflowPage.getters.saveButton().click();
|
||||
workflowPage.actions.openNode(WEBHOOK_NODE_NAME);
|
||||
|
||||
ndv.actions.close();
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('workflow-save-button').should('not.contain', 'Saved'),
|
||||
() => cy.getByTestId('workflow-save-button').should('contain', 'Saved'),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -90,6 +90,14 @@ function createRunDataWithError(inputMessage: string) {
|
|||
routine: 'InitPostgres',
|
||||
} as unknown as Error,
|
||||
} as ExecutionError,
|
||||
metadata: {
|
||||
subRun: [
|
||||
{
|
||||
node: 'Postgres Chat Memory',
|
||||
runIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
executionStatus: 'error',
|
||||
|
@ -124,14 +132,6 @@ function createRunDataWithError(inputMessage: string) {
|
|||
description: 'Internal error',
|
||||
message: 'Internal error',
|
||||
} as unknown as ExecutionError,
|
||||
metadata: {
|
||||
subRun: [
|
||||
{
|
||||
node: 'Postgres Chat Memory',
|
||||
runIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
86
cypress/e2e/2372-ado-prevent-clipping-params.cy.ts
Normal file
86
cypress/e2e/2372-ado-prevent-clipping-params.cy.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { NDV, WorkflowPage } from '../pages';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('ADO-2362 ADO-2350 NDV Prevent clipping long parameters and scrolling to expression', () => {
|
||||
it('should show last parameters and open at scroll top of parameters', () => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json');
|
||||
workflowPage.actions.openNode('Schedule Trigger');
|
||||
|
||||
ndv.getters.inlineExpressionEditorInput().should('be.visible');
|
||||
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.openNode('Edit Fields1');
|
||||
|
||||
// first parameter should be visible
|
||||
ndv.getters.inputLabel().eq(0).should('include.text', 'Mode');
|
||||
ndv.getters.inputLabel().eq(0).should('be.visible');
|
||||
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.length', 2);
|
||||
|
||||
// last parameter in view should be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible!');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||
|
||||
// next parameter in view should not be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
|
||||
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.openNode('Schedule Trigger');
|
||||
|
||||
// first parameter (notice) should be visible
|
||||
ndv.getters.nthParam(0).should('include.text', 'This workflow will run on the schedule ');
|
||||
ndv.getters.inputLabel().eq(0).should('be.visible');
|
||||
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.length', 2);
|
||||
|
||||
// last parameter in view should be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||
|
||||
// next parameter in view should not be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
|
||||
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.openNode('Slack');
|
||||
|
||||
// first field (credentials) should be visible
|
||||
ndv.getters.nodeCredentialsLabel().should('be.visible');
|
||||
|
||||
// last parameter in view should be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||
|
||||
// next parameter in view should not be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
|
||||
});
|
||||
|
||||
it('NODE-1272 ensure expressions scrolled to top, not middle', () => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json');
|
||||
workflowPage.actions.openNode('With long expression');
|
||||
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||
// should be scrolled at top
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.eq(0)
|
||||
.find('.cm-line')
|
||||
.eq(0)
|
||||
.should('have.text', '1 visible!');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(0).should('be.visible');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.eq(0)
|
||||
.find('.cm-line')
|
||||
.eq(6)
|
||||
.should('have.text', '7 not visible!');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(6).should('not.be.visible');
|
||||
});
|
||||
});
|
|
@ -117,7 +117,8 @@ describe('Debug', () => {
|
|||
workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty');
|
||||
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
ndv.getters.pinDataButton().click();
|
||||
ndv.actions.unPinData();
|
||||
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
openNode,
|
||||
getConnectionBySourceAndTarget,
|
||||
} from '../composables/workflow';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
|
||||
|
||||
describe('Langchain Integration', () => {
|
||||
|
@ -232,12 +233,7 @@ describe('Langchain Integration', () => {
|
|||
|
||||
const inputMessage = 'Hello!';
|
||||
const outputMessage = 'Hi there! How can I assist you today?';
|
||||
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => {
|
||||
sendManualChatMessage(inputMessage);
|
||||
},
|
||||
runData: [
|
||||
const runData = [
|
||||
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { input: inputMessage },
|
||||
|
@ -278,6 +274,9 @@ describe('Langchain Integration', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
inputOverride: {
|
||||
ai_languageModel: [
|
||||
[
|
||||
|
@ -316,11 +315,14 @@ describe('Langchain Integration', () => {
|
|||
jsonData: {
|
||||
main: { output: 'Hi there! How can I assist you today?' },
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
];
|
||||
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => {
|
||||
sendManualChatMessage(inputMessage);
|
||||
},
|
||||
runData,
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
|
@ -357,4 +359,56 @@ 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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
import workflow from '../fixtures/Manual_wait_set.json';
|
||||
import { getOutputTableRow } from '../composables/ndv';
|
||||
import { getCanvasNodes, openNode } from '../composables/workflow';
|
||||
import SIMPLE_WORKFLOW from '../fixtures/Manual_wait_set.json';
|
||||
import WORKFLOW_WITH_PINNED from '../fixtures/Webhook_set_pinned.json';
|
||||
import { importWorkflow, visitDemoPage } from '../pages/demo';
|
||||
import { errorToast } from '../pages/notifications';
|
||||
import { WorkflowPage } from '../pages/workflow';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
||||
describe('Demo', () => {
|
||||
beforeEach(() => {
|
||||
cy.overrideSettings({ previewMode: true });
|
||||
cy.signout();
|
||||
});
|
||||
|
||||
it('can import template', () => {
|
||||
visitDemoPage();
|
||||
errorToast().should('not.exist');
|
||||
importWorkflow(workflow);
|
||||
workflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
importWorkflow(SIMPLE_WORKFLOW);
|
||||
getCanvasNodes().should('have.length', 3);
|
||||
});
|
||||
|
||||
it('can import workflow with pin data', () => {
|
||||
visitDemoPage();
|
||||
importWorkflow(WORKFLOW_WITH_PINNED);
|
||||
getCanvasNodes().should('have.length', 2);
|
||||
openNode('Webhook');
|
||||
getOutputTableRow(0).should('include.text', 'headers');
|
||||
getOutputTableRow(1).should('include.text', 'dragons');
|
||||
});
|
||||
|
||||
it('can override theme to dark', () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
NDV,
|
||||
MainSidebar,
|
||||
} from '../pages';
|
||||
import { clearNotifications } from '../pages/notifications';
|
||||
import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
|
@ -440,7 +441,9 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.should('contain.text', 'Notion account personal project');
|
||||
});
|
||||
|
||||
it('should move resources between projects', () => {
|
||||
// Skip flaky test
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should move resources between projects', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
|
@ -448,38 +451,48 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowsPage.getters.workflowCards().should('not.have.length');
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project');
|
||||
clearNotifications();
|
||||
|
||||
projects.getHomeButton().click();
|
||||
projects.getProjectTabCredentials().should('be.visible').click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Credential in Home project');
|
||||
|
||||
clearNotifications();
|
||||
|
||||
// Create a project and add a credential and a workflow to it
|
||||
projects.createProject('Project 1');
|
||||
clearNotifications();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Credential in Project 1');
|
||||
clearNotifications();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1');
|
||||
|
||||
clearNotifications();
|
||||
|
||||
// Create another project and add a credential and a workflow to it
|
||||
projects.createProject('Project 2');
|
||||
clearNotifications();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Credential in Project 2');
|
||||
clearNotifications();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2');
|
||||
clearNotifications();
|
||||
|
||||
// Move the workflow owned by me from Home to Project 1
|
||||
// Move the workflow Personal from Home to Project 1
|
||||
projects.getHomeButton().click();
|
||||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 3)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('exist');
|
||||
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
|
||||
workflowsPage.getters.workflowMoveButton().click();
|
||||
|
@ -496,11 +509,12 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.filter(':contains("Project 1")')
|
||||
.click();
|
||||
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
|
||||
clearNotifications();
|
||||
|
||||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 3)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('not.exist');
|
||||
|
||||
// Move the workflow from Project 1 to Project 2
|
||||
|
@ -527,6 +541,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowsPage.getters.workflowCards().should('have.length', 2);
|
||||
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
|
||||
workflowsPage.getters.workflowMoveButton().click();
|
||||
clearNotifications();
|
||||
|
||||
projects
|
||||
.getResourceMoveModal()
|
||||
|
@ -566,10 +581,11 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.click();
|
||||
|
||||
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
|
||||
clearNotifications();
|
||||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 3)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('have.length', 1);
|
||||
|
||||
// Move the credential from Project 1 to Project 2
|
||||
|
@ -591,7 +607,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.filter(':contains("Project 2")')
|
||||
.click();
|
||||
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
|
||||
|
||||
clearNotifications();
|
||||
credentialsPage.getters.credentialCards().should('not.have.length');
|
||||
|
||||
// Move the credential from Project 2 to admin user
|
||||
|
@ -637,10 +653,12 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.click();
|
||||
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
|
||||
|
||||
clearNotifications();
|
||||
|
||||
credentialsPage.getters
|
||||
.credentialCards()
|
||||
.should('have.length', 3)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('have.length', 2);
|
||||
|
||||
// Move the credential from admin user back to its original project (Project 1)
|
||||
|
@ -668,7 +686,9 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should allow to change inaccessible credential when the workflow was moved to a team project', () => {
|
||||
// Skip flaky test
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should allow to change inaccessible credential when the workflow was moved to a team project', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
|
@ -699,7 +719,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 1)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('exist');
|
||||
workflowsPage.getters.workflowCardActions('My workflow').click();
|
||||
workflowsPage.getters.workflowMoveButton().click();
|
||||
|
@ -720,7 +740,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 1)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('not.exist');
|
||||
|
||||
//Log out with instance owner and log in with the member user
|
||||
|
|
|
@ -23,6 +23,7 @@ describe('Manual partial execution', () => {
|
|||
canvas.actions.openNode('Webhook1');
|
||||
|
||||
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('exist');
|
||||
ndv.getters.outputRunSelector().should('not.exist'); // single run
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import { clickCreateNewCredential, openCredentialSelect } from '../composables/n
|
|||
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();
|
||||
|
@ -11,6 +12,7 @@ const ndv = new NDV();
|
|||
const aiAssistant = new AIAssistant();
|
||||
const credentialsPage = new CredentialsPage();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
const nodeCreatorFeature = new NodeCreator();
|
||||
|
||||
describe('AI Assistant::disabled', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -145,42 +147,6 @@ describe('AI Assistant::enabled', () => {
|
|||
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it");
|
||||
});
|
||||
|
||||
it('should show quick replies when node is executed after new suggestion', () => {
|
||||
cy.intercept('POST', '/rest/ai/chat', (req) => {
|
||||
req.reply((res) => {
|
||||
if (['init-error-helper', 'message'].includes(req.body.payload.type)) {
|
||||
res.send({
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||
});
|
||||
} else if (req.body.payload.type === 'event') {
|
||||
res.send({
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/responses/node_execution_error_response.json',
|
||||
});
|
||||
} else {
|
||||
res.send({ statusCode: 500 });
|
||||
}
|
||||
});
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
|
||||
wf.actions.openNode('Edit Fields');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesAssistant().should('have.length', 1);
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
// Respond 'Yes' to the quick reply (request new suggestion)
|
||||
aiAssistant.getters.quickReplies().contains('Yes').click();
|
||||
cy.wait('@chatRequest');
|
||||
// No quick replies at this point
|
||||
aiAssistant.getters.quickReplies().should('not.exist');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
// But after executing the node again, quick replies should be shown
|
||||
aiAssistant.getters.quickReplies().should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should warn before starting a new session', () => {
|
||||
cy.intercept('POST', '/rest/ai/chat', {
|
||||
statusCode: 200,
|
||||
|
@ -316,6 +282,20 @@ describe('AI Assistant::enabled', () => {
|
|||
wf.getters.isWorkflowSaved();
|
||||
aiAssistant.getters.placeholderMessage().should('not.exist');
|
||||
});
|
||||
|
||||
it('should send message via enter even with global NodeCreator panel opened', () => {
|
||||
cy.intercept('POST', '/rest/ai/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
|
||||
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
aiAssistant.actions.openChat();
|
||||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
aiAssistant.getters.chatInput().type('Hello{Enter}');
|
||||
|
||||
aiAssistant.getters.placeholderMessage().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI Assistant Credential Help', () => {
|
||||
|
|
|
@ -133,9 +133,10 @@ describe('NDV', () => {
|
|||
"An expression here won't work because it uses .item and n8n can't figure out the matching item.",
|
||||
);
|
||||
ndv.getters.nodeRunErrorIndicator().should('be.visible');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('be.visible');
|
||||
// The error details should be hidden behind a tooltip
|
||||
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Start Time');
|
||||
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Execution Time');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Start Time');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Execution Time');
|
||||
});
|
||||
|
||||
it('should save workflow using keyboard shortcut from NDV', () => {
|
||||
|
@ -617,8 +618,10 @@ describe('NDV', () => {
|
|||
// Should not show run info before execution
|
||||
ndv.getters.nodeRunSuccessIndicator().should('not.exist');
|
||||
ndv.getters.nodeRunErrorIndicator().should('not.exist');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('not.exist');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('exist');
|
||||
});
|
||||
|
||||
it('should properly show node execution indicator for multiple nodes', () => {
|
||||
|
@ -630,6 +633,7 @@ describe('NDV', () => {
|
|||
// Manual tigger node should show success indicator
|
||||
workflowPage.actions.openNode('When clicking ‘Test workflow’');
|
||||
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('exist');
|
||||
// Code node should show error
|
||||
ndv.getters.backToCanvas().click();
|
||||
workflowPage.actions.openNode('Code');
|
||||
|
@ -791,4 +795,46 @@ describe('NDV', () => {
|
|||
.find('[data-test-id=run-data-schema-item]')
|
||||
.should('contain.text', 'onlyOnItem3');
|
||||
});
|
||||
|
||||
it('should keep search expanded after Test step node run', () => {
|
||||
cy.createFixtureWorkflow('Test_ndv_search.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.executeWorkflow();
|
||||
workflowPage.actions.openNode('Edit Fields');
|
||||
ndv.getters.outputPanel().should('be.visible');
|
||||
ndv.getters.outputPanel().findChildByTestId('ndv-search').click().type('US');
|
||||
ndv.getters.outputTableRow(1).find('mark').should('have.text', 'US');
|
||||
|
||||
ndv.actions.execute();
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
.findChildByTestId('ndv-search')
|
||||
.should('be.visible')
|
||||
.should('have.value', 'US');
|
||||
});
|
||||
|
||||
it('should not show items count when seaching in schema view', () => {
|
||||
cy.createFixtureWorkflow('Test_ndv_search.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.openNode('Edit Fields');
|
||||
ndv.getters.outputPanel().should('be.visible');
|
||||
ndv.actions.execute();
|
||||
ndv.actions.switchOutputMode('Schema');
|
||||
ndv.getters.outputPanel().find('[data-test-id=ndv-search]').click().type('US');
|
||||
ndv.getters.outputPanel().find('[data-test-id=ndv-items-count]').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show additional tooltip when seaching in schema view if no matches', () => {
|
||||
cy.createFixtureWorkflow('Test_ndv_search.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.openNode('Edit Fields');
|
||||
ndv.getters.outputPanel().should('be.visible');
|
||||
ndv.actions.execute();
|
||||
ndv.actions.switchOutputMode('Schema');
|
||||
ndv.getters.outputPanel().find('[data-test-id=ndv-search]').click().type('foo');
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
.contains('To search field contents rather than just names, use Table or JSON view')
|
||||
.should('exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -162,13 +162,6 @@ return []
|
|||
cy.get('#tab-code').should('have.class', 'is-active');
|
||||
});
|
||||
|
||||
it('should show error based on status code', () => {
|
||||
const prompt = nanoid(20);
|
||||
cy.get('#tab-ask-ai').click();
|
||||
ndv.actions.executePrevious();
|
||||
|
||||
cy.getByTestId('ask-ai-prompt-input').type(prompt);
|
||||
|
||||
const handledCodes = [
|
||||
{ code: 400, message: 'Code generation failed due to an unknown reason' },
|
||||
{ code: 413, message: 'Your workflow data is too large for AI to process' },
|
||||
|
@ -177,6 +170,13 @@ return []
|
|||
];
|
||||
|
||||
handledCodes.forEach(({ code, message }) => {
|
||||
it(`should show error based on status code ${code}`, () => {
|
||||
const prompt = nanoid(20);
|
||||
cy.get('#tab-ask-ai').click();
|
||||
ndv.actions.executePrevious();
|
||||
|
||||
cy.getByTestId('ask-ai-prompt-input').type(prompt);
|
||||
|
||||
cy.intercept('POST', '/rest/ai/ask-ai', {
|
||||
statusCode: code,
|
||||
status: code,
|
||||
|
|
347
cypress/fixtures/In_memory_vector_store_fake_embeddings.json
Normal file
347
cypress/fixtures/In_memory_vector_store_fake_embeddings.json
Normal file
File diff suppressed because one or more lines are too long
39
cypress/fixtures/Pinned_webhook_node.json
Normal file
39
cypress/fixtures/Pinned_webhook_node.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "FwrbSiaua2Xmvn6-Z-7CQ",
|
||||
"options": {}
|
||||
},
|
||||
"id": "8fcc7e5f-2cef-4938-9564-eea504c20aa0",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
360,
|
||||
220
|
||||
],
|
||||
"webhookId": "9c778f2a-e882-46ed-a0e4-c8e2f76ccd65"
|
||||
}
|
||||
],
|
||||
"connections": {},
|
||||
"pinData": {
|
||||
"Webhook": [
|
||||
{
|
||||
"headers": {
|
||||
"connection": "keep-alive",
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
||||
"accept": "*/*",
|
||||
"cookie": "n8n-auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjNiM2FhOTE5LWRhZDgtNDE5MS1hZWZiLTlhZDIwZTZkMjJjNiIsImhhc2giOiJ1ZVAxR1F3U2paIiwiaWF0IjoxNzI4OTE1NTQyLCJleHAiOjE3Mjk1MjAzNDJ9.fV02gpUnSiUoMxHwfB0npBjcjct7Mv9vGfj-jRTT3-I",
|
||||
"host": "localhost:5678",
|
||||
"accept-encoding": "gzip, deflate"
|
||||
},
|
||||
"params": {},
|
||||
"query": {},
|
||||
"body": {},
|
||||
"webhookUrl": "http://localhost:5678/webhook-test/FwrbSiaua2Xmvn6-Z-7CQ",
|
||||
"executionMode": "test"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
150
cypress/fixtures/Test-workflow-with-long-parameters.json
Normal file
150
cypress/fixtures/Test-workflow-with-long-parameters.json
Normal file
|
@ -0,0 +1,150 @@
|
|||
{
|
||||
"meta": {
|
||||
"instanceId": "777c68374367604fdf2a0bcfe9b1b574575ddea61aa8268e4bf034434bd7c894"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "0effebfc-fa8c-4d41-8a37-6d5695dfc9ee",
|
||||
"name": "test",
|
||||
"value": "test",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "beb8723f-6333-4186-ab88-41d4e2338866",
|
||||
"name": "test",
|
||||
"value": "test",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "85095836-4e94-442f-9270-e1a89008c129",
|
||||
"name": "test",
|
||||
"value": "test",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "b6163f8a-bca6-4364-8b38-182df37c55cd",
|
||||
"name": "=should be visible!",
|
||||
"value": "=not visible",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "950fcdc1-9e92-410f-8377-d4240e9bf6ff",
|
||||
"name": "Edit Fields1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
680,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"messageType": "block",
|
||||
"blocksUi": "blocks",
|
||||
"text": "=should be visible",
|
||||
"otherOptions": {
|
||||
"sendAsUser": "=not visible"
|
||||
}
|
||||
},
|
||||
"id": "dcf7410d-0f8e-4cdb-9819-ae275558bdaa",
|
||||
"name": "Slack",
|
||||
"type": "n8n-nodes-base.slack",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
900,
|
||||
460
|
||||
],
|
||||
"webhookId": "002b502e-31e5-4fdb-ac43-a56cfde8f82a"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [
|
||||
{},
|
||||
{
|
||||
"field": "=should be visible"
|
||||
},
|
||||
{
|
||||
"field": "=not visible"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "4c948a3f-19d4-4b08-a8be-f7d2964a21f4",
|
||||
"name": "Schedule Trigger",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [
|
||||
460,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "5dcaab37-1146-49c6-97a3-3b2f73483270",
|
||||
"name": "object",
|
||||
"value": "=1 visible!\n2 {\n3 \"str\": \"two\",\n4 \"str_date\": \"{{ $now }}\",\n5 \"str_int\": \"1\",\n6 \"str_float\": \"1.234\",\n7 not visible!\n \"str_bool\": \"true\",\n \"str_email\": \"david@thedavid.com\",\n \"str_with_email\":\"My email is david@n8n.io\",\n \"str_json_single\":\"{'one':'two'}\",\n \"str_json_double\":\"{\\\"one\\\":\\\"two\\\"}\",\n \"bool\": true,\n \"list\": [1, 2, 3],\n \"decimal\": 1.234,\n \"timestamp1\": 1708695471,\n \"timestamp2\": 1708695471000,\n \"timestamp3\": 1708695471000000,\n \"num_one\": 1\n}",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"includeOtherFields": true,
|
||||
"options": {}
|
||||
},
|
||||
"id": "a41dfb0d-38aa-42d2-b3e2-1854090bd319",
|
||||
"name": "With long expression",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [
|
||||
1100,
|
||||
460
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Edit Fields1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Slack",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Slack": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "With long expression",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Schedule Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {}
|
||||
}
|
135
cypress/fixtures/Test_ndv_search.json
Normal file
135
cypress/fixtures/Test_ndv_search.json
Normal file
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"name": "NDV search bugs (introduced by schema view?)",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "55635c7b-92ee-4d2d-a0c0-baff9ab071da",
|
||||
"name": "When clicking ‘Test workflow’",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"position": [
|
||||
800,
|
||||
380
|
||||
],
|
||||
"typeVersion": 1
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "getAllPeople"
|
||||
},
|
||||
"id": "4737af43-e49b-4c92-b76f-32605c047114",
|
||||
"name": "Customer Datastore (n8n training)",
|
||||
"type": "n8n-nodes-base.n8nTrainingCustomerDatastore",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1020,
|
||||
380
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": []
|
||||
},
|
||||
"includeOtherFields": true,
|
||||
"options": {}
|
||||
},
|
||||
"id": "8cc9b374-1856-4f3f-9315-08e6e27840d8",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
1240,
|
||||
380
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Customer Datastore (n8n training)": [
|
||||
{
|
||||
"json": {
|
||||
"id": "23423532",
|
||||
"name": "Jay Gatsby",
|
||||
"email": "gatsby@west-egg.com",
|
||||
"notes": "Keeps asking about a green light??",
|
||||
"country": "US",
|
||||
"created": "1925-04-10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423533",
|
||||
"name": "José Arcadio Buendía",
|
||||
"email": "jab@macondo.co",
|
||||
"notes": "Lots of people named after him. Very confusing",
|
||||
"country": "CO",
|
||||
"created": "1967-05-05"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423534",
|
||||
"name": "Max Sendak",
|
||||
"email": "info@in-and-out-of-weeks.org",
|
||||
"notes": "Keeps rolling his terrible eyes",
|
||||
"country": "US",
|
||||
"created": "1963-04-09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423535",
|
||||
"name": "Zaphod Beeblebrox",
|
||||
"email": "captain@heartofgold.com",
|
||||
"notes": "Felt like I was talking to more than one person",
|
||||
"country": null,
|
||||
"created": "1979-10-12"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423536",
|
||||
"name": "Edmund Pevensie",
|
||||
"email": "edmund@narnia.gov",
|
||||
"notes": "Passionate sailor",
|
||||
"country": "UK",
|
||||
"created": "1950-10-16"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Customer Datastore (n8n training)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Customer Datastore (n8n training)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "20178044-fb64-4443-88dd-e941517520d0",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
|
||||
},
|
||||
"id": "aBVnTRON9Y2cSmse",
|
||||
"tags": []
|
||||
}
|
|
@ -20,7 +20,8 @@ export class NDV extends BasePage {
|
|||
outputDataContainer: () => this.getters.outputPanel().findChildByTestId('ndv-data-container'),
|
||||
outputDisplayMode: () =>
|
||||
this.getters.outputPanel().findChildByTestId('ndv-run-data-display-mode').first(),
|
||||
pinDataButton: () => cy.getByTestId('ndv-pin-data'),
|
||||
pinDataButton: () => this.getters.outputPanel().findChildByTestId('ndv-pin-data'),
|
||||
unpinDataLink: () => this.getters.outputPanel().findChildByTestId('ndv-unpin-data'),
|
||||
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
||||
pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller .cm-content'),
|
||||
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
|
||||
|
@ -63,6 +64,7 @@ export class NDV extends BasePage {
|
|||
nodeRenameInput: () => cy.getByTestId('node-rename-input'),
|
||||
executePrevious: () => cy.getByTestId('execute-previous-node'),
|
||||
httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'),
|
||||
nodeCredentialsLabel: () => cy.getByTestId('credentials-label'),
|
||||
nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n),
|
||||
inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'),
|
||||
inputLinkRun: () => this.getters.inputPanel().findChildByTestId('link-run'),
|
||||
|
@ -130,8 +132,9 @@ export class NDV extends BasePage {
|
|||
codeEditorFullscreenButton: () => cy.getByTestId('code-editor-fullscreen-button'),
|
||||
codeEditorDialog: () => cy.getByTestId('code-editor-fullscreen'),
|
||||
codeEditorFullscreen: () => this.getters.codeEditorDialog().find('.cm-content'),
|
||||
nodeRunSuccessIndicator: () => cy.getByTestId('node-run-info-success'),
|
||||
nodeRunErrorIndicator: () => cy.getByTestId('node-run-info-danger'),
|
||||
nodeRunTooltipIndicator: () => cy.getByTestId('node-run-info'),
|
||||
nodeRunSuccessIndicator: () => cy.getByTestId('node-run-status-success'),
|
||||
nodeRunErrorIndicator: () => cy.getByTestId('node-run-status-danger'),
|
||||
nodeRunErrorMessage: () => cy.getByTestId('node-error-message'),
|
||||
nodeRunErrorDescription: () => cy.getByTestId('node-error-description'),
|
||||
fixedCollectionParameter: (paramName: string) =>
|
||||
|
@ -146,6 +149,9 @@ export class NDV extends BasePage {
|
|||
pinData: () => {
|
||||
this.getters.pinDataButton().click({ force: true });
|
||||
},
|
||||
unPinData: () => {
|
||||
this.getters.unpinDataLink().click({ force: true });
|
||||
},
|
||||
editPinnedData: () => {
|
||||
this.getters.editPinnedDataButton().click();
|
||||
},
|
||||
|
|
|
@ -2,7 +2,7 @@ import { BasePage } from './base';
|
|||
import { NodeCreator } from './features/node-creator';
|
||||
import { META_KEY } from '../constants';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
||||
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';
|
||||
|
||||
const nodeCreator = new NodeCreator();
|
||||
export class WorkflowPage extends BasePage {
|
||||
|
@ -27,7 +27,11 @@ 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.getByTestId('canvas-node'),
|
||||
canvasNodes: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('canvas-node'),
|
||||
() => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'),
|
||||
),
|
||||
canvasNodeByName: (nodeName: string) =>
|
||||
this.getters.canvasNodes().filter(`:contains(${nodeName})`),
|
||||
nodeIssuesByName: (nodeName: string) =>
|
||||
|
@ -37,6 +41,17 @@ export class WorkflowPage extends BasePage {
|
|||
.should('have.length.greaterThan', 0)
|
||||
.findChildByTestId('node-issues'),
|
||||
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}"]`;
|
||||
}
|
||||
if (type === 'output') {
|
||||
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-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-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`;
|
||||
},
|
||||
canvasNodeInputEndpointByName: (nodeName: string, index = 0) => {
|
||||
|
@ -46,7 +61,15 @@ export class WorkflowPage extends BasePage {
|
|||
return cy.get(this.getters.getEndpointSelector('output', nodeName, index));
|
||||
},
|
||||
canvasNodePlusEndpointByName: (nodeName: string, index = 0) => {
|
||||
return cy.get(this.getters.getEndpointSelector('plus', nodeName, index));
|
||||
return cy.ifCanvasVersion(
|
||||
() => cy.get(this.getters.getEndpointSelector('plus', nodeName, index)),
|
||||
() =>
|
||||
cy
|
||||
.get(
|
||||
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"] .clickable`,
|
||||
)
|
||||
.eq(index),
|
||||
);
|
||||
},
|
||||
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
|
||||
workflowMenu: () => cy.getByTestId('workflow-menu'),
|
||||
|
@ -56,13 +79,29 @@ export class WorkflowPage extends BasePage {
|
|||
expressionModalInput: () => cy.getByTestId('expression-modal-input').find('[role=textbox]'),
|
||||
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
||||
|
||||
nodeViewRoot: () => cy.getByTestId('node-view-root'),
|
||||
nodeViewRoot: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('node-view-root'),
|
||||
() => this.getters.nodeView(),
|
||||
),
|
||||
copyPasteInput: () => cy.getByTestId('hidden-copy-paste'),
|
||||
nodeConnections: () => cy.get('.jtk-connector'),
|
||||
nodeConnections: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.jtk-connector'),
|
||||
() => cy.getByTestId('edge-label-wrapper'),
|
||||
),
|
||||
zoomToFitButton: () => cy.getByTestId('zoom-to-fit'),
|
||||
nodeEndpoints: () => cy.get('.jtk-endpoint-connected'),
|
||||
disabledNodes: () => cy.get('.node-box.disabled'),
|
||||
selectedNodes: () => this.getters.canvasNodes().filter('.jtk-drag-selected'),
|
||||
disabledNodes: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.node-box.disabled'),
|
||||
() => cy.get('[data-test-id="canvas-trigger-node"][class*="disabled"]'),
|
||||
),
|
||||
selectedNodes: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => this.getters.canvasNodes().filter('.jtk-drag-selected'),
|
||||
() => this.getters.canvasNodes().parent().filter('.selected'),
|
||||
),
|
||||
// Workflow menu items
|
||||
workflowMenuItemDuplicate: () => cy.getByTestId('workflow-menu-item-duplicate'),
|
||||
workflowMenuItemDownload: () => cy.getByTestId('workflow-menu-item-download'),
|
||||
|
@ -92,8 +131,21 @@ export class WorkflowPage extends BasePage {
|
|||
shareButton: () => cy.getByTestId('workflow-share-button'),
|
||||
|
||||
duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'),
|
||||
nodeViewBackground: () => cy.getByTestId('node-view-background'),
|
||||
nodeView: () => cy.getByTestId('node-view'),
|
||||
nodeViewBackground: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('node-view-background'),
|
||||
() => cy.getByTestId('canvas'),
|
||||
),
|
||||
nodeView: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('node-view'),
|
||||
() => cy.get('[data-test-id="canvas-wrapper"]'),
|
||||
),
|
||||
canvasViewport: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('node-view'),
|
||||
() => cy.get('.vue-flow__transformationpane.vue-flow__container'),
|
||||
),
|
||||
inlineExpressionEditorInput: () =>
|
||||
cy.getByTestId('inline-expression-editor-input').find('[role=textbox]'),
|
||||
inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'),
|
||||
|
@ -115,19 +167,45 @@ export class WorkflowPage extends BasePage {
|
|||
ndvParameters: () => cy.getByTestId('parameter-item'),
|
||||
nodeCredentialsLabel: () => cy.getByTestId('credentials-label'),
|
||||
getConnectionBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
|
||||
cy.ifCanvasVersion(
|
||||
() =>
|
||||
cy.get(
|
||||
`.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
|
||||
),
|
||||
() =>
|
||||
cy.get(
|
||||
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`,
|
||||
),
|
||||
),
|
||||
getConnectionActionsBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
|
||||
cy.ifCanvasVersion(
|
||||
() =>
|
||||
cy.get(
|
||||
`.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
|
||||
),
|
||||
() =>
|
||||
cy.get(
|
||||
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`,
|
||||
),
|
||||
),
|
||||
addStickyButton: () => cy.getByTestId('add-sticky-button'),
|
||||
stickies: () => cy.getByTestId('sticky'),
|
||||
editorTabButton: () => cy.getByTestId('radio-button-workflow'),
|
||||
workflowHistoryButton: () => cy.getByTestId('workflow-history-button'),
|
||||
colors: () => cy.getByTestId('color'),
|
||||
contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`),
|
||||
getNodeLeftPosition: (element: JQuery<HTMLElement>) => {
|
||||
if (isCanvasV2()) {
|
||||
return parseFloat(element.parent().css('transform').split(',')[4]);
|
||||
}
|
||||
return parseFloat(element.css('left'));
|
||||
},
|
||||
getNodeTopPosition: (element: JQuery<HTMLElement>) => {
|
||||
if (isCanvasV2()) {
|
||||
return parseFloat(element.parent().css('transform').split(',')[5]);
|
||||
}
|
||||
return parseFloat(element.css('top'));
|
||||
},
|
||||
};
|
||||
|
||||
actions = {
|
||||
|
@ -332,7 +410,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.nodeViewBackground().trigger('wheel', {
|
||||
this.getters.nodeView().trigger('wheel', {
|
||||
force: true,
|
||||
bubbles: true,
|
||||
ctrlKey: true,
|
||||
|
@ -391,9 +469,12 @@ export class WorkflowPage extends BasePage {
|
|||
action?: string,
|
||||
) => {
|
||||
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
|
||||
this.getters
|
||||
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
|
||||
.find('.add')
|
||||
const connectionsBetweenNodes = () =>
|
||||
this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName);
|
||||
cy.ifCanvasVersion(
|
||||
() => connectionsBetweenNodes().find('.add'),
|
||||
() => connectionsBetweenNodes().get('[data-test-id="add-connection-button"]'),
|
||||
)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
||||
|
@ -401,9 +482,12 @@ export class WorkflowPage extends BasePage {
|
|||
},
|
||||
deleteNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string) => {
|
||||
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
|
||||
this.getters
|
||||
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
|
||||
.find('.delete')
|
||||
const connectionsBetweenNodes = () =>
|
||||
this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName);
|
||||
cy.ifCanvasVersion(
|
||||
() => connectionsBetweenNodes().find('.delete'),
|
||||
() => connectionsBetweenNodes().get('[data-test-id="delete-connection-button"]'),
|
||||
)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
N8N_AUTH_COOKIE,
|
||||
} from '../constants';
|
||||
import { WorkflowPage } from '../pages';
|
||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
||||
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';
|
||||
|
||||
Cypress.Commands.add('setAppDate', (targetDate: number | Date) => {
|
||||
cy.window().then((win) => {
|
||||
|
@ -26,6 +26,10 @@ Cypress.Commands.add('getByTestId', (selector, ...args) => {
|
|||
return cy.get(`[data-test-id="${selector}"]`, ...args);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('ifCanvasVersion', (getterV1, getterV2) => {
|
||||
return isCanvasV2() ? getterV2() : getterV1();
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
'createFixtureWorkflow',
|
||||
(fixtureKey: string, workflowName = getUniqueWorkflowName()) => {
|
||||
|
@ -70,6 +74,10 @@ Cypress.Commands.add('signin', ({ email, password }) => {
|
|||
})
|
||||
.then((response) => {
|
||||
Cypress.env('currentUserId', response.body.data.id);
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('NodeView.switcher.discovered', 'true'); // @TODO Remove this once the switcher is removed
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,11 @@ beforeEach(() => {
|
|||
win.localStorage.setItem('N8N_THEME', 'light');
|
||||
win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true');
|
||||
win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true');
|
||||
|
||||
const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION');
|
||||
if (nodeViewVersion) {
|
||||
win.localStorage.setItem('NodeView.version', nodeViewVersion);
|
||||
}
|
||||
});
|
||||
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
|
|
|
@ -28,6 +28,7 @@ declare global {
|
|||
selector: string,
|
||||
...args: Array<Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined>
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
ifCanvasVersion<T1, T2>(getterV1: () => T1, getterV2: () => T2): T1 | T2;
|
||||
findChildByTestId(childTestId: string): Chainable<JQuery<HTMLElement>>;
|
||||
/**
|
||||
* Creates a workflow from the given fixture and optionally renames it.
|
||||
|
|
|
@ -16,7 +16,7 @@ export function createMockNodeExecutionData(
|
|||
return {
|
||||
[name]: {
|
||||
startTime: new Date().getTime(),
|
||||
executionTime: 0,
|
||||
executionTime: 1,
|
||||
executionStatus,
|
||||
data: jsonData
|
||||
? Object.keys(jsonData).reduce((acc, key) => {
|
||||
|
@ -33,6 +33,7 @@ export function createMockNodeExecutionData(
|
|||
}, {} as ITaskDataConnections)
|
||||
: data,
|
||||
source: [null],
|
||||
inputOverride,
|
||||
...rest,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,3 +3,7 @@ import { nanoid } from 'nanoid';
|
|||
export function getUniqueWorkflowName(workflowNamePrefix?: string) {
|
||||
return workflowNamePrefix ? `${workflowNamePrefix} ${nanoid(12)}` : nanoid(12);
|
||||
}
|
||||
|
||||
export function isCanvasV2() {
|
||||
return Cypress.env('NODE_VIEW_VERSION') === 2;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,30 @@ WORKDIR /home/node
|
|||
COPY --from=builder /compiled /usr/local/lib/node_modules/n8n
|
||||
COPY docker/images/n8n/docker-entrypoint.sh /
|
||||
|
||||
# Setup the Task Runner Launcher
|
||||
ARG TARGETPLATFORM
|
||||
ARG LAUNCHER_VERSION=0.1.1
|
||||
ENV N8N_RUNNERS_MODE=internal_launcher \
|
||||
N8N_RUNNERS_LAUNCHER_PATH=/usr/local/bin/task-runner-launcher
|
||||
COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json
|
||||
# First, download, verify, then extract the launcher binary
|
||||
# Second, chmod with 4555 to allow the use of setuid
|
||||
# Third, create a new user and group to execute the Task Runners under
|
||||
RUN \
|
||||
if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="x86_64"; \
|
||||
elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="aarch64"; fi; \
|
||||
mkdir /launcher-temp && \
|
||||
cd /launcher-temp && \
|
||||
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip && \
|
||||
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
|
||||
sha256sum -c task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
|
||||
unzip -d $(dirname ${N8N_RUNNERS_LAUNCHER_PATH}) task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip task-runner-launcher && \
|
||||
cd - && \
|
||||
rm -r /launcher-temp && \
|
||||
chmod 4555 ${N8N_RUNNERS_LAUNCHER_PATH} && \
|
||||
addgroup -g 2000 task-runner && \
|
||||
adduser -D -u 2000 -g "Task Runner User" -G task-runner task-runner
|
||||
|
||||
RUN \
|
||||
cd /usr/local/lib/node_modules/n8n && \
|
||||
npm rebuild sqlite3 && \
|
||||
|
|
|
@ -22,6 +22,30 @@ RUN set -eux; \
|
|||
find /usr/local/lib/node_modules/n8n -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" | xargs rm -f && \
|
||||
rm -rf /root/.npm
|
||||
|
||||
# Setup the Task Runner Launcher
|
||||
ARG TARGETPLATFORM
|
||||
ARG LAUNCHER_VERSION=0.1.1
|
||||
ENV N8N_RUNNERS_MODE=internal_launcher \
|
||||
N8N_RUNNERS_LAUNCHER_PATH=/usr/local/bin/task-runner-launcher
|
||||
COPY n8n-task-runners.json /etc/n8n-task-runners.json
|
||||
# First, download, verify, then extract the launcher binary
|
||||
# Second, chmod with 4555 to allow the use of setuid
|
||||
# Third, create a new user and group to execute the Task Runners under
|
||||
RUN \
|
||||
if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="x86_64"; \
|
||||
elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="aarch64"; fi; \
|
||||
mkdir /launcher-temp && \
|
||||
cd /launcher-temp && \
|
||||
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip && \
|
||||
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
|
||||
sha256sum -c task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
|
||||
unzip -d $(dirname ${N8N_RUNNERS_LAUNCHER_PATH}) task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip task-runner-launcher && \
|
||||
cd - && \
|
||||
rm -r /launcher-temp && \
|
||||
chmod 4555 ${N8N_RUNNERS_LAUNCHER_PATH} && \
|
||||
addgroup -g 2000 task-runner && \
|
||||
adduser -D -u 2000 -g "Task Runner User" -G task-runner task-runner
|
||||
|
||||
COPY docker-entrypoint.sh /
|
||||
|
||||
RUN \
|
||||
|
|
22
docker/images/n8n/n8n-task-runners.json
Normal file
22
docker/images/n8n/n8n-task-runners.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"task-runners": [
|
||||
{
|
||||
"runner-type": "javascript",
|
||||
"workdir": "/home/task-runner",
|
||||
"command": "/usr/local/bin/node",
|
||||
"args": ["/usr/local/lib/node_modules/n8n/node_modules/@n8n/task-runner/dist/start.js"],
|
||||
"allowed-env": [
|
||||
"PATH",
|
||||
"N8N_RUNNERS_GRANT_TOKEN",
|
||||
"N8N_RUNNERS_N8N_URI",
|
||||
"N8N_RUNNERS_MAX_PAYLOAD",
|
||||
"N8N_RUNNERS_MAX_CONCURRENCY",
|
||||
"NODE_FUNCTION_ALLOW_BUILTIN",
|
||||
"NODE_FUNCTION_ALLOW_EXTERNAL",
|
||||
"NODE_OPTIONS"
|
||||
],
|
||||
"uid": 2000,
|
||||
"gid": 2000
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.63.0",
|
||||
"version": "1.67.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.15",
|
||||
|
@ -43,7 +43,9 @@
|
|||
"@biomejs/biome": "^1.9.0",
|
||||
"@n8n_io/eslint-config": "workspace:*",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/node": "*",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^29.6.2",
|
||||
"jest-environment-jsdom": "^29.6.2",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
|
@ -57,8 +59,8 @@
|
|||
"run-script-os": "^1.0.7",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tsc-alias": "^1.8.7",
|
||||
"tsc-watch": "^6.0.4",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"turbo": "2.1.2",
|
||||
"typescript": "*",
|
||||
"zx": "^8.1.4"
|
||||
|
@ -82,7 +84,6 @@
|
|||
},
|
||||
"patchedDependencies": {
|
||||
"typedi@0.10.0": "patches/typedi@0.10.0.patch",
|
||||
"@sentry/cli@2.36.2": "patches/@sentry__cli@2.36.2.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.4.0",
|
||||
"version": "0.6.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
@ -21,6 +21,7 @@
|
|||
"dist/**/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@n8n/config": "workspace:*",
|
||||
"n8n-workflow": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { FrontendBetaFeatures } from '@n8n/config';
|
||||
import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow';
|
||||
|
||||
export interface IVersionNotificationSettings {
|
||||
|
@ -169,4 +170,5 @@ export interface FrontendSettings {
|
|||
security: {
|
||||
blockFileAccessToN8nFiles: boolean;
|
||||
};
|
||||
betaFeatures: FrontendBetaFeatures[];
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# n8n benchmarking tool
|
||||
|
||||
Tool for executing benchmarks against an n8n instance. The tool consists of these components:
|
||||
Tool for executing benchmarks against an n8n instance.
|
||||
|
||||
## Directory structure
|
||||
|
||||
|
@ -12,6 +12,39 @@ packages/@n8n/benchmark
|
|||
├── scripts Orchestration scripts
|
||||
```
|
||||
|
||||
## Benchmarking an existing n8n instance
|
||||
|
||||
The easiest way to run the existing benchmark scenarios is to use the benchmark docker image:
|
||||
|
||||
```sh
|
||||
docker pull ghcr.io/n8n-io/n8n-benchmark:latest
|
||||
# Print the help to list all available flags
|
||||
docker run ghcr.io/n8n-io/n8n-benchmark:latest run --help
|
||||
# Run all available benchmark scenarios for 1 minute with 5 concurrent requests
|
||||
docker run ghcr.io/n8n-io/n8n-benchmark:latest run \
|
||||
--n8nBaseUrl=https://instance.url \
|
||||
--n8nUserEmail=InstanceOwner@email.com \
|
||||
--n8nUserPassword=InstanceOwnerPassword \
|
||||
--vus=5 \
|
||||
--duration=1m \
|
||||
--scenarioFilter SingleWebhook
|
||||
```
|
||||
|
||||
### Using custom scenarios with the Docker image
|
||||
|
||||
It is also possible to create your own [benchmark scenarios](#benchmark-scenarios) and load them using the `--testScenariosPath` flag:
|
||||
|
||||
```sh
|
||||
# Assuming your scenarios are located in `./scenarios`, mount them into `/scenarios` in the container
|
||||
docker run -v ./scenarios:/scenarios ghcr.io/n8n-io/n8n-benchmark:latest run \
|
||||
--n8nBaseUrl=https://instance.url \
|
||||
--n8nUserEmail=InstanceOwner@email.com \
|
||||
--n8nUserPassword=InstanceOwnerPassword \
|
||||
--vus=5 \
|
||||
--duration=1m \
|
||||
--testScenariosPath=/scenarios
|
||||
```
|
||||
|
||||
## Running the entire benchmark suite
|
||||
|
||||
The benchmark suite consists of [benchmark scenarios](#benchmark-scenarios) and different [n8n setups](#n8n-setups).
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/n8n-benchmark",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"description": "Cli for running benchmark tests for n8n",
|
||||
"main": "dist/index",
|
||||
"scripts": {
|
||||
|
@ -17,7 +17,7 @@
|
|||
"benchmark-locally": "pnpm benchmark --env local",
|
||||
"provision-cloud-env": "zx scripts/provision-cloud-env.mjs",
|
||||
"destroy-cloud-env": "zx scripts/destroy-cloud-env.mjs",
|
||||
"watch": "concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\""
|
||||
"watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.10"
|
||||
|
@ -41,10 +41,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/convict": "^6.1.1",
|
||||
"@types/k6": "^0.52.0",
|
||||
"@types/node": "^20.14.8",
|
||||
"tsc-alias": "^1.8.7",
|
||||
"typescript": "^5.5.2"
|
||||
"@types/k6": "^0.52.0"
|
||||
},
|
||||
"bin": {
|
||||
"n8n-benchmark": "./bin/n8n-benchmark"
|
||||
|
|
|
@ -15,6 +15,12 @@ export default function () {
|
|||
|
||||
const res = http.post(`${apiBaseUrl}/webhook/binary-files-benchmark`, data);
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error(
|
||||
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
'has correct content type': (r) =>
|
||||
|
|
|
@ -6,6 +6,12 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
|||
export default function () {
|
||||
const res = http.post(`${apiBaseUrl}/webhook/benchmark-http-node`);
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error(
|
||||
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
'http requests were OK': (r) => {
|
||||
|
|
|
@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
|||
|
||||
export default function () {
|
||||
const res = http.post(`${apiBaseUrl}/webhook/code-node-benchmark`, {});
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error(
|
||||
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
'has items in response': (r) => {
|
||||
|
|
|
@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
|||
|
||||
export default function () {
|
||||
const res = http.post(`${apiBaseUrl}/webhook/set-expressions-benchmark`, {});
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error(
|
||||
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
});
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
"name": "SingleWebhook",
|
||||
"description": "A single webhook trigger that responds with a 200 status code",
|
||||
"scenarioData": { "workflowFiles": ["single-webhook.json"] },
|
||||
"scriptPath": "single-webhook.script.ts"
|
||||
"scriptPath": "single-webhook.script.js"
|
||||
}
|
||||
|
|
|
@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
|||
|
||||
export default function () {
|
||||
const res = http.get(`${apiBaseUrl}/webhook/single-webhook`);
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error(
|
||||
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
});
|
|
@ -176,7 +176,7 @@ services:
|
|||
|
||||
# Load balancer that acts as an entry point for n8n
|
||||
n8n:
|
||||
image: nginx:latest
|
||||
image: nginx:1.27.2
|
||||
ports:
|
||||
- '5678:80'
|
||||
volumes:
|
||||
|
|
|
@ -3,6 +3,7 @@ events {}
|
|||
http {
|
||||
client_max_body_size 50M;
|
||||
access_log off;
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
upstream backend {
|
||||
server n8n_main1:5678;
|
||||
|
|
|
@ -78,12 +78,6 @@ async function runBenchmarksOnVm(config, benchmarkEnv) {
|
|||
const bootstrapScriptPath = path.join(scriptsDir, 'bootstrap.sh');
|
||||
await sshClient.ssh(`chmod a+x ${bootstrapScriptPath} && ${bootstrapScriptPath}`);
|
||||
|
||||
// Benchmarking the VM
|
||||
const vmBenchmarkScriptPath = path.join(scriptsDir, 'vm-benchmark.sh');
|
||||
await sshClient.ssh(`chmod a+x ${vmBenchmarkScriptPath} && ${vmBenchmarkScriptPath}`, {
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
// Give some time for the VM to be ready
|
||||
await sleep(1000);
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Install fio
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get -y install fio > /dev/null
|
||||
|
||||
# Run the disk benchmark
|
||||
fio --name=rand_rw --ioengine=libaio --rw=randrw --rwmixread=70 --bs=4k --numjobs=4 --size=1G --runtime=30 --directory=/n8n --group_reporting
|
||||
|
||||
# Remove files
|
||||
sudo rm /n8n/rand_rw.*
|
||||
|
||||
# Uninstall fio
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get -y remove fio > /dev/null
|
|
@ -13,6 +13,10 @@ export default class RunCommand extends Command {
|
|||
|
||||
static flags = {
|
||||
testScenariosPath,
|
||||
scenarioFilter: Flags.string({
|
||||
char: 'f',
|
||||
description: 'Filter scenarios by name',
|
||||
}),
|
||||
scenarioNamePrefix: Flags.string({
|
||||
description: 'Prefix for the scenario name',
|
||||
default: 'Unnamed',
|
||||
|
@ -95,7 +99,7 @@ export default class RunCommand extends Command {
|
|||
flags.scenarioNamePrefix,
|
||||
);
|
||||
|
||||
const allScenarios = scenarioLoader.loadAll(flags.testScenariosPath);
|
||||
const allScenarios = scenarioLoader.loadAll(flags.testScenariosPath, flags.scenarioFilter);
|
||||
|
||||
await scenarioRunner.runManyScenarios(allScenarios);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export class ScenarioLoader {
|
|||
/**
|
||||
* Loads all scenarios from the given path
|
||||
*/
|
||||
loadAll(pathToScenarios: string): Scenario[] {
|
||||
loadAll(pathToScenarios: string, filter?: string): Scenario[] {
|
||||
pathToScenarios = path.resolve(pathToScenarios);
|
||||
const scenarioFolders = fs
|
||||
.readdirSync(pathToScenarios, { withFileTypes: true })
|
||||
|
@ -18,6 +18,9 @@ export class ScenarioLoader {
|
|||
const scenarios: Scenario[] = [];
|
||||
|
||||
for (const folder of scenarioFolders) {
|
||||
if (filter && !folder.toLowerCase().includes(filter.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
const scenarioPath = path.join(pathToScenarios, folder);
|
||||
const manifestFileName = `${folder}.manifest.json`;
|
||||
const scenarioManifestPath = path.join(pathToScenarios, folder, manifestFileName);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { sleep } from 'zx';
|
||||
|
||||
import { AuthenticatedN8nApiClient } from '@/n8n-api-client/authenticated-n8n-api-client';
|
||||
import type { N8nApiClient } from '@/n8n-api-client/n8n-api-client';
|
||||
import type { ScenarioDataFileLoader } from '@/scenario/scenario-data-loader';
|
||||
|
@ -47,6 +49,10 @@ export class ScenarioRunner {
|
|||
const testData = await this.dataLoader.loadDataForScenario(scenario);
|
||||
await testDataImporter.importTestScenarioData(testData.workflows);
|
||||
|
||||
// Wait for 1s before executing the scenario to ensure that the workflows are activated.
|
||||
// In multi-main mode it can take some time before the workflow becomes active.
|
||||
await sleep(1000);
|
||||
|
||||
console.log('Executing scenario script');
|
||||
await this.k6Executor.executeTestScenario(scenario, {
|
||||
scenarioRunName,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "@n8n/chat",
|
||||
"version": "0.28.0",
|
||||
"version": "0.30.0",
|
||||
"scripts": {
|
||||
"dev": "pnpm run storybook",
|
||||
"build": "pnpm build:vite && pnpm build:bundle",
|
||||
"build:vite": "vite build",
|
||||
"build:bundle": "INCLUDE_VUE=true vite build",
|
||||
"build:vite": "cross-env vite build",
|
||||
"build:bundle": "cross-env INCLUDE_VUE=true vite build",
|
||||
"preview": "vite preview",
|
||||
"test:dev": "vitest",
|
||||
"test": "vitest run",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/config",
|
||||
"version": "1.13.0",
|
||||
"version": "1.17.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
11
packages/@n8n/config/src/configs/frontend.config.ts
Normal file
11
packages/@n8n/config/src/configs/frontend.config.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
import { StringArray } from '../utils';
|
||||
|
||||
export type FrontendBetaFeatures = 'canvas_v2';
|
||||
|
||||
@Config
|
||||
export class FrontendConfig {
|
||||
/** Which UI experiments to enable. Separate multiple values with a comma `,` */
|
||||
@Env('N8N_UI_BETA_FEATURES')
|
||||
betaFeatures: StringArray<FrontendBetaFeatures> = [];
|
||||
}
|
15
packages/@n8n/config/src/configs/generic.config.ts
Normal file
15
packages/@n8n/config/src/configs/generic.config.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class GenericConfig {
|
||||
/** Default timezone for the n8n instance. Can be overridden on a per-workflow basis. */
|
||||
@Env('GENERIC_TIMEZONE')
|
||||
timezone: string = 'America/New_York';
|
||||
|
||||
@Env('N8N_RELEASE_TYPE')
|
||||
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev';
|
||||
|
||||
/** Grace period (in seconds) to wait for components to shut down before process exit. */
|
||||
@Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT')
|
||||
gracefulShutdownTimeout: number = 30;
|
||||
}
|
28
packages/@n8n/config/src/configs/license.config.ts
Normal file
28
packages/@n8n/config/src/configs/license.config.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class LicenseConfig {
|
||||
/** License server URL to retrieve license. */
|
||||
@Env('N8N_LICENSE_SERVER_URL')
|
||||
serverUrl: string = 'https://license.n8n.io/v1';
|
||||
|
||||
/** Whether autorenewal for licenses is enabled. */
|
||||
@Env('N8N_LICENSE_AUTO_RENEW_ENABLED')
|
||||
autoRenewalEnabled: boolean = true;
|
||||
|
||||
/** How long (in seconds) before expiry a license should be autorenewed. */
|
||||
@Env('N8N_LICENSE_AUTO_RENEW_OFFSET')
|
||||
autoRenewOffset: number = 60 * 60 * 72; // 72 hours
|
||||
|
||||
/** Activation key to initialize license. */
|
||||
@Env('N8N_LICENSE_ACTIVATION_KEY')
|
||||
activationKey: string = '';
|
||||
|
||||
/** Tenant ID used by the license manager SDK, e.g. for self-hosted, sandbox, embed, cloud. */
|
||||
@Env('N8N_LICENSE_TENANT_ID')
|
||||
tenantId: number = 1;
|
||||
|
||||
/** Ephemeral license certificate. See: https://github.com/n8n-io/license-management?tab=readme-ov-file#concept-ephemeral-entitlements */
|
||||
@Env('N8N_LICENSE_CERT')
|
||||
cert: string = '';
|
||||
}
|
|
@ -1,14 +1,19 @@
|
|||
import { Config, Env, Nested } from '../decorators';
|
||||
import { StringArray } from '../utils';
|
||||
|
||||
/**
|
||||
* Scopes (areas of functionality) to filter logs by.
|
||||
*
|
||||
* `executions` -> execution lifecycle
|
||||
* `license` -> license SDK
|
||||
* `scaling` -> scaling mode
|
||||
*/
|
||||
export const LOG_SCOPES = ['executions', 'license', 'scaling'] as const;
|
||||
/** Scopes (areas of functionality) to filter logs by. */
|
||||
export const LOG_SCOPES = [
|
||||
'concurrency',
|
||||
'external-secrets',
|
||||
'license',
|
||||
'multi-main-setup',
|
||||
'pruning',
|
||||
'pubsub',
|
||||
'redis',
|
||||
'scaling',
|
||||
'waiting-executions',
|
||||
'task-runner',
|
||||
] as const;
|
||||
|
||||
export type LogScope = (typeof LOG_SCOPES)[number];
|
||||
|
||||
|
@ -59,14 +64,20 @@ export class LoggingConfig {
|
|||
/**
|
||||
* Scopes to filter logs by. Nothing is filtered by default.
|
||||
*
|
||||
* Currently supported log scopes:
|
||||
* - `executions`
|
||||
* Supported log scopes:
|
||||
*
|
||||
* - `concurrency`
|
||||
* - `external-secrets`
|
||||
* - `license`
|
||||
* - `multi-main-setup`
|
||||
* - `pubsub`
|
||||
* - `redis`
|
||||
* - `scaling`
|
||||
* - `waiting-executions`
|
||||
*
|
||||
* @example
|
||||
* `N8N_LOG_SCOPES=license`
|
||||
* `N8N_LOG_SCOPES=license,executions`
|
||||
* `N8N_LOG_SCOPES=license,waiting-executions`
|
||||
*/
|
||||
@Env('N8N_LOG_SCOPES')
|
||||
scopes: StringArray<LogScope> = [];
|
||||
|
|
16
packages/@n8n/config/src/configs/multi-main-setup.config.ts
Normal file
16
packages/@n8n/config/src/configs/multi-main-setup.config.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class MultiMainSetupConfig {
|
||||
/** Whether to enable multi-main setup (if licensed) for scaling mode. */
|
||||
@Env('N8N_MULTI_MAIN_SETUP_ENABLED')
|
||||
enabled: boolean = false;
|
||||
|
||||
/** Time to live (in seconds) for leader key in multi-main setup. */
|
||||
@Env('N8N_MULTI_MAIN_SETUP_KEY_TTL')
|
||||
ttl: number = 10;
|
||||
|
||||
/** Interval (in seconds) for leader check in multi-main setup. */
|
||||
@Env('N8N_MULTI_MAIN_SETUP_CHECK_INTERVAL')
|
||||
interval: number = 3;
|
||||
}
|
35
packages/@n8n/config/src/configs/pruning.config.ts
Normal file
35
packages/@n8n/config/src/configs/pruning.config.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class PruningConfig {
|
||||
/** Whether to delete past executions on a rolling basis. */
|
||||
@Env('EXECUTIONS_DATA_PRUNE')
|
||||
isEnabled: boolean = true;
|
||||
|
||||
/** How old (hours) a finished execution must be to qualify for soft-deletion. */
|
||||
@Env('EXECUTIONS_DATA_MAX_AGE')
|
||||
maxAge: number = 336;
|
||||
|
||||
/**
|
||||
* Max number of finished executions to keep in database. Does not necessarily
|
||||
* prune to the exact max number. `0` for unlimited.
|
||||
*/
|
||||
@Env('EXECUTIONS_DATA_PRUNE_MAX_COUNT')
|
||||
maxCount: number = 10_000;
|
||||
|
||||
/**
|
||||
* How old (hours) a finished execution must be to qualify for hard-deletion.
|
||||
* This buffer by default excludes recent executions as the user may need
|
||||
* them while building a workflow.
|
||||
*/
|
||||
@Env('EXECUTIONS_DATA_HARD_DELETE_BUFFER')
|
||||
hardDeleteBuffer: number = 1;
|
||||
|
||||
/** How often (minutes) execution data should be hard-deleted. */
|
||||
@Env('EXECUTIONS_DATA_PRUNE_HARD_DELETE_INTERVAL')
|
||||
hardDeleteInterval: number = 15;
|
||||
|
||||
/** How often (minutes) execution data should be soft-deleted */
|
||||
@Env('EXECUTIONS_DATA_PRUNE_SOFT_DELETE_INTERVAL')
|
||||
softDeleteInterval: number = 60;
|
||||
}
|
|
@ -1,10 +1,21 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
/**
|
||||
* Whether to enable task runners and how to run them
|
||||
* - internal_childprocess: Task runners are run as a child process and launched by n8n
|
||||
* - internal_launcher: Task runners are run as a child process and launched by n8n using a separate launch program
|
||||
* - external: Task runners are run as a separate program not launched by n8n
|
||||
*/
|
||||
export type TaskRunnerMode = 'internal_childprocess' | 'internal_launcher' | 'external';
|
||||
|
||||
@Config
|
||||
export class TaskRunnersConfig {
|
||||
@Env('N8N_RUNNERS_ENABLED')
|
||||
enabled: boolean = false;
|
||||
|
||||
// Defaults to true for now
|
||||
@Env('N8N_RUNNERS_DISABLED')
|
||||
disabled: boolean = true;
|
||||
@Env('N8N_RUNNERS_MODE')
|
||||
mode: TaskRunnerMode = 'internal_childprocess';
|
||||
|
||||
@Env('N8N_RUNNERS_PATH')
|
||||
path: string = '/runners';
|
||||
|
@ -18,5 +29,28 @@ export class TaskRunnersConfig {
|
|||
|
||||
/** IP address task runners server should listen on */
|
||||
@Env('N8N_RUNNERS_SERVER_LISTEN_ADDRESS')
|
||||
listen_address: string = '127.0.0.1';
|
||||
listenAddress: string = '127.0.0.1';
|
||||
|
||||
/** Maximum size of a payload sent to the runner in bytes, Default 1G */
|
||||
@Env('N8N_RUNNERS_MAX_PAYLOAD')
|
||||
maxPayload: number = 1024 * 1024 * 1024;
|
||||
|
||||
@Env('N8N_RUNNERS_LAUNCHER_PATH')
|
||||
launcherPath: string = '';
|
||||
|
||||
/** Which task runner to launch from the config */
|
||||
@Env('N8N_RUNNERS_LAUNCHER_RUNNER')
|
||||
launcherRunner: string = 'javascript';
|
||||
|
||||
/** The --max-old-space-size option to use for the runner (in MB). Default means node.js will determine it based on the available memory. */
|
||||
@Env('N8N_RUNNERS_MAX_OLD_SPACE_SIZE')
|
||||
maxOldSpaceSize: string = '';
|
||||
|
||||
/** How many concurrent tasks can a runner execute at a time */
|
||||
@Env('N8N_RUNNERS_MAX_CONCURRENCY')
|
||||
maxConcurrency: number = 5;
|
||||
|
||||
/** Should the output of deduplication be asserted for correctness */
|
||||
@Env('N8N_RUNNERS_ASSERT_DEDUPLICATION_OUTPUT')
|
||||
assertDeduplicationOutput: boolean = false;
|
||||
}
|
||||
|
|
|
@ -82,10 +82,6 @@ class BullConfig {
|
|||
@Nested
|
||||
redis: RedisConfig;
|
||||
|
||||
/** How often (in seconds) to poll the Bull queue to identify executions finished during a Redis crash. `0` to disable. May increase Redis traffic significantly. */
|
||||
@Env('QUEUE_RECOVERY_INTERVAL')
|
||||
queueRecoveryInterval: number = 60; // watchdog interval
|
||||
|
||||
/** @deprecated How long (in seconds) a worker must wait for active executions to finish before exiting. Use `N8N_GRACEFUL_SHUTDOWN_TIMEOUT` instead */
|
||||
@Env('QUEUE_WORKER_TIMEOUT')
|
||||
gracefulShutdownTimeout: number = 30;
|
||||
|
|
27
packages/@n8n/config/src/configs/security.config.ts
Normal file
27
packages/@n8n/config/src/configs/security.config.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class SecurityConfig {
|
||||
/**
|
||||
* Which directories to limit n8n's access to. Separate multiple dirs with semicolon `;`.
|
||||
*
|
||||
* @example N8N_RESTRICT_FILE_ACCESS_TO=/home/user/.n8n;/home/user/n8n-data
|
||||
*/
|
||||
@Env('N8N_RESTRICT_FILE_ACCESS_TO')
|
||||
restrictFileAccessTo: string = '';
|
||||
|
||||
/**
|
||||
* Whether to block access to all files at:
|
||||
* - the ".n8n" directory,
|
||||
* - the static cache dir at ~/.cache/n8n/public, and
|
||||
* - user-defined config files.
|
||||
*/
|
||||
@Env('N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES')
|
||||
blockFileAccessToN8nFiles: boolean = true;
|
||||
|
||||
/**
|
||||
* In a [security audit](https://docs.n8n.io/hosting/securing/security-audit/), how many days for a workflow to be considered abandoned if not executed.
|
||||
*/
|
||||
@Env('N8N_SECURITY_AUDIT_DAYS_ABANDONED_WORKFLOW')
|
||||
daysAbandonedWorkflow: number = 90;
|
||||
}
|
|
@ -5,12 +5,16 @@ import { EndpointsConfig } from './configs/endpoints.config';
|
|||
import { EventBusConfig } from './configs/event-bus.config';
|
||||
import { ExternalSecretsConfig } from './configs/external-secrets.config';
|
||||
import { ExternalStorageConfig } from './configs/external-storage.config';
|
||||
import { GenericConfig } from './configs/generic.config';
|
||||
import { LicenseConfig } from './configs/license.config';
|
||||
import { LoggingConfig } from './configs/logging.config';
|
||||
import { MultiMainSetupConfig } from './configs/multi-main-setup.config';
|
||||
import { NodesConfig } from './configs/nodes.config';
|
||||
import { PruningConfig } from './configs/pruning.config';
|
||||
import { PublicApiConfig } from './configs/public-api.config';
|
||||
import { TaskRunnersConfig } from './configs/runners.config';
|
||||
export { TaskRunnersConfig } from './configs/runners.config';
|
||||
import { ScalingModeConfig } from './configs/scaling-mode.config';
|
||||
import { SecurityConfig } from './configs/security.config';
|
||||
import { SentryConfig } from './configs/sentry.config';
|
||||
import { TemplatesConfig } from './configs/templates.config';
|
||||
import { UserManagementConfig } from './configs/user-management.config';
|
||||
|
@ -18,6 +22,11 @@ import { VersionNotificationsConfig } from './configs/version-notifications.conf
|
|||
import { WorkflowsConfig } from './configs/workflows.config';
|
||||
import { Config, Env, Nested } from './decorators';
|
||||
|
||||
export { Config, Env, Nested } from './decorators';
|
||||
export { TaskRunnersConfig } from './configs/runners.config';
|
||||
export { SecurityConfig } from './configs/security.config';
|
||||
export { PruningConfig } from './configs/pruning.config';
|
||||
export { FrontendBetaFeatures, FrontendConfig } from './configs/frontend.config';
|
||||
export { LOG_SCOPES } from './configs/logging.config';
|
||||
export type { LogScope } from './configs/logging.config';
|
||||
|
||||
|
@ -93,4 +102,19 @@ export class GlobalConfig {
|
|||
|
||||
@Nested
|
||||
taskRunners: TaskRunnersConfig;
|
||||
|
||||
@Nested
|
||||
multiMainSetup: MultiMainSetupConfig;
|
||||
|
||||
@Nested
|
||||
generic: GenericConfig;
|
||||
|
||||
@Nested
|
||||
license: LicenseConfig;
|
||||
|
||||
@Nested
|
||||
security: SecurityConfig;
|
||||
|
||||
@Nested
|
||||
pruning: PruningConfig;
|
||||
}
|
||||
|
|
|
@ -211,7 +211,6 @@ describe('GlobalConfig', () => {
|
|||
clusterNodes: '',
|
||||
tls: false,
|
||||
},
|
||||
queueRecoveryInterval: 60,
|
||||
gracefulShutdownTimeout: 30,
|
||||
prefix: 'bull',
|
||||
settings: {
|
||||
|
@ -223,11 +222,18 @@ describe('GlobalConfig', () => {
|
|||
},
|
||||
},
|
||||
taskRunners: {
|
||||
disabled: true,
|
||||
enabled: false,
|
||||
mode: 'internal_childprocess',
|
||||
path: '/runners',
|
||||
authToken: '',
|
||||
listen_address: '127.0.0.1',
|
||||
listenAddress: '127.0.0.1',
|
||||
maxPayload: 1024 * 1024 * 1024,
|
||||
port: 5679,
|
||||
launcherPath: '',
|
||||
launcherRunner: 'javascript',
|
||||
maxOldSpaceSize: '',
|
||||
maxConcurrency: 5,
|
||||
assertDeduplicationOutput: false,
|
||||
},
|
||||
sentry: {
|
||||
backendDsn: '',
|
||||
|
@ -243,6 +249,37 @@ describe('GlobalConfig', () => {
|
|||
},
|
||||
scopes: [],
|
||||
},
|
||||
multiMainSetup: {
|
||||
enabled: false,
|
||||
ttl: 10,
|
||||
interval: 3,
|
||||
},
|
||||
generic: {
|
||||
timezone: 'America/New_York',
|
||||
releaseChannel: 'dev',
|
||||
gracefulShutdownTimeout: 30,
|
||||
},
|
||||
license: {
|
||||
serverUrl: 'https://license.n8n.io/v1',
|
||||
autoRenewalEnabled: true,
|
||||
autoRenewOffset: 60 * 60 * 72,
|
||||
activationKey: '',
|
||||
tenantId: 1,
|
||||
cert: '',
|
||||
},
|
||||
security: {
|
||||
restrictFileAccessTo: '',
|
||||
blockFileAccessToN8nFiles: true,
|
||||
daysAbandonedWorkflow: 90,
|
||||
},
|
||||
pruning: {
|
||||
isEnabled: true,
|
||||
maxAge: 336,
|
||||
maxCount: 10_000,
|
||||
hardDeleteBuffer: 1,
|
||||
hardDeleteInterval: 15,
|
||||
softDeleteInterval: 60,
|
||||
},
|
||||
};
|
||||
|
||||
it('should use all default values when no env variables are defined', () => {
|
||||
|
|
21
packages/@n8n/json-schema-to-zod/.eslintrc.js
Normal file
21
packages/@n8n/json-schema-to-zod/.eslintrc.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
const sharedOptions = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/node'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
ignorePatterns: ['jest.config.js'],
|
||||
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
'@typescript-eslint/no-duplicate-imports': 'off',
|
||||
'import/no-cycle': 'off',
|
||||
'n8n-local-rules/no-plain-errors': 'off',
|
||||
|
||||
complexity: 'error',
|
||||
},
|
||||
};
|
4
packages/@n8n/json-schema-to-zod/.gitignore
vendored
Normal file
4
packages/@n8n/json-schema-to-zod/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
coverage
|
||||
test/output
|
3
packages/@n8n/json-schema-to-zod/.npmignore
Normal file
3
packages/@n8n/json-schema-to-zod/.npmignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
src
|
||||
tsconfig*
|
||||
test
|
16
packages/@n8n/json-schema-to-zod/LICENSE
Normal file
16
packages/@n8n/json-schema-to-zod/LICENSE
Normal file
|
@ -0,0 +1,16 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) 2024, n8n
|
||||
Copyright (c) 2021, Stefan Terdell
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
34
packages/@n8n/json-schema-to-zod/README.md
Normal file
34
packages/@n8n/json-schema-to-zod/README.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Json-Schema-to-Zod
|
||||
|
||||
A package to convert JSON schema (draft 4+) objects into Zod schemas in the form of Zod objects at runtime.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install @n8n/json-schema-to-zod
|
||||
```
|
||||
|
||||
### Simple example
|
||||
|
||||
```typescript
|
||||
import { jsonSchemaToZod } from "json-schema-to-zod";
|
||||
|
||||
const jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
hello: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const zodSchema = jsonSchemaToZod(myObject);
|
||||
```
|
||||
|
||||
### Overriding a parser
|
||||
|
||||
You can pass a function to the `overrideParser` option, which represents a function that receives the current schema node and the reference object, and should return a zod object when it wants to replace a default output. If the default output should be used for the node just return undefined.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This is a fork of [`json-schema-to-zod`](https://github.com/StefanTerdell/json-schema-to-zod).
|
5
packages/@n8n/json-schema-to-zod/jest.config.js
Normal file
5
packages/@n8n/json-schema-to-zod/jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...require('../../../jest.config'),
|
||||
setupFilesAfterEnv: ['<rootDir>/test/extend-expect.ts'],
|
||||
};
|
68
packages/@n8n/json-schema-to-zod/package.json
Normal file
68
packages/@n8n/json-schema-to-zod/package.json
Normal file
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"name": "@n8n/json-schema-to-zod",
|
||||
"version": "1.1.0",
|
||||
"description": "Converts JSON schema objects into Zod schemas",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"exports": {
|
||||
"import": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/cjs/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w",
|
||||
"format": "biome format --write src",
|
||||
"format:check": "biome ci src",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"build:types": "tsc -p tsconfig.types.json",
|
||||
"build:cjs": "tsc -p tsconfig.cjs.json && node postcjs.js",
|
||||
"build:esm": "tsc -p tsconfig.esm.json && node postesm.js",
|
||||
"build": "rimraf ./dist && pnpm run build:types && pnpm run build:cjs && pnpm run build:esm",
|
||||
"dry": "pnpm run build && pnpm pub --dry-run",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"keywords": [
|
||||
"zod",
|
||||
"json",
|
||||
"schema",
|
||||
"converter",
|
||||
"cli"
|
||||
],
|
||||
"author": "Stefan Terdell",
|
||||
"contributors": [
|
||||
"Chen (https://github.com/werifu)",
|
||||
"Nuno Carduso (https://github.com/ncardoso-barracuda)",
|
||||
"Lars Strojny (https://github.com/lstrojny)",
|
||||
"Navtoj Chahal (https://github.com/navtoj)",
|
||||
"Ben McCann (https://github.com/benmccann)",
|
||||
"Dmitry Zakharov (https://github.com/DZakh)",
|
||||
"Michel Turpin (https://github.com/grimly)",
|
||||
"David Barratt (https://github.com/davidbarratt)",
|
||||
"pevisscher (https://github.com/pevisscher)",
|
||||
"Aidin Abedi (https://github.com/aidinabedi)",
|
||||
"Brett Zamir (https://github.com/brettz9)",
|
||||
"n8n (https://github.com/n8n-io)"
|
||||
],
|
||||
"license": "ISC",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/n8n-io/n8n"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
1
packages/@n8n/json-schema-to-zod/postcjs.js
Normal file
1
packages/@n8n/json-schema-to-zod/postcjs.js
Normal file
|
@ -0,0 +1 @@
|
|||
require('fs').writeFileSync('./dist/cjs/package.json', '{"type":"commonjs"}', 'utf-8');
|
1
packages/@n8n/json-schema-to-zod/postesm.js
Normal file
1
packages/@n8n/json-schema-to-zod/postesm.js
Normal file
|
@ -0,0 +1 @@
|
|||
require('fs').writeFileSync('./dist/esm/package.json', '{"type":"module"}', 'utf-8');
|
2
packages/@n8n/json-schema-to-zod/src/index.ts
Normal file
2
packages/@n8n/json-schema-to-zod/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export type * from './types';
|
||||
export { jsonSchemaToZod } from './json-schema-to-zod.js';
|
15
packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts
Normal file
15
packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parsers/parse-schema';
|
||||
import type { JsonSchemaToZodOptions, JsonSchema } from './types';
|
||||
|
||||
export const jsonSchemaToZod = <T extends z.ZodTypeAny = z.ZodTypeAny>(
|
||||
schema: JsonSchema,
|
||||
options: JsonSchemaToZodOptions = {},
|
||||
): T => {
|
||||
return parseSchema(schema, {
|
||||
path: [],
|
||||
seen: new Map(),
|
||||
...options,
|
||||
}) as T;
|
||||
};
|
46
packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts
Normal file
46
packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
import { half } from '../utils/half';
|
||||
|
||||
const originalIndex = Symbol('Original index');
|
||||
|
||||
const ensureOriginalIndex = (arr: JsonSchema[]) => {
|
||||
const newArr = [];
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const item = arr[i];
|
||||
if (typeof item === 'boolean') {
|
||||
newArr.push(item ? { [originalIndex]: i } : { [originalIndex]: i, not: {} });
|
||||
} else if (originalIndex in item) {
|
||||
return arr;
|
||||
} else {
|
||||
newArr.push({ ...item, [originalIndex]: i });
|
||||
}
|
||||
}
|
||||
|
||||
return newArr;
|
||||
};
|
||||
|
||||
export function parseAllOf(
|
||||
jsonSchema: JsonSchemaObject & { allOf: JsonSchema[] },
|
||||
refs: Refs,
|
||||
): z.ZodTypeAny {
|
||||
if (jsonSchema.allOf.length === 0) {
|
||||
return z.never();
|
||||
}
|
||||
|
||||
if (jsonSchema.allOf.length === 1) {
|
||||
const item = jsonSchema.allOf[0];
|
||||
|
||||
return parseSchema(item, {
|
||||
...refs,
|
||||
path: [...refs.path, 'allOf', (item as never)[originalIndex]],
|
||||
});
|
||||
}
|
||||
|
||||
const [left, right] = half(ensureOriginalIndex(jsonSchema.allOf));
|
||||
|
||||
return z.intersection(parseAllOf({ allOf: left }, refs), parseAllOf({ allOf: right }, refs));
|
||||
}
|
19
packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts
Normal file
19
packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
|
||||
export const parseAnyOf = (jsonSchema: JsonSchemaObject & { anyOf: JsonSchema[] }, refs: Refs) => {
|
||||
return jsonSchema.anyOf.length
|
||||
? jsonSchema.anyOf.length === 1
|
||||
? parseSchema(jsonSchema.anyOf[0], {
|
||||
...refs,
|
||||
path: [...refs.path, 'anyOf', 0],
|
||||
})
|
||||
: z.union(
|
||||
jsonSchema.anyOf.map((schema, i) =>
|
||||
parseSchema(schema, { ...refs, path: [...refs.path, 'anyOf', i] }),
|
||||
) as [z.ZodTypeAny, z.ZodTypeAny],
|
||||
)
|
||||
: z.any();
|
||||
};
|
34
packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts
Normal file
34
packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, Refs } from '../types';
|
||||
import { extendSchemaWithMessage } from '../utils/extend-schema';
|
||||
|
||||
export const parseArray = (jsonSchema: JsonSchemaObject & { type: 'array' }, refs: Refs) => {
|
||||
if (Array.isArray(jsonSchema.items)) {
|
||||
return z.tuple(
|
||||
jsonSchema.items.map((v, i) =>
|
||||
parseSchema(v, { ...refs, path: [...refs.path, 'items', i] }),
|
||||
) as [z.ZodTypeAny],
|
||||
);
|
||||
}
|
||||
|
||||
let zodSchema = !jsonSchema.items
|
||||
? z.array(z.any())
|
||||
: z.array(parseSchema(jsonSchema.items, { ...refs, path: [...refs.path, 'items'] }));
|
||||
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'minItems',
|
||||
(zs, minItems, errorMessage) => zs.min(minItems, errorMessage),
|
||||
);
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'maxItems',
|
||||
(zs, maxItems, errorMessage) => zs.max(maxItems, errorMessage),
|
||||
);
|
||||
|
||||
return zodSchema;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
|
||||
export const parseBoolean = (_jsonSchema: JsonSchemaObject & { type: 'boolean' }) => {
|
||||
return z.boolean();
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject, Serializable } from '../types';
|
||||
|
||||
export const parseConst = (jsonSchema: JsonSchemaObject & { const: Serializable }) => {
|
||||
return z.literal(jsonSchema.const as z.Primitive);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
|
||||
export const parseDefault = (_jsonSchema: JsonSchemaObject) => {
|
||||
return z.any();
|
||||
};
|
25
packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts
Normal file
25
packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject, Serializable } from '../types';
|
||||
|
||||
export const parseEnum = (jsonSchema: JsonSchemaObject & { enum: Serializable[] }) => {
|
||||
if (jsonSchema.enum.length === 0) {
|
||||
return z.never();
|
||||
}
|
||||
|
||||
if (jsonSchema.enum.length === 1) {
|
||||
// union does not work when there is only one element
|
||||
return z.literal(jsonSchema.enum[0] as z.Primitive);
|
||||
}
|
||||
|
||||
if (jsonSchema.enum.every((x) => typeof x === 'string')) {
|
||||
return z.enum(jsonSchema.enum as [string]);
|
||||
}
|
||||
|
||||
return z.union(
|
||||
jsonSchema.enum.map((x) => z.literal(x as z.Primitive)) as unknown as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
],
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
|
||||
export const parseIfThenElse = (
|
||||
jsonSchema: JsonSchemaObject & {
|
||||
if: JsonSchema;
|
||||
then: JsonSchema;
|
||||
else: JsonSchema;
|
||||
},
|
||||
refs: Refs,
|
||||
) => {
|
||||
const $if = parseSchema(jsonSchema.if, { ...refs, path: [...refs.path, 'if'] });
|
||||
const $then = parseSchema(jsonSchema.then, {
|
||||
...refs,
|
||||
path: [...refs.path, 'then'],
|
||||
});
|
||||
const $else = parseSchema(jsonSchema.else, {
|
||||
...refs,
|
||||
path: [...refs.path, 'else'],
|
||||
});
|
||||
|
||||
return z.union([$then, $else]).superRefine((value, ctx) => {
|
||||
const result = $if.safeParse(value).success ? $then.safeParse(value) : $else.safeParse(value);
|
||||
|
||||
if (!result.success) {
|
||||
result.error.errors.forEach((error) => ctx.addIssue(error));
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchema, JsonSchemaObject, Refs } from '../types';
|
||||
|
||||
export const parseMultipleType = (
|
||||
jsonSchema: JsonSchemaObject & { type: string[] },
|
||||
refs: Refs,
|
||||
) => {
|
||||
return z.union(
|
||||
jsonSchema.type.map((type) => parseSchema({ ...jsonSchema, type } as JsonSchema, refs)) as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
],
|
||||
);
|
||||
};
|
15
packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts
Normal file
15
packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
|
||||
export const parseNot = (jsonSchema: JsonSchemaObject & { not: JsonSchema }, refs: Refs) => {
|
||||
return z.any().refine(
|
||||
(value) =>
|
||||
!parseSchema(jsonSchema.not, {
|
||||
...refs,
|
||||
path: [...refs.path, 'not'],
|
||||
}).safeParse(value).success,
|
||||
'Invalid input: Should NOT be valid against schema',
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
|
||||
export const parseNull = (_jsonSchema: JsonSchemaObject & { type: 'null' }) => {
|
||||
return z.null();
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, Refs } from '../types';
|
||||
import { omit } from '../utils/omit';
|
||||
|
||||
/**
|
||||
* For compatibility with open api 3.0 nullable
|
||||
*/
|
||||
export const parseNullable = (jsonSchema: JsonSchemaObject & { nullable: true }, refs: Refs) => {
|
||||
return parseSchema(omit(jsonSchema, 'nullable'), refs, true).nullable();
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue