diff --git a/.github/test-metrics/playwright.json b/.github/test-metrics/playwright.json index b398ada3cfa..13143f003b8 100644 --- a/.github/test-metrics/playwright.json +++ b/.github/test-metrics/playwright.json @@ -1,732 +1,782 @@ { - "updatedAt": "2026-01-27T22:20:54.780Z", + "updatedAt": "2026-03-03T14:06:03.725Z", "source": "currents", "projectId": "LRxcNt", "specs": { "tests/e2e/projects/projects.spec.ts": { - "avgDuration": 181938, - "testCount": 9, - "flakyRate": 0.1222 + "avgDuration": 146428, + "testCount": 7, + "flakyRate": 0.0269 }, "tests/e2e/workflows/editor/canvas/actions.spec.ts": { - "avgDuration": 136685, + "avgDuration": 132050, "testCount": 20, - "flakyRate": 0.0042 + "flakyRate": 0.0071 }, "tests/e2e/credentials/crud.spec.ts": { - "avgDuration": 126381, + "avgDuration": 120000, "testCount": 14, - "flakyRate": 0.0028 - }, - "tests/e2e/sharing/credential-visibility.spec.ts": { - "avgDuration": 112068, - "testCount": 5, - "flakyRate": 0.1 + "flakyRate": 0 }, "tests/e2e/data-tables/tables.spec.ts": { - "avgDuration": 110045, + "avgDuration": 117860, "testCount": 7, - "flakyRate": 0.0014 - }, - "tests/e2e/workflows/editor/code/code-node.spec.ts": { - "avgDuration": 99751, - "testCount": 12, - "flakyRate": 0.0437 - }, - "tests/e2e/data-tables/details.spec.ts": { - "avgDuration": 99274, - "testCount": 11, - "flakyRate": 0.0014 - }, - "tests/e2e/ai/langchain-agents.spec.ts": { - "avgDuration": 98947, - "testCount": 7, - "flakyRate": 0.0139 - }, - "tests/e2e/workflows/editor/canvas/undo-redo.spec.ts": { - "avgDuration": 96500, - "testCount": 14, - "flakyRate": 0 - }, - "tests/e2e/ai/assistant-basic.spec.ts": { - "avgDuration": 95751, - "testCount": 11, - "flakyRate": 0.0085 + "flakyRate": 0.0054 }, "tests/e2e/workflows/list/workflows.spec.ts": { - "avgDuration": 93615, + "avgDuration": 110286, "testCount": 9, - "flakyRate": 0.0323 - }, - "tests/e2e/workflows/editor/ndv/ndv-core.spec.ts": { - "avgDuration": 93461, - "testCount": 14, - "flakyRate": 0.0225 + "flakyRate": 0.0143 }, "tests/e2e/workflows/editor/canvas/canvas-nodes.spec.ts": { - "avgDuration": 91986, + "avgDuration": 106230, "testCount": 8, - "flakyRate": 0.0811 + "flakyRate": 0.2183 }, - "tests/e2e/workflows/editor/canvas/canvas-zoom.spec.ts": { - "avgDuration": 91383, + "tests/e2e/workflows/editor/code/code-node.spec.ts": { + "avgDuration": 104346, "testCount": 12, - "flakyRate": 0.0506 + "flakyRate": 0.1071 }, - "tests/e2e/workflows/editor/code/editors.spec.ts": { - "avgDuration": 90065, + "tests/e2e/ai/assistant-basic.spec.ts": { + "avgDuration": 104278, "testCount": 11, - "flakyRate": 0.0266 - }, - "tests/e2e/workflows/editor/ndv/ndv-data-display.spec.ts": { - "avgDuration": 89783, - "testCount": 11, - "flakyRate": 0.0296 - }, - "tests/e2e/projects/folders-operations.spec.ts": { - "avgDuration": 86349, - "testCount": 14, - "flakyRate": 0.0056 + "flakyRate": 0.0143 }, "tests/e2e/settings/personal/two-factor-authentication.spec.ts": { - "avgDuration": 85085, + "avgDuration": 103362, "testCount": 7, - "flakyRate": 0.0042 + "flakyRate": 0.0036 }, - "tests/e2e/nodes/webhook.spec.ts": { - "avgDuration": 84386, - "testCount": 9, - "flakyRate": 0.0112 - }, - "tests/e2e/workflows/executions/list.spec.ts": { - "avgDuration": 79256, + "tests/e2e/data-tables/details.spec.ts": { + "avgDuration": 102518, "testCount": 11, - "flakyRate": 0.1491 + "flakyRate": 0.0036 }, - "tests/e2e/workflows/editor/execution/debug.spec.ts": { - "avgDuration": 76887, - "testCount": 4, - "flakyRate": 0.0628 + "tests/e2e/workflows/editor/canvas/canvas-zoom.spec.ts": { + "avgDuration": 98829, + "testCount": 13, + "flakyRate": 0.0698 }, - "tests/e2e/building-blocks/node-details-configuration.spec.ts": { - "avgDuration": 73937, + "tests/e2e/workflows/editor/canvas/undo-redo.spec.ts": { + "avgDuration": 98612, + "testCount": 14, + "flakyRate": 0.0018 + }, + "tests/e2e/ai/langchain-agents.spec.ts": { + "avgDuration": 97616, "testCount": 7, - "flakyRate": 0.007 + "flakyRate": 0.0215 + }, + "tests/e2e/workflows/editor/ndv/ndv-data-display.spec.ts": { + "avgDuration": 91228, + "testCount": 11, + "flakyRate": 0.0477 + }, + "tests/e2e/workflows/editor/ndv/ndv-core.spec.ts": { + "avgDuration": 91044, + "testCount": 14, + "flakyRate": 0.0036 }, "tests/e2e/auth/oidc.spec.ts": { - "avgDuration": 69764, + "avgDuration": 90276, "testCount": 1, - "flakyRate": 0.0239 + "flakyRate": 0.0214 }, - "tests/e2e/workflows/editor/ndv/ndv-parameters.spec.ts": { - "avgDuration": 68318, - "testCount": 9, - "flakyRate": 0.0042 + "tests/e2e/workflows/editor/code/editors.spec.ts": { + "avgDuration": 87228, + "testCount": 11, + "flakyRate": 0.0036 }, - "tests/e2e/workflows/editor/execution/execution.spec.ts": { - "avgDuration": 68225, + "tests/e2e/projects/folders-operations.spec.ts": { + "avgDuration": 80775, "testCount": 14, - "flakyRate": 0.0254 + "flakyRate": 0.009 }, - "tests/e2e/building-blocks/credentials.spec.ts": { - "avgDuration": 65277, - "testCount": 6, - "flakyRate": 0.0084 - }, - "tests/e2e/workflows/editor/execution/logs.spec.ts": { - "avgDuration": 64068, - "testCount": 8, - "flakyRate": 0.0226 - }, - "tests/e2e/cloud/cloud.spec.ts": { - "avgDuration": 63135, - "testCount": 3, - "flakyRate": 0.0294 - }, - "tests/e2e/workflows/editor/ndv/pinning.spec.ts": { - "avgDuration": 62923, - "testCount": 10, - "flakyRate": 0.0112 - }, - "tests/e2e/ai/hitl-for-tools.spec.ts": { - "avgDuration": 60802, - "testCount": 2, - "flakyRate": 0.0044 - }, - "tests/e2e/workflows/editor/expressions/mapping.spec.ts": { - "avgDuration": 60442, - "testCount": 10, - "flakyRate": 0.0014 - }, - "tests/e2e/projects/project-settings.spec.ts": { - "avgDuration": 60360, - "testCount": 8, - "flakyRate": 0 - }, - "tests/e2e/nodes/form-trigger-node.spec.ts": { - "avgDuration": 59348, - "testCount": 5, - "flakyRate": 0.0056 - }, - "tests/e2e/workflows/editor/subworkflows/extraction.spec.ts": { - "avgDuration": 59314, - "testCount": 3, - "flakyRate": 0.0014 + "tests/e2e/nodes/webhook.spec.ts": { + "avgDuration": 80112, + "testCount": 9, + "flakyRate": 0.0143 }, "tests/e2e/workflows/templates/credentials-setup.spec.ts": { - "avgDuration": 56552, + "avgDuration": 79929, "testCount": 8, - "flakyRate": 0.0098 + "flakyRate": 0.0178 }, - "tests/e2e/workflows/editor/expressions/modal.spec.ts": { - "avgDuration": 55733, - "testCount": 6, - "flakyRate": 0.0141 + "tests/e2e/ai/langchain-chains.spec.ts": { + "avgDuration": 77134, + "testCount": 4, + "flakyRate": 0.0179 }, - "tests/e2e/workflows/editor/routing.spec.ts": { - "avgDuration": 52723, - "testCount": 6, - "flakyRate": 0.0281 + "tests/e2e/workflows/executions/list.spec.ts": { + "avgDuration": 75367, + "testCount": 9, + "flakyRate": 0.2228 }, - "tests/e2e/workflows/editor/expressions/inline.spec.ts": { - "avgDuration": 51958, - "testCount": 7, - "flakyRate": 0.0521 + "tests/e2e/ai/hitl-for-tools.spec.ts": { + "avgDuration": 75127, + "testCount": 2, + "flakyRate": 0.0215 + }, + "tests/e2e/workflows/editor/execution/execution.spec.ts": { + "avgDuration": 72856, + "testCount": 14, + "flakyRate": 0.0623 + }, + "tests/e2e/sharing/credential-visibility.spec.ts": { + "avgDuration": 71778, + "testCount": 5, + "flakyRate": 0.0107 + }, + "tests/e2e/workflows/editor/ndv/ndv-parameters.spec.ts": { + "avgDuration": 70303, + "testCount": 9, + "flakyRate": 0.0125 }, "tests/e2e/ai/assistant-credential-help.spec.ts": { - "avgDuration": 51745, + "avgDuration": 67106, "testCount": 4, - "flakyRate": 0.0042 - }, - "tests/e2e/chat-hub/chat-hub-attachment.spec.ts": { - "avgDuration": 50885, - "testCount": 3, - "flakyRate": 0.1351 - }, - "tests/e2e/projects/folders-basic.spec.ts": { - "avgDuration": 50217, - "testCount": 11, - "flakyRate": 0.007 + "flakyRate": 0.0143 }, "tests/e2e/ai/assistant-code-help.spec.ts": { - "avgDuration": 50056, + "avgDuration": 65391, "testCount": 2, - "flakyRate": 0.0127 - }, - "tests/e2e/ai/assistant-support-chat.spec.ts": { - "avgDuration": 48597, - "testCount": 3, - "flakyRate": 0.0085 - }, - "tests/e2e/building-blocks/canvas-actions.spec.ts": { - "avgDuration": 47884, - "testCount": 9, - "flakyRate": 0.0028 - }, - "tests/e2e/workflows/editor/ndv/paired-item.spec.ts": { - "avgDuration": 47692, - "testCount": 5, - "flakyRate": 0.007 - }, - "tests/e2e/chat-hub/chat-hub-workflow-agent.spec.ts": { - "avgDuration": 47588, - "testCount": 2, - "flakyRate": 0.2061 - }, - "tests/e2e/workflows/editor/expressions/transformation.spec.ts": { - "avgDuration": 47442, - "testCount": 6, - "flakyRate": 0 - }, - "tests/e2e/workflows/editor/ndv/resource-locator.spec.ts": { - "avgDuration": 46601, - "testCount": 7, - "flakyRate": 0 - }, - "tests/e2e/workflows/checklist/production-checklist.spec.ts": { - "avgDuration": 44366, - "testCount": 7, - "flakyRate": 0.0028 - }, - "tests/e2e/settings/log-streaming/log-streaming-observability.spec.ts": { - "avgDuration": 43991, - "testCount": 2, - "flakyRate": 0 - }, - "tests/e2e/projects/folders-advanced.spec.ts": { - "avgDuration": 43435, - "testCount": 6, - "flakyRate": 0 + "flakyRate": 0.0375 }, "tests/e2e/workflows/editor/viewer-permissions.spec.ts": { - "avgDuration": 43417, + "avgDuration": 63815, "testCount": 3, - "flakyRate": 0.0028 + "flakyRate": 0.0071 }, - "tests/e2e/workflows/editor/tags.spec.ts": { - "avgDuration": 42899, + "tests/e2e/workflows/editor/execution/logs.spec.ts": { + "avgDuration": 63194, + "testCount": 8, + "flakyRate": 0.0677 + }, + "tests/e2e/workflows/editor/execution/debug.spec.ts": { + "avgDuration": 63190, + "testCount": 4, + "flakyRate": 0.0357 + }, + "tests/e2e/workflows/editor/ndv/pinning.spec.ts": { + "avgDuration": 63033, + "testCount": 10, + "flakyRate": 0.0268 + }, + "tests/e2e/building-blocks/node-details-configuration.spec.ts": { + "avgDuration": 62896, "testCount": 7, - "flakyRate": 0.0155 + "flakyRate": 0.0143 }, - "tests/e2e/workflows/templates/templates.spec.ts": { - "avgDuration": 42575, + "tests/e2e/ai/assistant-support-chat.spec.ts": { + "avgDuration": 62156, + "testCount": 3, + "flakyRate": 0.0196 + }, + "tests/e2e/workflows/editor/subworkflows/extraction.spec.ts": { + "avgDuration": 60164, + "testCount": 3, + "flakyRate": 0.0018 + }, + "tests/e2e/building-blocks/canvas-actions.spec.ts": { + "avgDuration": 59268, "testCount": 9, - "flakyRate": 0.14 + "flakyRate": 0.0018 }, - "tests/e2e/workflows/editor/workflow-actions/publish.spec.ts": { - "avgDuration": 41405, - "testCount": 7, - "flakyRate": 0.0119 + "tests/e2e/workflows/editor/expressions/mapping.spec.ts": { + "avgDuration": 58896, + "testCount": 10, + "flakyRate": 0.0036 }, - "tests/e2e/nodes/kafka-nodes.spec.ts": { - "avgDuration": 40956, + "tests/e2e/chat-hub/chat-hub-basic.spec.ts": { + "avgDuration": 58724, + "testCount": 3, + "flakyRate": 0.0519 + }, + "tests/e2e/workflows/editor/ndv/paired-item.spec.ts": { + "avgDuration": 58608, + "testCount": 6, + "flakyRate": 0.0555 + }, + "tests/e2e/capabilities/proxy-server.spec.ts": { + "avgDuration": 57636, + "testCount": 4, + "flakyRate": 0.0179 + }, + "tests/e2e/projects/project-settings.spec.ts": { + "avgDuration": 57079, + "testCount": 8, + "flakyRate": 0 + }, + "tests/e2e/building-blocks/credentials.spec.ts": { + "avgDuration": 56988, + "testCount": 6, + "flakyRate": 0.0142 + }, + "tests/e2e/nodes/form-trigger-node.spec.ts": { + "avgDuration": 55372, + "testCount": 5, + "flakyRate": 0.0143 + }, + "tests/e2e/workflows/executions/filter.spec.ts": { + "avgDuration": 54860, "testCount": 2, - "flakyRate": 0.0021 + "flakyRate": 0.4223 }, - "tests/e2e/ai/workflow-builder.spec.ts": { - "avgDuration": 40449, + "tests/e2e/building-blocks/workflow-entry-points.spec.ts": { + "avgDuration": 54615, "testCount": 5, - "flakyRate": 0.0084 + "flakyRate": 0.025 }, - "tests/e2e/workflows/editor/subworkflows/workflow-selector.spec.ts": { - "avgDuration": 39594, - "testCount": 5, + "tests/e2e/dynamic-credentials/execution-status.spec.ts": { + "avgDuration": 54611, + "testCount": 2, "flakyRate": 0.0028 }, - "tests/e2e/app-config/demo.spec.ts": { - "avgDuration": 38950, - "testCount": 4, - "flakyRate": 0.1353 + "tests/e2e/regression/ADO-4462-template-setup-experiment.spec.ts": { + "avgDuration": 54493, + "testCount": 2, + "flakyRate": 0.0108 }, - "tests/e2e/workflows/editor/ndv/ndv-floating-nodes.spec.ts": { - "avgDuration": 37757, - "testCount": 4, + "tests/e2e/node-creator/categories.spec.ts": { + "avgDuration": 53655, + "testCount": 5, + "flakyRate": 0.6301 + }, + "tests/e2e/workflows/editor/expressions/modal.spec.ts": { + "avgDuration": 53254, + "testCount": 6, "flakyRate": 0 }, "tests/e2e/ai/rag-callout.spec.ts": { - "avgDuration": 35912, + "avgDuration": 53211, "testCount": 2, - "flakyRate": 0.0113 + "flakyRate": 0.0197 }, - "tests/e2e/settings/personal/personal.spec.ts": { - "avgDuration": 35307, + "tests/e2e/projects/projects-move-resources.spec.ts": { + "avgDuration": 52538, "testCount": 2, - "flakyRate": 0.007 + "flakyRate": 0.0054 }, - "tests/e2e/chat-hub/chat-hub-basic.spec.ts": { - "avgDuration": 32409, - "testCount": 3, - "flakyRate": 0.1407 - }, - "tests/e2e/api/webhook-external.spec.ts": { - "avgDuration": 30636, - "testCount": 2, - "flakyRate": 0.0113 - }, - "tests/e2e/ai/langchain-vectorstores.spec.ts": { - "avgDuration": 30502, - "testCount": 2, - "flakyRate": 0.0292 - }, - "tests/e2e/ai/langchain-chains.spec.ts": { - "avgDuration": 29786, - "testCount": 4, - "flakyRate": 0 - }, - "tests/e2e/app-config/security-notifications.spec.ts": { - "avgDuration": 29324, + "tests/e2e/auth/authenticated.spec.ts": { + "avgDuration": 51796, "testCount": 5, - "flakyRate": 0.0127 + "flakyRate": 0.0205 }, - "tests/e2e/workflows/editor/ndv/resource-mapper.spec.ts": { - "avgDuration": 29269, - "testCount": 4, - "flakyRate": 0 - }, - "tests/e2e/capabilities/task-runner.spec.ts": { - "avgDuration": 28582, - "testCount": 2, - "flakyRate": 0 - }, - "tests/e2e/auth/password-reset.spec.ts": { - "avgDuration": 28574, - "testCount": 1, - "flakyRate": 0.0084 + "tests/e2e/api/webhook-isolation.spec.ts": { + "avgDuration": 51794, + "testCount": 14, + "flakyRate": 0.0525 }, "tests/e2e/credentials/global.spec.ts": { - "avgDuration": 28384, + "avgDuration": 51699, "testCount": 5, + "flakyRate": 0.0143 + }, + "tests/e2e/nodes/kafka-nodes.spec.ts": { + "avgDuration": 51563, + "testCount": 2, + "flakyRate": 0.018 + }, + "tests/e2e/workflows/editor/routing.spec.ts": { + "avgDuration": 51272, + "testCount": 6, + "flakyRate": 0.0071 + }, + "tests/e2e/app-config/demo.spec.ts": { + "avgDuration": 50675, + "testCount": 4, + "flakyRate": 0.0198 + }, + "tests/e2e/workflows/checklist/production-checklist.spec.ts": { + "avgDuration": 50490, + "testCount": 7, + "flakyRate": 0.0107 + }, + "tests/e2e/workflows/editor/expressions/inline.spec.ts": { + "avgDuration": 50014, + "testCount": 7, + "flakyRate": 0.0323 + }, + "tests/e2e/workflows/editor/expressions/transformation.spec.ts": { + "avgDuration": 49030, + "testCount": 6, + "flakyRate": 0 + }, + "tests/e2e/ai/chat-session.spec.ts": { + "avgDuration": 48206, + "testCount": 1, + "flakyRate": 0.0215 + }, + "tests/e2e/workflows/editor/ndv/resource-locator.spec.ts": { + "avgDuration": 47480, + "testCount": 7, + "flakyRate": 0.0018 + }, + "tests/e2e/projects/folders-basic.spec.ts": { + "avgDuration": 47403, + "testCount": 11, + "flakyRate": 0.0089 + }, + "tests/e2e/settings/log-streaming/log-streaming-observability.spec.ts": { + "avgDuration": 46652, + "testCount": 2, + "flakyRate": 0 + }, + "tests/e2e/chat-hub/chat-hub-attachment.spec.ts": { + "avgDuration": 46545, + "testCount": 3, + "flakyRate": 0.0735 + }, + "tests/e2e/app-config/security-notifications.spec.ts": { + "avgDuration": 44200, + "testCount": 5, + "flakyRate": 0.0071 + }, + "tests/e2e/workflows/editor/tags.spec.ts": { + "avgDuration": 43658, + "testCount": 7, + "flakyRate": 0.0053 + }, + "tests/e2e/ai/workflow-builder.spec.ts": { + "avgDuration": 43520, + "testCount": 5, + "flakyRate": 0.0036 + }, + "tests/e2e/workflows/editor/workflow-actions/publish.spec.ts": { + "avgDuration": 43049, + "testCount": 8, + "flakyRate": 0.0036 + }, + "tests/e2e/workflows/templates/templates.spec.ts": { + "avgDuration": 41875, + "testCount": 9, + "flakyRate": 0.1299 + }, + "tests/e2e/projects/folders-advanced.spec.ts": { + "avgDuration": 40967, + "testCount": 6, + "flakyRate": 0.0018 + }, + "tests/e2e/chat-hub/chat-hub-workflow-agent.spec.ts": { + "avgDuration": 40858, + "testCount": 2, + "flakyRate": 0.0054 + }, + "tests/e2e/workflows/editor/subworkflows/workflow-selector.spec.ts": { + "avgDuration": 39536, + "testCount": 5, + "flakyRate": 0.0053 + }, + "tests/e2e/cloud/cloud.spec.ts": { + "avgDuration": 39055, + "testCount": 3, + "flakyRate": 0.0036 + }, + "tests/e2e/ai/langchain-vectorstores.spec.ts": { + "avgDuration": 36808, + "testCount": 2, + "flakyRate": 0.0894 + }, + "tests/e2e/workflows/editor/ndv/ndv-floating-nodes.spec.ts": { + "avgDuration": 36193, + "testCount": 4, + "flakyRate": 0 + }, + "tests/e2e/settings/external-secrets/aws-secrets-manager.spec.ts": { + "avgDuration": 35223, + "testCount": 1, + "flakyRate": 0.0089 + }, + "tests/e2e/sentry/sentry-baseline.spec.ts": { + "avgDuration": 34042, + "testCount": 3, + "flakyRate": 0.0036 + }, + "tests/e2e/chat-hub/chat-hub-chat-user.spec.ts": { + "avgDuration": 33473, + "testCount": 1, + "flakyRate": 0.0125 + }, + "tests/e2e/auth/password-reset.spec.ts": { + "avgDuration": 28646, + "testCount": 1, + "flakyRate": 0.0089 + }, + "tests/e2e/workflows/editor/ndv/resource-mapper.spec.ts": { + "avgDuration": 28344, + "testCount": 4, + "flakyRate": 0.0107 + }, + "tests/e2e/settings/personal/personal.spec.ts": { + "avgDuration": 28212, + "testCount": 2, + "flakyRate": 0.0036 + }, + "tests/e2e/auth/admin-smoke.spec.ts": { + "avgDuration": 26384, + "testCount": 1, + "flakyRate": 0.0089 + }, + "tests/e2e/nodes/community-nodes.spec.ts": { + "avgDuration": 26234, + "testCount": 3, + "flakyRate": 0.0018 + }, + "tests/e2e/workflows/list/import.spec.ts": { + "avgDuration": 26160, + "testCount": 5, + "flakyRate": 0.0036 + }, + "tests/e2e/settings/environments/variables.spec.ts": { + "avgDuration": 25384, + "testCount": 7, + "flakyRate": 0.0036 + }, + "tests/e2e/workflows/editor/editor-after-route-changes.spec.ts": { + "avgDuration": 25048, + "testCount": 1, + "flakyRate": 0 + }, + "tests/e2e/workflows/editor/subworkflows/debugging.spec.ts": { + "avgDuration": 24663, + "testCount": 4, + "flakyRate": 0.0036 + }, + "tests/e2e/nodes/if-node.spec.ts": { + "avgDuration": 24306, + "testCount": 2, + "flakyRate": 0.066 + }, + "tests/e2e/app-config/env-feature-flags.spec.ts": { + "avgDuration": 23869, + "testCount": 2, + "flakyRate": 0.0036 + }, + "tests/e2e/node-creator/navigation.spec.ts": { + "avgDuration": 23617, + "testCount": 4, + "flakyRate": 0.0036 + }, + "tests/e2e/settings/log-streaming/log-streaming.spec.ts": { + "avgDuration": 23611, + "testCount": 5, + "flakyRate": 0.0018 + }, + "tests/e2e/node-creator/actions.spec.ts": { + "avgDuration": 22871, + "testCount": 4, + "flakyRate": 0.0036 + }, + "tests/e2e/nodes/schedule-trigger-node.spec.ts": { + "avgDuration": 22699, + "testCount": 1, + "flakyRate": 0.0159 + }, + "tests/e2e/workflows/editor/execution/inject-previous.spec.ts": { + "avgDuration": 22386, + "testCount": 2, + "flakyRate": 0 + }, + "tests/e2e/chat-hub/chat-hub-personal-agent.spec.ts": { + "avgDuration": 21180, + "testCount": 2, + "flakyRate": 0.0072 + }, + "tests/e2e/building-blocks/user-service.spec.ts": { + "avgDuration": 20964, + "testCount": 8, + "flakyRate": 0.0018 + }, + "tests/e2e/sharing/access-control.spec.ts": { + "avgDuration": 20638, + "testCount": 5, + "flakyRate": 0.0018 + }, + "tests/e2e/capabilities/task-runner.spec.ts": { + "avgDuration": 19897, + "testCount": 2, "flakyRate": 0 }, "tests/e2e/chat-hub/chat-hub-settings.spec.ts": { - "avgDuration": 27248, + "avgDuration": 19840, "testCount": 2, - "flakyRate": 0.1281 + "flakyRate": 0.0323 }, - "tests/e2e/settings/environments/variables.spec.ts": { - "avgDuration": 27115, - "testCount": 7, - "flakyRate": 0.0028 - }, - "tests/e2e/nodes/community-nodes.spec.ts": { - "avgDuration": 26771, - "testCount": 3, - "flakyRate": 0 - }, - "tests/e2e/workflows/editor/editor-after-route-changes.spec.ts": { - "avgDuration": 26201, - "testCount": 1, - "flakyRate": 0.0014 - }, - "tests/e2e/node-creator/special-nodes.spec.ts": { - "avgDuration": 25945, - "testCount": 3, - "flakyRate": 0.0042 - }, - "tests/e2e/workflows/list/import.spec.ts": { - "avgDuration": 25716, - "testCount": 5, - "flakyRate": 0.0014 - }, - "tests/e2e/workflows/editor/subworkflows/debugging.spec.ts": { - "avgDuration": 25072, - "testCount": 4, - "flakyRate": 0 - }, - "tests/e2e/node-creator/categories.spec.ts": { - "avgDuration": 24567, - "testCount": 5, - "flakyRate": 0 - }, - "tests/e2e/app-config/env-feature-flags.spec.ts": { - "avgDuration": 24294, - "testCount": 2, - "flakyRate": 0.0042 - }, - "tests/e2e/settings/log-streaming/log-streaming.spec.ts": { - "avgDuration": 23845, - "testCount": 5, - "flakyRate": 0 - }, - "tests/e2e/auth/signin.spec.ts": { - "avgDuration": 23513, - "testCount": 1, - "flakyRate": 0.0056 - }, - "tests/e2e/node-creator/actions.spec.ts": { - "avgDuration": 22680, - "testCount": 4, - "flakyRate": 0 + "tests/e2e/nodes/mcp-trigger.spec.ts": { + "avgDuration": 19374, + "testCount": 23, + "flakyRate": 0.0505 }, "tests/e2e/settings/users/users.spec.ts": { - "avgDuration": 22427, + "avgDuration": 19158, "testCount": 5, - "flakyRate": 0.0395 - }, - "tests/e2e/workflows/editor/execution/inject-previous.spec.ts": { - "avgDuration": 22203, - "testCount": 2, - "flakyRate": 0.0028 - }, - "tests/e2e/building-blocks/user-service.spec.ts": { - "avgDuration": 21803, - "testCount": 8, - "flakyRate": 0.0042 - }, - "tests/e2e/node-creator/navigation.spec.ts": { - "avgDuration": 21589, - "testCount": 4, - "flakyRate": 0 + "flakyRate": 0.0036 }, "tests/e2e/workflows/demo-diff.spec.ts": { - "avgDuration": 21489, + "avgDuration": 19043, "testCount": 9, - "flakyRate": 0 + "flakyRate": 0.0036 }, - "tests/e2e/sharing/access-control.spec.ts": { - "avgDuration": 21465, - "testCount": 5, - "flakyRate": 0.0026 - }, - "tests/e2e/building-blocks/workflow-entry-points.spec.ts": { - "avgDuration": 21451, - "testCount": 5, - "flakyRate": 0.0014 - }, - "tests/e2e/auth/admin-smoke.spec.ts": { - "avgDuration": 20234, - "testCount": 1, - "flakyRate": 0.0085 - }, - "tests/e2e/credentials/oauth.spec.ts": { - "avgDuration": 19947, - "testCount": 1, - "flakyRate": 0.007 - }, - "tests/e2e/nodes/if-node.spec.ts": { - "avgDuration": 18980, + "tests/e2e/api/webhook-external.spec.ts": { + "avgDuration": 18850, "testCount": 2, - "flakyRate": 0.0295 + "flakyRate": 0.0125 }, - "tests/e2e/app-config/versions.spec.ts": { - "avgDuration": 18809, - "testCount": 1, - "flakyRate": 0.0042 - }, - "tests/e2e/chat-hub/chat-hub-personal-agent.spec.ts": { - "avgDuration": 18451, - "testCount": 2, - "flakyRate": 0.0669 - }, - "tests/e2e/node-creator/vector-stores.spec.ts": { - "avgDuration": 18443, + "tests/e2e/regression/PAY-4367-node-shifting-cyclic.spec.ts": { + "avgDuration": 18217, "testCount": 3, "flakyRate": 0 }, "tests/e2e/sharing/workflow-sharing.spec.ts": { - "avgDuration": 16920, + "avgDuration": 18037, "testCount": 4, + "flakyRate": 0.0161 + }, + "tests/e2e/auth/signin.spec.ts": { + "avgDuration": 17601, + "testCount": 1, + "flakyRate": 0.0036 + }, + "tests/e2e/nodes/pdf-node.spec.ts": { + "avgDuration": 17482, + "testCount": 1, + "flakyRate": 0.0389 + }, + "tests/e2e/node-creator/vector-stores.spec.ts": { + "avgDuration": 17319, + "testCount": 3, + "flakyRate": 0.0072 + }, + "tests/e2e/node-creator/special-nodes.spec.ts": { + "avgDuration": 17174, + "testCount": 3, "flakyRate": 0 }, "tests/e2e/workflows/editor/ndv/io-filter.spec.ts": { - "avgDuration": 16245, + "avgDuration": 15837, "testCount": 2, - "flakyRate": 0.0028 - }, - "tests/e2e/chat-hub/chat-hub-tools.spec.ts": { - "avgDuration": 16206, - "testCount": 1, - "flakyRate": 0.1574 - }, - "tests/e2e/nodes/pdf-node.spec.ts": { - "avgDuration": 15332, - "testCount": 1, - "flakyRate": 0.0698 - }, - "tests/e2e/ai/chat-session.spec.ts": { - "avgDuration": 15262, - "testCount": 1, - "flakyRate": 0.0042 - }, - "tests/e2e/regression/ADO-4462-template-setup-experiment.spec.ts": { - "avgDuration": 14918, - "testCount": 2, - "flakyRate": 0.0014 - }, - "tests/e2e/auth/authenticated.spec.ts": { - "avgDuration": 14826, - "testCount": 5, - "flakyRate": 0.0014 + "flakyRate": 0.0071 }, "tests/e2e/sharing/credential-sharing.spec.ts": { - "avgDuration": 14707, + "avgDuration": 14796, "testCount": 3, - "flakyRate": 0.0026 - }, - "tests/e2e/capabilities/proxy-server.spec.ts": { - "avgDuration": 14632, - "testCount": 4, - "flakyRate": 0.0042 - }, - "tests/e2e/regression/SUG-121-fields-reset-after-closing-ndv.spec.ts": { - "avgDuration": 14359, - "testCount": 1, - "flakyRate": 0.0056 + "flakyRate": 0.0036 }, "tests/e2e/nodes/http-request-node.spec.ts": { - "avgDuration": 14147, + "avgDuration": 14130, "testCount": 2, - "flakyRate": 0.0028 - }, - "tests/e2e/credentials/api-operations.spec.ts": { - "avgDuration": 14117, - "testCount": 5, - "flakyRate": 0.0028 + "flakyRate": 0.0089 }, "tests/e2e/workflows/editor/execution/partial.spec.ts": { - "avgDuration": 13936, + "avgDuration": 13393, "testCount": 2, "flakyRate": 0 }, "tests/e2e/regression/AI-812-partial-execs-broken-when-using-chat-trigger.spec.ts": { - "avgDuration": 12303, + "avgDuration": 12865, "testCount": 2, - "flakyRate": 0.0014 + "flakyRate": 0 }, "tests/e2e/regression/ADO-2372-prevent-clipping-params.spec.ts": { - "avgDuration": 11609, + "avgDuration": 11308, "testCount": 2, "flakyRate": 0 }, + "tests/e2e/node-creator/workflows.spec.ts": { + "avgDuration": 11144, + "testCount": 2, + "flakyRate": 0.0018 + }, + "tests/e2e/app-config/versions.spec.ts": { + "avgDuration": 11109, + "testCount": 1, + "flakyRate": 0 + }, "tests/e2e/settings/workers/workers.spec.ts": { - "avgDuration": 11295, + "avgDuration": 10525, "testCount": 4, - "flakyRate": 0.0028 + "flakyRate": 0.0018 }, - "tests/e2e/api/webhook-isolation.spec.ts": { - "avgDuration": 10999, - "testCount": 14, - "flakyRate": 0.0058 - }, - "tests/e2e/node-creator/workflows.spec.ts": { - "avgDuration": 10812, - "testCount": 2, - "flakyRate": 0 + "tests/e2e/chat-hub/chat-hub-tools.spec.ts": { + "avgDuration": 10235, + "testCount": 1, + "flakyRate": 0.0054 }, "tests/e2e/regression/ADO-1338-ndv-missing-input-panel.spec.ts": { - "avgDuration": 10370, + "avgDuration": 9645, "testCount": 1, - "flakyRate": 0 - }, - "tests/e2e/nodes/schedule-trigger-node.spec.ts": { - "avgDuration": 9491, - "testCount": 1, - "flakyRate": 0.0014 - }, - "tests/e2e/workflows/editor/ndv/schema-preview.spec.ts": { - "avgDuration": 7783, - "testCount": 1, - "flakyRate": 0 - }, - "tests/e2e/regression/AI-1401-sub-nodes-input-panel.spec.ts": { - "avgDuration": 7634, - "testCount": 1, - "flakyRate": 0 + "flakyRate": 0.0018 }, "tests/e2e/regression/CAT-726-canvas-node-connectors-not-rendered-when-nodes-inserted.spec.ts": { - "avgDuration": 7430, + "avgDuration": 8351, "testCount": 1, + "flakyRate": 0.0018 + }, + "tests/e2e/regression/AI-1401-sub-nodes-input-panel.spec.ts": { + "avgDuration": 7922, + "testCount": 1, + "flakyRate": 0.0018 + }, + "tests/e2e/credentials/api-operations.spec.ts": { + "avgDuration": 7747, + "testCount": 5, "flakyRate": 0 }, - "tests/e2e/regression/SUG-38-inline-expression-preview.spec.ts": { - "avgDuration": 7227, + "tests/e2e/workflows/editor/ndv/schema-preview.spec.ts": { + "avgDuration": 7653, "testCount": 1, "flakyRate": 0 }, "tests/e2e/regression/AI-716-correctly-set-up-agent-model-shows-error.spec.ts": { - "avgDuration": 7121, + "avgDuration": 7322, "testCount": 1, - "flakyRate": 0 + "flakyRate": 0.0018 }, "tests/e2e/nodes/email-send-node.spec.ts": { - "avgDuration": 7118, + "avgDuration": 7251, + "testCount": 1, + "flakyRate": 0.0036 + }, + "tests/e2e/regression/SUG-121-fields-reset-after-closing-ndv.spec.ts": { + "avgDuration": 7203, + "testCount": 1, + "flakyRate": 0.0071 + }, + "tests/e2e/regression/SUG-38-inline-expression-preview.spec.ts": { + "avgDuration": 7181, "testCount": 1, "flakyRate": 0 }, - "tests/e2e/regression/PAY-4367-node-shifting-cyclic.spec.ts": { - "avgDuration": 6401, + "tests/e2e/credentials/oauth.spec.ts": { + "avgDuration": 6886, + "testCount": 1, + "flakyRate": 0.0053 + }, + "tests/e2e/settings/community-nodes/community-nodes.spec.ts": { + "avgDuration": 6527, "testCount": 1, "flakyRate": 0 }, "tests/e2e/regression/ADO-2230-ndv-reset-data-pagination.spec.ts": { - "avgDuration": 5967, + "avgDuration": 6230, "testCount": 1, - "flakyRate": 0 - }, - "tests/e2e/settings/community-nodes/community-nodes.spec.ts": { - "avgDuration": 5920, - "testCount": 1, - "flakyRate": 0 - }, - "tests/e2e/workflows/editor/canvas/focus-panel.spec.ts": { - "avgDuration": 5401, - "testCount": 1, - "flakyRate": 0 + "flakyRate": 0.0018 }, "tests/e2e/workflows/editor/canvas/stickies.spec.ts": { - "avgDuration": 5297, + "avgDuration": 5194, "testCount": 1, "flakyRate": 0 }, "tests/e2e/regression/ADO-2929-can-load-old-switch-node-workflows.spec.ts": { - "avgDuration": 5170, + "avgDuration": 5121, + "testCount": 1, + "flakyRate": 0 + }, + "tests/e2e/workflows/editor/canvas/focus-panel.spec.ts": { + "avgDuration": 5070, "testCount": 1, "flakyRate": 0 }, "tests/e2e/settings/log-streaming/log-streaming-ui-e2e.spec.ts": { - "avgDuration": 5046, + "avgDuration": 4739, "testCount": 1, "flakyRate": 0 }, - "tests/e2e/chat-hub/chat-hub-chat-user.spec.ts": { - "avgDuration": 4983, - "testCount": 1, + "tests/e2e/mcp/mcp-service.spec.ts": { + "avgDuration": 3442, + "testCount": 23, "flakyRate": 0 }, "tests/e2e/workflows/editor/subworkflows/wait.spec.ts": { - "avgDuration": 4186, - "testCount": 4, - "flakyRate": 0.0126 - }, - "tests/e2e/workflows/editor/subworkflows/subworkflow-version-resolution.spec.ts": { - "avgDuration": 1793, + "avgDuration": 3295, "testCount": 4, "flakyRate": 0 }, - "tests/e2e/ai/langchain-memory.spec.ts": { + "tests/e2e/dynamic-credentials/external-user-trigger.spec.ts": { + "avgDuration": 2166, + "testCount": 1, + "flakyRate": 0.0028 + }, + "tests/e2e/workflows/editor/subworkflows/subworkflow-version-resolution.spec.ts": { + "avgDuration": 1509, + "testCount": 4, + "flakyRate": 0.0036 + }, + "tests/e2e/nodes/n8n-trigger.spec.ts": { "avgDuration": 60000, "testCount": 2, "flakyRate": 0 }, + "tests/e2e/settings/external-secrets/secret-providers-connections.spec.ts": { + "avgDuration": 60000, + "testCount": 1, + "flakyRate": 0 + }, "tests/e2e/source-control/push.spec.ts": { "avgDuration": 60000, "testCount": 4, "flakyRate": 0 }, - "tests/e2e/app-config/nps-survey.spec.ts": { - "avgDuration": 60000, - "testCount": 1, - "flakyRate": 0 - }, "tests/e2e/settings/environments/source-control.spec.ts": { "avgDuration": 60000, "testCount": 4, "flakyRate": 0 }, - "tests/e2e/workflows/editor/workflow-actions/duplicate.spec.ts": { - "avgDuration": 60000, - "testCount": 2, - "flakyRate": 0 - }, - "tests/e2e/ai/evaluations.spec.ts": { - "avgDuration": 60000, - "testCount": 1, - "flakyRate": 0 - }, - "tests/e2e/workflows/editor/workflow-actions/settings.spec.ts": { - "avgDuration": 60000, - "testCount": 3, - "flakyRate": 0 - }, - "tests/e2e/ai/langchain-tools.spec.ts": { - "avgDuration": 60000, - "testCount": 2, - "flakyRate": 0 - }, - "tests/e2e/workflows/editor/workflow-actions/copy-paste.spec.ts": { - "avgDuration": 60000, - "testCount": 3, - "flakyRate": 0 - }, - "tests/e2e/source-control/pull.spec.ts": { - "avgDuration": 60000, - "testCount": 2, - "flakyRate": 0 - }, - "tests/e2e/workflows/editor/workflow-actions/run.spec.ts": { - "avgDuration": 60000, - "testCount": 4, - "flakyRate": 0 - }, "tests/e2e/workflows/editor/workflow-actions/archive.spec.ts": { "avgDuration": 60000, "testCount": 7, "flakyRate": 0 }, + "tests/e2e/workflows/editor/workflow-actions/run.spec.ts": { + "avgDuration": 60000, + "testCount": 4, + "flakyRate": 0 + }, + "tests/e2e/ai/langchain-tools.spec.ts": { + "avgDuration": 60000, + "testCount": 2, + "flakyRate": 0 + }, + "tests/e2e/source-control/pull.spec.ts": { + "avgDuration": 60000, + "testCount": 2, + "flakyRate": 0 + }, + "tests/e2e/ai/langchain-memory.spec.ts": { + "avgDuration": 60000, + "testCount": 2, + "flakyRate": 0 + }, + "tests/e2e/workflows/editor/workflow-actions/duplicate.spec.ts": { + "avgDuration": 60000, + "testCount": 2, + "flakyRate": 0 + }, + "tests/e2e/workflows/editor/workflow-actions/copy-paste.spec.ts": { + "avgDuration": 60000, + "testCount": 3, + "flakyRate": 0 + }, + "tests/e2e/app-config/nps-survey.spec.ts": { + "avgDuration": 60000, + "testCount": 1, + "flakyRate": 0 + }, + "tests/e2e/workflows/editor/workflow-actions/settings.spec.ts": { + "avgDuration": 60000, + "testCount": 3, + "flakyRate": 0 + }, "tests/e2e/workflows/editor/execution/previous-nodes.spec.ts": { "avgDuration": 60000, "testCount": 1, "flakyRate": 0 + }, + "tests/e2e/ai/evaluations.spec.ts": { + "avgDuration": 60000, + "testCount": 1, + "flakyRate": 0 } } } diff --git a/packages/testing/janitor/src/cli.ts b/packages/testing/janitor/src/cli.ts index ab508710ac7..b081eedaef9 100644 --- a/packages/testing/janitor/src/cli.ts +++ b/packages/testing/janitor/src/cli.ts @@ -27,6 +27,7 @@ import { showOrchestrateHelp, } from './cli/index.js'; import { setConfig, getConfig, defineConfig, type JanitorConfig } from './config.js'; +import { diffFileMethods } from './core/ast-diff-analyzer.js'; import { generateBaseline, saveBaseline, @@ -176,9 +177,17 @@ async function runImpact(options: CliOptions): Promise { return; } + // Compute AST diffs for additive-only narrowing + const diffs = changedFiles + .filter((f) => f.endsWith('.ts') && !f.endsWith('.spec.ts')) + .map((f) => { + const abs = path.isAbsolute(f) ? f : path.resolve(config.rootDir, f); + return diffFileMethods(abs); + }); + // Analyze impact const analyzer = new ImpactAnalyzer(project); - const result = analyzer.analyze(changedFiles); + const result = analyzer.analyze(changedFiles, diffs); // Output if (options.json) { @@ -447,8 +456,15 @@ async function runOrchestrate(options: CliOptions): Promise { console.error('Impact: No changed files detected. Returning empty orchestration.'); specs = []; } else { + const diffs = changedFiles + .filter((f) => f.endsWith('.ts') && !f.endsWith('.spec.ts')) + .map((f) => { + const abs = path.isAbsolute(f) ? f : path.resolve(config.rootDir, f); + return diffFileMethods(abs); + }); + const impactAnalyzer = new ImpactAnalyzer(project); - const impactResult = impactAnalyzer.analyze(changedFiles); + const impactResult = impactAnalyzer.analyze(changedFiles, diffs); const affectedSet = new Set(impactResult.affectedTests); const totalBefore = specs.length; specs = specs.filter((s) => affectedSet.has(s.path)); diff --git a/packages/testing/janitor/src/core/impact-analyzer.test.ts b/packages/testing/janitor/src/core/impact-analyzer.test.ts index fc9faa9f7d4..befa1d1f54f 100644 --- a/packages/testing/janitor/src/core/impact-analyzer.test.ts +++ b/packages/testing/janitor/src/core/impact-analyzer.test.ts @@ -1,6 +1,7 @@ import { Project } from 'ts-morph'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import type { FileDiffResult } from './ast-diff-analyzer.js'; import { ImpactAnalyzer } from './impact-analyzer.js'; import { setConfig, resetConfig, defineConfig } from '../config.js'; @@ -27,9 +28,9 @@ vi.mock('../utils/paths.js', async () => { }; }); -// Mock fs for file reading in findTestsUsingProperties -vi.mock('fs', async () => { - const actual = await vi.importActual('fs'); +// Mock node:fs for file reading in findConsumersUsingProperties +vi.mock('node:fs', async () => { + const actual = await vi.importActual('node:fs'); return { ...actual, readFileSync: (filePath: string) => mockReadFileSync(filePath), @@ -540,6 +541,256 @@ test('standalone', () => {}); expect(result.affectedTests).not.toContain('tests/standalone.spec.ts'); }); + it('resolves multi-hop chain through composable (page → facade → composable → facade → test)', () => { + // The MFA case: MfaLoginPage → facade(mfaLogin) → MfaComposer uses .mfaLogin.* + // → facade(mfaComposer) → test uses .mfaComposer.* + + // Page being changed + project.createSourceFile( + '/test-root/pages/MfaLoginPage.ts', + ` +export class MfaLoginPage { + async enterCode(code: string) {} +} +`, + ); + + // Composable that uses the page via facade property + project.createSourceFile( + '/test-root/composables/MfaComposer.ts', + ` +export class MfaComposer { + async loginWithMfa() { + await this.n8n.mfaLogin.enterCode('123456'); + } +} +`, + ); + + // Facade exposes both the page and the composable + project.createSourceFile( + '/test-root/pages/AppPage.ts', + ` +import { Page } from '@playwright/test'; +import { MfaLoginPage } from './MfaLoginPage'; +import { MfaComposer } from '../composables/MfaComposer'; + +export class AppPage { + readonly page: Page; + readonly mfaLogin: MfaLoginPage; + readonly mfaComposer: MfaComposer; +} +`, + ); + + // Mock: return composable + test files when searching all .ts files + mockFindFilesRecursive.mockReturnValue([ + '/test-root/composables/MfaComposer.ts', + '/test-root/tests/mfa.spec.ts', + '/test-root/tests/login.spec.ts', + ]); + + mockReadFileSync.mockImplementation((filePath) => { + const p = String(filePath); + if (p.includes('MfaComposer.ts')) { + return 'await this.n8n.mfaLogin.enterCode("123456");'; + } + if (p.includes('mfa.spec.ts')) { + return 'await n8n.mfaComposer.loginWithMfa();'; + } + if (p.includes('login.spec.ts')) { + return 'await n8n.login.signIn();'; // unrelated + } + return ''; + }); + + const analyzer = new ImpactAnalyzer(project); + const result = analyzer.analyze(['pages/MfaLoginPage.ts']); + + // mfa.spec.ts should be found via the multi-hop chain + expect(result.affectedTests).toContain('tests/mfa.spec.ts'); + // login.spec.ts doesn't use mfaLogin or mfaComposer + expect(result.affectedTests).not.toContain('tests/login.spec.ts'); + }); + + it('finds tests via both direct property usage and indirect composable chain', () => { + // Same page used directly by one test AND through a composable by another + + project.createSourceFile( + '/test-root/pages/SettingsPage.ts', + ` +export class SettingsPage { + async openApiKeys() {} +} +`, + ); + + project.createSourceFile( + '/test-root/composables/SetupComposer.ts', + ` +export class SetupComposer { + async completeSetup() { + await this.n8n.settings.openApiKeys(); + } +} +`, + ); + + project.createSourceFile( + '/test-root/pages/AppPage.ts', + ` +import { Page } from '@playwright/test'; +import { SettingsPage } from './SettingsPage'; +import { SetupComposer } from '../composables/SetupComposer'; + +export class AppPage { + readonly page: Page; + readonly settings: SettingsPage; + readonly setupComposer: SetupComposer; +} +`, + ); + + mockFindFilesRecursive.mockReturnValue([ + '/test-root/composables/SetupComposer.ts', + '/test-root/tests/settings-direct.spec.ts', + '/test-root/tests/setup-flow.spec.ts', + '/test-root/tests/unrelated.spec.ts', + ]); + + mockReadFileSync.mockImplementation((filePath) => { + const p = String(filePath); + if (p.includes('SetupComposer.ts')) { + return 'await this.n8n.settings.openApiKeys();'; + } + if (p.includes('settings-direct.spec.ts')) { + return 'await n8n.settings.openApiKeys();'; // direct usage + } + if (p.includes('setup-flow.spec.ts')) { + return 'await n8n.setupComposer.completeSetup();'; // indirect via composable + } + return ''; + }); + + const analyzer = new ImpactAnalyzer(project); + const result = analyzer.analyze(['pages/SettingsPage.ts']); + + // Both direct and indirect consumers found + expect(result.affectedTests).toContain('tests/settings-direct.spec.ts'); + expect(result.affectedTests).toContain('tests/setup-flow.spec.ts'); + expect(result.affectedTests).not.toContain('tests/unrelated.spec.ts'); + }); + + it('traces non-facade intermediary via import graph', () => { + // Helper file not on the facade — should fall back to import tracing + + project.createSourceFile( + '/test-root/pages/NodePage.ts', + ` +export class NodePage { + async configureNode() {} +} +`, + ); + + // Helper that uses NodePage via facade property but is NOT on the facade itself + project.createSourceFile( + '/test-root/helpers/nodeHelper.ts', + ` +export function setupNode(n8n: any) { + return n8n.node.configureNode(); +} +`, + ); + + // Test imports the helper directly + project.createSourceFile( + '/test-root/tests/node-helper.spec.ts', + ` +import { setupNode } from '../helpers/nodeHelper'; +test('configures node', () => {}); +`, + ); + + project.createSourceFile( + '/test-root/pages/AppPage.ts', + ` +import { Page } from '@playwright/test'; +import { NodePage } from './NodePage'; + +export class AppPage { + readonly page: Page; + readonly node: NodePage; +} +`, + ); + + // findConsumersUsingProperties finds the helper (it references .node.) + mockFindFilesRecursive.mockReturnValue(['/test-root/helpers/nodeHelper.ts']); + + mockReadFileSync.mockImplementation((filePath) => { + const p = String(filePath); + if (p.includes('nodeHelper.ts')) { + return 'return n8n.node.configureNode();'; + } + return ''; + }); + + const analyzer = new ImpactAnalyzer(project); + const result = analyzer.analyze(['pages/NodePage.ts']); + + // Helper is not on facade, so import-trace picks up the test + expect(result.affectedTests).toContain('tests/node-helper.spec.ts'); + }); + + it('does not match unrelated composables with similar but distinct property names', () => { + // Ensure .node doesn't match .nodePanel (word boundary check) + + project.createSourceFile( + '/test-root/pages/NodePage.ts', + ` +export class NodePage { + async openNode() {} +} +`, + ); + + project.createSourceFile( + '/test-root/pages/AppPage.ts', + ` +import { Page } from '@playwright/test'; +import { NodePage } from './NodePage'; + +export class AppPage { + readonly page: Page; + readonly node: NodePage; +} +`, + ); + + mockFindFilesRecursive.mockReturnValue([ + '/test-root/tests/node-panel.spec.ts', + '/test-root/tests/node.spec.ts', + ]); + + mockReadFileSync.mockImplementation((filePath) => { + const p = String(filePath); + if (p.includes('node-panel.spec.ts')) { + return 'await n8n.nodePanel.search("HTTP");'; // nodePanel, not node + } + if (p.includes('node.spec.ts')) { + return 'await n8n.node.openNode();'; + } + return ''; + }); + + const analyzer = new ImpactAnalyzer(project); + const result = analyzer.analyze(['pages/NodePage.ts']); + + expect(result.affectedTests).toContain('tests/node.spec.ts'); + expect(result.affectedTests).not.toContain('tests/node-panel.spec.ts'); + }); + it('handles page not exposed on facade (falls back to camelCase)', () => { // If a page isn't in the facade, we fall back to camelCase property name project.createSourceFile( @@ -571,6 +822,183 @@ export class AppPage { }); }); + describe('Additive-Only Narrowing', () => { + it('skips dependency tracing when all changes are additive', () => { + // Shared service imported by a test — but only new methods were added + project.createSourceFile( + '/test-root/services/api-helper.ts', + ` +export class ApiHelper { + async getWorkflows() {} + async newMethod() {} +} +`, + ); + + project.createSourceFile( + '/test-root/tests/api.spec.ts', + ` +import { ApiHelper } from '../services/api-helper'; +test('uses api', () => {}); +`, + ); + + const diffs: FileDiffResult[] = [ + { + filePath: '/test-root/services/api-helper.ts', + changedMethods: [ + { className: 'ApiHelper', methodName: 'newMethod', changeType: 'added' }, + ], + isNewFile: false, + isDeletedFile: false, + parseTimeMs: 0, + }, + ]; + + const analyzer = new ImpactAnalyzer(project); + const result = analyzer.analyze(['services/api-helper.ts'], diffs); + + // No transitive tests affected — the change is purely additive + expect(result.affectedTests).toEqual([]); + }); + + it('traces normally when a method is modified', () => { + project.createSourceFile( + '/test-root/services/api-helper.ts', + ` +export class ApiHelper { + async getWorkflows() {} +} +`, + ); + + project.createSourceFile( + '/test-root/tests/api.spec.ts', + ` +import { ApiHelper } from '../services/api-helper'; +test('uses api', () => {}); +`, + ); + + const diffs: FileDiffResult[] = [ + { + filePath: '/test-root/services/api-helper.ts', + changedMethods: [ + { className: 'ApiHelper', methodName: 'getWorkflows', changeType: 'modified' }, + ], + isNewFile: false, + isDeletedFile: false, + parseTimeMs: 0, + }, + ]; + + const analyzer = new ImpactAnalyzer(project); + const result = analyzer.analyze(['services/api-helper.ts'], diffs); + + expect(result.affectedTests).toContain('tests/api.spec.ts'); + }); + + it('traces normally when changes are mixed (added + modified)', () => { + project.createSourceFile( + '/test-root/services/api-helper.ts', + ` +export class ApiHelper { + async getWorkflows() {} + async newMethod() {} +} +`, + ); + + project.createSourceFile( + '/test-root/tests/api.spec.ts', + ` +import { ApiHelper } from '../services/api-helper'; +test('uses api', () => {}); +`, + ); + + const diffs: FileDiffResult[] = [ + { + filePath: '/test-root/services/api-helper.ts', + changedMethods: [ + { className: 'ApiHelper', methodName: 'newMethod', changeType: 'added' }, + { className: 'ApiHelper', methodName: 'getWorkflows', changeType: 'modified' }, + ], + isNewFile: false, + isDeletedFile: false, + parseTimeMs: 0, + }, + ]; + + const analyzer = new ImpactAnalyzer(project); + const result = analyzer.analyze(['services/api-helper.ts'], diffs); + + expect(result.affectedTests).toContain('tests/api.spec.ts'); + }); + + it('traces normally when no diffs are provided (backwards-compatible)', () => { + project.createSourceFile( + '/test-root/services/api-helper.ts', + ` +export class ApiHelper { + async getWorkflows() {} +} +`, + ); + + project.createSourceFile( + '/test-root/tests/api.spec.ts', + ` +import { ApiHelper } from '../services/api-helper'; +test('uses api', () => {}); +`, + ); + + const analyzer = new ImpactAnalyzer(project); + // No diffs parameter — full tracing as before + const result = analyzer.analyze(['services/api-helper.ts']); + + expect(result.affectedTests).toContain('tests/api.spec.ts'); + }); + + it('traces conservatively when changedMethods is empty', () => { + // Empty changedMethods means the file changed but no method-level changes detected + // (e.g., import reordering, comments). We should still trace conservatively. + project.createSourceFile( + '/test-root/services/api-helper.ts', + ` +export class ApiHelper { + async getWorkflows() {} +} +`, + ); + + project.createSourceFile( + '/test-root/tests/api.spec.ts', + ` +import { ApiHelper } from '../services/api-helper'; +test('uses api', () => {}); +`, + ); + + const diffs: FileDiffResult[] = [ + { + filePath: '/test-root/services/api-helper.ts', + changedMethods: [], + isNewFile: false, + isDeletedFile: false, + parseTimeMs: 0, + }, + ]; + + const analyzer = new ImpactAnalyzer(project); + const result = analyzer.analyze(['services/api-helper.ts'], diffs); + + // Empty changedMethods = conservative, still traces + expect(result.affectedTests).toContain('tests/api.spec.ts'); + }); + }); + describe('Edge Cases', () => { it('handles file not found gracefully', () => { const analyzer = new ImpactAnalyzer(project); diff --git a/packages/testing/janitor/src/core/impact-analyzer.ts b/packages/testing/janitor/src/core/impact-analyzer.ts index 6a2f593b7a5..0328ff6def9 100644 --- a/packages/testing/janitor/src/core/impact-analyzer.ts +++ b/packages/testing/janitor/src/core/impact-analyzer.ts @@ -9,9 +9,15 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { type Project, type SourceFile } from 'ts-morph'; +import { type FileDiffResult } from './ast-diff-analyzer.js'; import { FacadeResolver } from './facade-resolver.js'; import { getRootDir, findFilesRecursive, getRelativePath, isTestFile } from '../utils/paths.js'; +function isAdditiveOnly(diff: FileDiffResult): boolean { + if (diff.changedMethods.length === 0) return false; + return diff.changedMethods.every((m) => m.changeType === 'added'); +} + export interface ImpactResult { changedFiles: string[]; affectedFiles: string[]; @@ -32,13 +38,22 @@ export class ImpactAnalyzer { } /** - * Given a list of changed files, determine which test files are affected + * Given a list of changed files, determine which test files are affected. + * When diffs are provided, files with only additive changes (new methods) + * skip dependency tracing — new exports can't break existing consumers. */ - analyze(changedFiles: string[]): ImpactResult { + analyze(changedFiles: string[], diffs?: FileDiffResult[]): ImpactResult { const absolutePaths = changedFiles.map((f) => path.isAbsolute(f) ? f : path.join(this.root, f), ); + const diffMap = new Map(); + if (diffs) { + for (const diff of diffs) { + diffMap.set(diff.filePath, diff); + } + } + const affectedSet = new Set(); const graph: Record = {}; @@ -57,6 +72,13 @@ export class ImpactAnalyzer { affectedSet.add(filePath); } + // If we have diff info and all changes are additive, skip dependency tracing. + // New exports can't break existing consumers. + const diff = diffMap.get(filePath); + if (diff && isAdditiveOnly(diff)) { + continue; + } + // Find property names this file exposes (for property-based search) const propertyNames = this.extractPropertyNames(sourceFile); @@ -136,8 +158,8 @@ export class ImpactAnalyzer { // If we hit a facade, stop import tracing and switch to property search if (this.facade.isFacade(depPath)) { - // Find tests that actually USE the property, not just import the facade - const testsUsingProperty = this.findTestsUsingProperties(propertyNames); + // Resolve through multi-hop chains (page → facade → composable → test) + const testsUsingProperty = this.resolvePropertyToTests(propertyNames, visited); dependents.push(...testsUsingProperty); continue; } @@ -157,29 +179,33 @@ export class ImpactAnalyzer { } /** - * Find test files that actually use the given property names - * Uses grep-style search for .propertyName. patterns + * Find all .ts files that use the given property names via text search. + * Searches the entire project root (not just tests/) to catch composables, + * helpers, and other intermediaries between the facade and tests. */ - private findTestsUsingProperties(propertyNames: string[]): string[] { + private findConsumersUsingProperties(propertyNames: string[]): string[] { if (propertyNames.length === 0) { return []; } - const testsDir = path.join(this.root, 'tests'); - const matchingTests = new Set(); + const matchingFiles = new Set(); + const facadePath = this.facade.getFacadePath(); - // Build regex pattern to match property access: .logsPanel. or .logsPanel) - const patterns = propertyNames.map((name) => new RegExp(`\\.${name}[.)]`)); + // Word-boundary regex: matches .name followed by any non-identifier char + const patterns = propertyNames.map((name) => new RegExp(`\\.${name}(?![a-zA-Z0-9_])`)); - // Recursively find all test files - const testFiles = findFilesRecursive(testsDir, '.spec.ts'); + // Search all .ts files in the project root + const allFiles = findFilesRecursive(this.root, '.ts'); + + for (const file of allFiles) { + // Skip the facade itself — it declares properties, doesn't consume them + if (file === facadePath) continue; - for (const testFile of testFiles) { try { - const content = fs.readFileSync(testFile, 'utf-8'); + const content = fs.readFileSync(file, 'utf-8'); for (const pattern of patterns) { if (pattern.test(content)) { - matchingTests.add(testFile); + matchingFiles.add(file); break; } } @@ -188,7 +214,62 @@ export class ImpactAnalyzer { } } - return Array.from(matchingTests); + return Array.from(matchingFiles); + } + + /** + * Resolve property names to affected test files, following multi-hop chains. + * + * When a page changes and we hit the facade, the consumers might be: + * 1. Test files → add directly to results + * 2. Composables/helpers on the facade → extract THEIR property names, recurse + * 3. Non-facade files → import-trace via findAllDependents + * + * Example chain: MfaLoginPage → facade(mfaLogin) → MfaComposer(mfaLogin.*) → + * facade(mfaComposer) → test(n8n.mfaComposer.*) + */ + private resolvePropertyToTests( + propertyNames: string[], + visited: Set, + resolvedConsumers: Set = new Set(), + ): string[] { + const consumers = this.findConsumersUsingProperties(propertyNames); + const tests: string[] = []; + + for (const consumer of consumers) { + // Guard against cyclic property chains (A→B→A) + if (resolvedConsumers.has(consumer)) continue; + resolvedConsumers.add(consumer); + + const relativePath = getRelativePath(consumer); + + if (isTestFile(relativePath)) { + tests.push(consumer); + continue; + } + + // Non-test consumer (composable, helper, etc.) + const sourceFile = this.project.getSourceFile(consumer); + if (!sourceFile) continue; + + // Check if this file is exposed on the facade + const consumerPropertyNames = this.extractPropertyNames(sourceFile); + if (consumerPropertyNames.length > 0) { + // File is on the facade — recurse with its property names + const transitiveTests = this.resolvePropertyToTests( + consumerPropertyNames, + visited, + resolvedConsumers, + ); + tests.push(...transitiveTests); + } else { + // Not on the facade — fall back to import tracing + const dependents = this.findAllDependents(sourceFile, visited, []); + tests.push(...dependents); + } + } + + return tests; } } diff --git a/packages/testing/playwright/.janitor-baseline.json b/packages/testing/playwright/.janitor-baseline.json index a807d6a234d..70f5a39c8cd 100644 --- a/packages/testing/playwright/.janitor-baseline.json +++ b/packages/testing/playwright/.janitor-baseline.json @@ -1,7 +1,7 @@ { "version": 1, - "generated": "2026-02-24T22:17:42.106Z", - "totalViolations": 552, + "generated": "2026-03-03T13:03:42.179Z", + "totalViolations": 553, "violations": { "pages/AIAssistantPage.ts": [ { @@ -145,14 +145,6 @@ "hash": "c904517ef2df" } ], - "pages/MfaLoginPage.ts": [ - { - "rule": "scope-lockdown", - "line": 8, - "message": "MfaLoginPage: Ambiguous page - add a container (for scoped components) or a navigation method (for top-level pages)", - "hash": "65f89f81ef90" - } - ], "pages/MfaSetupModal.ts": [ { "rule": "scope-lockdown", @@ -1224,55 +1216,55 @@ "tests/e2e/ai/langchain-memory.spec.ts": [ { "rule": "selector-purity", - "line": 92, + "line": 93, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Define below' }).cl...", "hash": "39e5d44a50a7" }, { "rule": "selector-purity", - "line": 92, + "line": 93, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Define below' })", "hash": "5c42d41d0397" }, { "rule": "selector-purity", - "line": 93, + "line": 94, "message": "Chained locator call in test: n8n.ndv.getParameterInput('sessionKey').locator('input')", "hash": "4860c40ad30b" }, { "rule": "selector-purity", - "line": 107, + "line": 108, "message": "Chained locator call in test: n8n.ndv.getAiOutputModeToggle().locator('[role=\"radio\"]')", "hash": "712bf333eaaf" }, { "rule": "selector-purity", - "line": 115, + "line": 116, "message": "Chained locator call in test: n8n.ndv.getOutputPanel().getByTestId('node-error-message')", "hash": "9436eb5e9511" }, { "rule": "selector-purity", - "line": 118, + "line": 119, "message": "Chained locator call in test: n8n.ndv.getOutputPanel().getByTestId('node-error-message')", "hash": "9436eb5e9511" }, { "rule": "selector-purity", - "line": 169, + "line": 170, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Define below' }).cl...", "hash": "39e5d44a50a7" }, { "rule": "selector-purity", - "line": 169, + "line": 170, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Define below' })", "hash": "5c42d41d0397" }, { "rule": "selector-purity", - "line": 170, + "line": 171, "message": "Chained locator call in test: n8n.ndv.getParameterInput('text').locator('textarea')", "hash": "986a0637e45b" } @@ -1294,13 +1286,13 @@ "tests/e2e/ai/workflow-builder.spec.ts": [ { "rule": "selector-purity", - "line": 81, + "line": 82, "message": "Direct page locator call: n8n.page.getByRole('button', { name: 'Execute and refine' })", "hash": "ba2cdfe0e07b" }, { "rule": "selector-purity", - "line": 115, + "line": 113, "message": "Direct page locator call: n8n.page.getByText('Task aborted')", "hash": "beba4221d694" } @@ -1450,19 +1442,19 @@ "tests/e2e/credentials/crud.spec.ts": [ { "rule": "selector-purity", - "line": 347, + "line": 346, "message": "Direct page locator call: n8n.page.locator('.el-overlay').first()", "hash": "6e0141ec89e6" }, { "rule": "selector-purity", - "line": 347, + "line": 346, "message": "Direct page locator call: n8n.page.locator('.el-overlay')", "hash": "4668d190d0af" }, { "rule": "api-purity", - "line": 300, + "line": 299, "message": "Raw API call detected: fetch(", "hash": "b52df352569e" } @@ -2036,55 +2028,55 @@ "tests/e2e/settings/environments/source-control.spec.ts": [ { "rule": "selector-purity", - "line": 56, - "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'main' })", - "hash": "8e36dd6e7c35" - }, - { - "rule": "selector-purity", - "line": 75, + "line": 57, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'main' })", "hash": "8e36dd6e7c35" }, { "rule": "selector-purity", "line": 76, + "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'main' })", + "hash": "8e36dd6e7c35" + }, + { + "rule": "selector-purity", + "line": 77, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'development' })", "hash": "4157d0d852a3" }, { "rule": "selector-purity", - "line": 77, + "line": 78, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'staging' })", "hash": "5ea1e1189ae1" }, { "rule": "selector-purity", - "line": 78, + "line": 79, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'production' })", "hash": "13bbd087b456" }, { "rule": "selector-purity", - "line": 79, + "line": 80, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'development' }).cli...", "hash": "2d47edbe9c6a" }, { "rule": "selector-purity", - "line": 79, + "line": 80, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'development' })", "hash": "4157d0d852a3" }, { "rule": "api-purity", - "line": 83, + "line": 84, "message": "Raw API call detected: request.get(", "hash": "5128d500e46f" }, { "rule": "api-purity", - "line": 92, + "line": 93, "message": "Raw API call detected: request.get(", "hash": "5128d500e46f" } @@ -2174,7 +2166,7 @@ "tests/e2e/source-control/pull.spec.ts": [ { "rule": "selector-purity", - "line": 94, + "line": 95, "message": "Chained locator call in test: n8n.canvas.tagsManagerModal.getTable().getByText('pull-te...", "hash": "9cd079655a6b" } @@ -2182,55 +2174,55 @@ "tests/e2e/workflows/checklist/production-checklist.spec.ts": [ { "rule": "selector-purity", - "line": 67, + "line": 68, "message": "Direct page locator call: n8n.page.getByTestId('workflow-settings-dialog')", "hash": "6dff31c05187" }, { "rule": "selector-purity", - "line": 68, + "line": 69, "message": "Direct page locator call: n8n.page.getByTestId('workflow-settings-error-workflow')", "hash": "e86d143e5094" }, { "rule": "selector-purity", - "line": 83, + "line": 86, "message": "Direct page locator call: n8n.page.getByTestId('workflow-settings-dialog')", "hash": "6dff31c05187" }, { "rule": "selector-purity", - "line": 95, + "line": 98, "message": "Chained locator call in test: n8n.canvas.getProductionChecklistActionItem().first().get...", "hash": "b99bf85bffb8" }, { "rule": "selector-purity", - "line": 98, + "line": 101, "message": "Direct page locator call: n8n.page.locator('body').click({ position: { x: 0, y: 0 } })", "hash": "82ea0a8120b9" }, { "rule": "selector-purity", - "line": 98, + "line": 101, "message": "Direct page locator call: n8n.page.locator('body')", "hash": "6667df4502c4" }, { "rule": "selector-purity", - "line": 136, + "line": 139, "message": "Direct page locator call: n8n.page.getByTestId('workflow-settings-dialog')", "hash": "6dff31c05187" }, { "rule": "selector-purity", - "line": 142, + "line": 145, "message": "Chained locator call in test: n8n.canvas .getProductionChecklistActionItem() .first() ....", "hash": "049e1b469b18" }, { "rule": "selector-purity", - "line": 159, + "line": 162, "message": "Direct page locator call: n8n.page.locator('.el-message-box')", "hash": "a41c304c373a" } @@ -2386,19 +2378,19 @@ "tests/e2e/workflows/editor/canvas/canvas-zoom.spec.ts": [ { "rule": "selector-purity", - "line": 222, + "line": 241, "message": "Chained locator call in test: n8n.canvas.nodeByName('n8n').getByTestId('overflow-node-b...", "hash": "3d500494797e" }, { "rule": "selector-purity", - "line": 223, + "line": 242, "message": "Direct page locator call: n8n.page.getByTestId('context-menu-item-open').click()", "hash": "803edc6e2d15" }, { "rule": "selector-purity", - "line": 223, + "line": 242, "message": "Direct page locator call: n8n.page.getByTestId('context-menu-item-open')", "hash": "049003f6550b" } @@ -2444,19 +2436,19 @@ "tests/e2e/workflows/editor/execution/execution.spec.ts": [ { "rule": "selector-purity", - "line": 149, + "line": 150, "message": "Direct page locator call: n8n.page.getByTestId('copy-input').click()", "hash": "20e9bf63ae83" }, { "rule": "selector-purity", - "line": 149, + "line": 150, "message": "Direct page locator call: n8n.page.getByTestId('copy-input')", "hash": "dd5c167f4482" }, { "rule": "api-purity", - "line": 153, + "line": 154, "message": "Raw API call detected: request.get(", "hash": "5128d500e46f" } @@ -2472,13 +2464,13 @@ "tests/e2e/workflows/editor/execution/logs.spec.ts": [ { "rule": "selector-purity", - "line": 253, + "line": 268, "message": "Chained locator call in test: n8n.ndv.outputPanel .getDataContainer() .locator('a')", "hash": "6a673016ebff" }, { "rule": "api-purity", - "line": 263, + "line": 278, "message": "Raw API call detected: request.get(", "hash": "5128d500e46f" } @@ -2486,7 +2478,7 @@ "tests/e2e/workflows/editor/expressions/inline.spec.ts": [ { "rule": "selector-purity", - "line": 55, + "line": 54, "message": "Chained locator call in test: n8n.ndv.getParameterInput('value').getByRole('textbox')", "hash": "e6c73a21e494" } @@ -2494,31 +2486,31 @@ "tests/e2e/workflows/editor/expressions/mapping.spec.ts": [ { "rule": "selector-purity", - "line": 60, + "line": 61, "message": "Chained locator call in test: n8n.ndv.inputPanel.get().getByText(expectedJsonText)", "hash": "9865414150f3" }, { "rule": "selector-purity", - "line": 106, + "line": 107, "message": "Chained locator call in test: n8n.ndv.inputPanel.get().getByText('Schedule Trigger')", "hash": "2043cf03c7a7" }, { "rule": "selector-purity", - "line": 117, + "line": 118, "message": "Chained locator call in test: schemaItem.locator('span')", "hash": "dae1f9ee44ac" }, { "rule": "selector-purity", - "line": 158, + "line": 159, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'String' }).click()", "hash": "c3130f54285b" }, { "rule": "selector-purity", - "line": 158, + "line": 159, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'String' })", "hash": "6351fa2e946a" } @@ -2838,19 +2830,19 @@ "tests/e2e/workflows/editor/ndv/pinning.spec.ts": [ { "rule": "selector-purity", - "line": 81, + "line": 82, "message": "Chained locator call in test: runDataHeader.getByRole('button', { name: 'Edit Output' })", "hash": "aa1904651d9c" }, { "rule": "selector-purity", - "line": 175, + "line": 178, "message": "Chained locator call in test: n8n.ndv.getAssignmentValue('assignments').getByText('Expr...", "hash": "25491e82dd43" }, { "rule": "selector-purity", - "line": 184, + "line": 187, "message": "Chained locator call in test: n8n.ndv.getParameterInputHint().getByText(expectedOutput)", "hash": "393fc24d0bf4" } @@ -2994,7 +2986,7 @@ "tests/e2e/workflows/editor/workflow-actions/archive.spec.ts": [ { "rule": "selector-purity", - "line": 40, + "line": 41, "message": "Chained locator call in test: n8n.workflowSettingsModal.getArchiveMenuItem().locator('..')", "hash": "ec8d53703e0b" } @@ -3002,106 +2994,88 @@ "tests/e2e/workflows/editor/workflow-actions/settings.spec.ts": [ { "rule": "selector-purity", - "line": 32, + "line": 33, "message": "Direct page locator call: n8n.page.getByRole('option').count()", "hash": "43ba2cdb87a8" }, { "rule": "selector-purity", - "line": 32, + "line": 33, "message": "Direct page locator call: n8n.page.getByRole('option')", "hash": "c58b0877f9a1" }, { "rule": "selector-purity", - "line": 34, + "line": 35, "message": "Direct page locator call: n8n.page.getByRole('option').last().click()", "hash": "4dcd8e905b3c" }, { "rule": "selector-purity", - "line": 34, + "line": 35, "message": "Direct page locator call: n8n.page.getByRole('option').last()", "hash": "a8b0a552c436" }, { "rule": "selector-purity", - "line": 34, + "line": 35, "message": "Direct page locator call: n8n.page.getByRole('option')", "hash": "c58b0877f9a1" }, { "rule": "selector-purity", - "line": 37, + "line": 38, "message": "Direct page locator call: n8n.page.getByRole('option').first()", "hash": "f8c4aff6a0b3" }, { "rule": "selector-purity", - "line": 37, + "line": 38, "message": "Direct page locator call: n8n.page.getByRole('option')", "hash": "c58b0877f9a1" }, { "rule": "selector-purity", - "line": 38, + "line": 39, "message": "Direct page locator call: n8n.page.getByRole('option').nth(1).click()", "hash": "5a25d5a68877" }, { "rule": "selector-purity", - "line": 38, + "line": 39, "message": "Direct page locator call: n8n.page.getByRole('option').nth(1)", "hash": "0e8a6f4130fa" }, { "rule": "selector-purity", - "line": 38, - "message": "Direct page locator call: n8n.page.getByRole('option')", - "hash": "c58b0877f9a1" - }, - { - "rule": "selector-purity", - "line": 41, + "line": 39, "message": "Direct page locator call: n8n.page.getByRole('option')", "hash": "c58b0877f9a1" }, { "rule": "selector-purity", "line": 42, + "message": "Direct page locator call: n8n.page.getByRole('option')", + "hash": "c58b0877f9a1" + }, + { + "rule": "selector-purity", + "line": 43, "message": "Direct page locator call: n8n.page.getByRole('option').last().click()", "hash": "4dcd8e905b3c" }, { "rule": "selector-purity", - "line": 42, + "line": 43, "message": "Direct page locator call: n8n.page.getByRole('option').last()", "hash": "a8b0a552c436" }, { "rule": "selector-purity", - "line": 42, + "line": 43, "message": "Direct page locator call: n8n.page.getByRole('option')", "hash": "c58b0877f9a1" }, - { - "rule": "selector-purity", - "line": 45, - "message": "Direct page locator call: n8n.page.getByRole('option')", - "hash": "c58b0877f9a1" - }, - { - "rule": "selector-purity", - "line": 46, - "message": "Direct page locator call: n8n.page.getByRole('option').last().click()", - "hash": "4dcd8e905b3c" - }, - { - "rule": "selector-purity", - "line": 46, - "message": "Direct page locator call: n8n.page.getByRole('option').last()", - "hash": "a8b0a552c436" - }, { "rule": "selector-purity", "line": 46, @@ -3110,22 +3084,22 @@ }, { "rule": "selector-purity", - "line": 49, - "message": "Direct page locator call: n8n.page.getByRole('option')", - "hash": "c58b0877f9a1" - }, - { - "rule": "selector-purity", - "line": 50, + "line": 47, "message": "Direct page locator call: n8n.page.getByRole('option').last().click()", "hash": "4dcd8e905b3c" }, { "rule": "selector-purity", - "line": 50, + "line": 47, "message": "Direct page locator call: n8n.page.getByRole('option').last()", "hash": "a8b0a552c436" }, + { + "rule": "selector-purity", + "line": 47, + "message": "Direct page locator call: n8n.page.getByRole('option')", + "hash": "c58b0877f9a1" + }, { "rule": "selector-purity", "line": 50, @@ -3134,111 +3108,129 @@ }, { "rule": "selector-purity", - "line": 53, - "message": "Direct page locator call: n8n.page.getByRole('option')", - "hash": "c58b0877f9a1" - }, - { - "rule": "selector-purity", - "line": 54, + "line": 51, "message": "Direct page locator call: n8n.page.getByRole('option').last().click()", "hash": "4dcd8e905b3c" }, { "rule": "selector-purity", - "line": 54, + "line": 51, "message": "Direct page locator call: n8n.page.getByRole('option').last()", "hash": "a8b0a552c436" }, + { + "rule": "selector-purity", + "line": 51, + "message": "Direct page locator call: n8n.page.getByRole('option')", + "hash": "c58b0877f9a1" + }, { "rule": "selector-purity", "line": 54, "message": "Direct page locator call: n8n.page.getByRole('option')", "hash": "c58b0877f9a1" + }, + { + "rule": "selector-purity", + "line": 55, + "message": "Direct page locator call: n8n.page.getByRole('option').last().click()", + "hash": "4dcd8e905b3c" + }, + { + "rule": "selector-purity", + "line": 55, + "message": "Direct page locator call: n8n.page.getByRole('option').last()", + "hash": "a8b0a552c436" + }, + { + "rule": "selector-purity", + "line": 55, + "message": "Direct page locator call: n8n.page.getByRole('option')", + "hash": "c58b0877f9a1" } ], "tests/e2e/workflows/executions/list.spec.ts": [ { "rule": "selector-purity", - "line": 48, + "line": 49, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Success' }).click()", "hash": "fbad8571f166" }, { "rule": "selector-purity", - "line": 48, + "line": 49, "message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Success' })", "hash": "75835fe42ab8" }, { "rule": "selector-purity", - "line": 192, + "line": 194, "message": "Chained locator call in test: iframe.locator('body')", "hash": "44c6c75041a8" }, { "rule": "selector-purity", - "line": 233, + "line": 235, "message": "Chained locator call in test: iframe.locator('body')", "hash": "44c6c75041a8" }, { "rule": "selector-purity", - "line": 236, + "line": 238, "message": "Chained locator call in test: iframe.locator('body')", "hash": "44c6c75041a8" }, { "rule": "selector-purity", - "line": 239, + "line": 241, "message": "Chained locator call in test: iframe.locator('body')", "hash": "44c6c75041a8" }, { "rule": "selector-purity", - "line": 242, + "line": 244, "message": "Chained locator call in test: iframe.locator('body')", "hash": "44c6c75041a8" }, { "rule": "selector-purity", - "line": 245, + "line": 247, "message": "Chained locator call in test: iframe.locator('body')", "hash": "44c6c75041a8" }, { "rule": "selector-purity", - "line": 248, + "line": 250, "message": "Chained locator call in test: iframe.locator('body')", "hash": "44c6c75041a8" }, { "rule": "selector-purity", - "line": 251, + "line": 253, "message": "Chained locator call in test: iframe.locator('body')", "hash": "44c6c75041a8" }, { "rule": "selector-purity", - "line": 270, + "line": 272, "message": "Direct page locator call: n8n.page.getByTestId('workflow-execution-no-trigger-conte...", "hash": "407bd53b3360" }, { "rule": "selector-purity", - "line": 272, + "line": 274, "message": "Direct page locator call: n8n.page.getByRole('button', { name: 'Add first step' })....", "hash": "90ae792f4909" }, { "rule": "selector-purity", - "line": 272, + "line": 274, "message": "Direct page locator call: n8n.page.getByRole('button', { name: 'Add first step' })", "hash": "ae0e7537f378" }, { "rule": "selector-purity", - "line": 278, + "line": 280, "message": "Direct page locator call: n8n.page.getByTestId('workflow-execution-no-content')", "hash": "36b71f3e9617" } @@ -3328,7 +3320,7 @@ "tests/e2e/app-config/nps-survey.spec.ts": [ { "rule": "api-purity", - "line": 50, + "line": 51, "message": "Raw API call detected: fetch(", "hash": "b52df352569e" } @@ -3363,6 +3355,22 @@ "hash": "b52df352569e" } ], + "tests/e2e/dynamic-credentials/execution-status.spec.ts": [ + { + "rule": "api-purity", + "line": 203, + "message": "Raw API call detected: request.get(", + "hash": "5128d500e46f" + } + ], + "tests/e2e/dynamic-credentials/external-user-trigger.spec.ts": [ + { + "rule": "api-purity", + "line": 173, + "message": "Raw API call detected: request.get(", + "hash": "5128d500e46f" + } + ], "tests/e2e/sharing/workflow-sharing.spec.ts": [ { "rule": "api-purity", diff --git a/packages/testing/playwright/pages/MfaLoginPage.ts b/packages/testing/playwright/pages/MfaLoginPage.ts index acc4ea4ceab..f0634ddf6e5 100644 --- a/packages/testing/playwright/pages/MfaLoginPage.ts +++ b/packages/testing/playwright/pages/MfaLoginPage.ts @@ -6,10 +6,14 @@ import { BasePage } from './BasePage'; * Page object for the MFA login page that appears after entering email/password when MFA is enabled. */ export class MfaLoginPage extends BasePage { - getForm(): Locator { + get container(): Locator { return this.page.getByTestId('mfa-login-form'); } + getForm(): Locator { + return this.container; + } + getMfaCodeField(): Locator { return this.getForm().locator('input[name="mfaCode"]'); }