From 66e3f2b6391f4cf3b6979037900114fe83ff3899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Mon, 15 Sep 2025 10:29:34 +0200 Subject: [PATCH] test(editor): Migrate Paired item e2e tests to playwright (#19536) --- .../playwright/pages/NodeDetailsViewPage.ts | 4 + .../tests/ui/24-ndv-paired-item.spec.ts | 320 ++++++++++++++++++ ..._with_paired_item_in_multi_input_node.json | 138 ++++++++ 3 files changed, 462 insertions(+) create mode 100644 packages/testing/playwright/tests/ui/24-ndv-paired-item.spec.ts create mode 100644 packages/testing/playwright/workflows/expression_with_paired_item_in_multi_input_node.json diff --git a/packages/testing/playwright/pages/NodeDetailsViewPage.ts b/packages/testing/playwright/pages/NodeDetailsViewPage.ts index 55b2ae44c4..cffa14c3d5 100644 --- a/packages/testing/playwright/pages/NodeDetailsViewPage.ts +++ b/packages/testing/playwright/pages/NodeDetailsViewPage.ts @@ -88,6 +88,10 @@ export class NodeDetailsViewPage extends BasePage { return this.page.getByTestId('parameter-expression-preview-value'); } + getParameterExpressionPreviewOutput() { + return this.page.getByTestId('parameter-expression-preview-output'); + } + getInlineExpressionEditorPreview() { return this.page.getByTestId('inline-expression-editor-output'); } diff --git a/packages/testing/playwright/tests/ui/24-ndv-paired-item.spec.ts b/packages/testing/playwright/tests/ui/24-ndv-paired-item.spec.ts new file mode 100644 index 0000000000..afd237aeb1 --- /dev/null +++ b/packages/testing/playwright/tests/ui/24-ndv-paired-item.spec.ts @@ -0,0 +1,320 @@ +import { test, expect } from '../../fixtures/base'; + +test.describe('NDV Paired Items', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.start.fromBlankCanvas(); + }); + + test('maps paired input and output items', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_5.json'); + await n8n.canvas.clickZoomToFitButton(); + + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + + await n8n.canvas.openNode('Sort'); + + await expect(n8n.ndv.inputPanel.get()).toContainText('6 items'); + await expect(n8n.ndv.outputPanel.get()).toContainText('6 items'); + + await n8n.ndv.inputPanel.switchDisplayMode('table'); + await n8n.ndv.outputPanel.switchDisplayMode('table'); + + // input to output + const inputTableRow1 = n8n.ndv.inputPanel.getTableRow(1); + await expect(inputTableRow1).toBeVisible(); + await expect(inputTableRow1).toHaveAttribute('data-test-id', 'hovering-item'); + + // Move the cursor to simulate hover behavior + await inputTableRow1.hover(); + await expect(n8n.ndv.outputPanel.getTableRow(4)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await n8n.ndv.inputPanel.getTableRow(2).hover(); + await expect(n8n.ndv.outputPanel.getTableRow(2)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await n8n.ndv.inputPanel.getTableRow(3).hover(); + await expect(n8n.ndv.outputPanel.getTableRow(6)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + // output to input + await n8n.ndv.outputPanel.getTableRow(1).hover(); + await expect(n8n.ndv.inputPanel.getTableRow(4)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await n8n.ndv.outputPanel.getTableRow(4).hover(); + await expect(n8n.ndv.inputPanel.getTableRow(1)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await n8n.ndv.outputPanel.getTableRow(2).hover(); + await expect(n8n.ndv.inputPanel.getTableRow(2)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await n8n.ndv.outputPanel.getTableRow(6).hover(); + await expect(n8n.ndv.inputPanel.getTableRow(3)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await n8n.ndv.outputPanel.getTableRow(1).hover(); + await expect(n8n.ndv.inputPanel.getTableRow(4)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + }); + + test('maps paired input and output items based on selected input node', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_5.json'); + await n8n.canvas.clickZoomToFitButton(); + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + await n8n.canvas.openNode('Set2'); + + await expect(n8n.ndv.inputPanel.get()).toContainText('6 items'); + await expect(n8n.ndv.outputPanel.getRunSelectorInput()).toHaveValue('2 of 2 (6 items)'); + + await n8n.ndv.inputPanel.switchDisplayMode('table'); + await n8n.ndv.outputPanel.switchDisplayMode('table'); + + // Default hover state should have first item from input node highlighted + const hoveringItem = n8n.page.locator('[data-test-id="hovering-item"]'); + await expect(hoveringItem).toContainText('1111'); + await expect(n8n.ndv.getParameterExpressionPreviewValue()).toContainText('1111'); + + // Select different input node and check that the hover state is updated + await n8n.ndv.inputPanel.getNodeInputOptions().click(); + await n8n.page.getByRole('option', { name: 'Set1' }).click(); + await expect(hoveringItem).toContainText('1000'); + + // Hover on input item and verify output hover state + await n8n.ndv.inputPanel.getTable().locator('text=1000').hover(); + await expect(n8n.ndv.outputPanel.get().locator('[data-test-id="hovering-item"]')).toContainText( + '1000', + ); + await expect(n8n.ndv.getParameterExpressionPreviewValue()).toContainText('1000'); + + // Switch back to Sort input + await n8n.ndv.inputPanel.getNodeInputOptions().click(); + await n8n.page.getByRole('option', { name: 'Sort' }).click(); + await n8n.ndv.changeOutputRunSelector('1 of 2 (6 items)'); + + await expect(hoveringItem).toContainText('1111'); + await n8n.ndv.inputPanel.getTable().locator('text=1111').hover(); + await expect(n8n.ndv.outputPanel.get().locator('[data-test-id="hovering-item"]')).toContainText( + '1111', + ); + await expect(n8n.ndv.getParameterExpressionPreviewValue()).toContainText('1111'); + }); + + test('maps paired input and output items based on selected run', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_5.json'); + await n8n.canvas.clickZoomToFitButton(); + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + await n8n.canvas.openNode('Set3'); + + await n8n.ndv.inputPanel.switchDisplayMode('table'); + await n8n.ndv.outputPanel.switchDisplayMode('table'); + + // Start from linked state + await n8n.ndv.ensureOutputRunLinking(true); + await n8n.ndv.inputPanel.getTbodyCell(0, 0).click(); // remove tooltip + + await expect(n8n.ndv.inputPanel.getRunSelectorInput()).toHaveValue('2 of 2 (6 items)'); + await expect(n8n.ndv.outputPanel.getRunSelectorInput()).toHaveValue('2 of 2 (6 items)'); + + await n8n.ndv.changeOutputRunSelector('1 of 2 (6 items)'); + await expect(n8n.ndv.inputPanel.getRunSelectorInput()).toHaveValue('1 of 2 (6 items)'); + await expect(n8n.ndv.outputPanel.getRunSelectorInput()).toHaveValue('1 of 2 (6 items)'); + + await expect(n8n.ndv.inputPanel.getTableRow(1)).toContainText('1111'); + await expect(n8n.ndv.inputPanel.getTableRow(1)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await expect(n8n.ndv.outputPanel.getTableRow(1)).toContainText('1111'); + await n8n.ndv.outputPanel.getTableRow(1).hover(); + + await expect(n8n.ndv.outputPanel.getTableRow(3)).toContainText('4444'); + await n8n.ndv.outputPanel.getTableRow(3).hover(); + + await expect(n8n.ndv.inputPanel.getTableRow(3)).toContainText('4444'); + await expect(n8n.ndv.inputPanel.getTableRow(3)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await n8n.ndv.changeOutputRunSelector('2 of 2 (6 items)'); + + await expect(n8n.ndv.inputPanel.getTableRow(1)).toContainText('1000'); + await n8n.ndv.inputPanel.getTableRow(1).hover(); + + await expect(n8n.ndv.outputPanel.getTableRow(1)).toContainText('1000'); + await expect(n8n.ndv.outputPanel.getTableRow(1)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await expect(n8n.ndv.outputPanel.getTableRow(3)).toContainText('2000'); + await n8n.ndv.outputPanel.getTableRow(3).hover(); + + await expect(n8n.ndv.inputPanel.getTableRow(3)).toContainText('2000'); + await expect(n8n.ndv.inputPanel.getTableRow(3)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + }); + + test('can pair items between input and output across branches and runs', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_5.json'); + await n8n.canvas.clickZoomToFitButton(); + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + await n8n.canvas.openNode('IF'); + + await n8n.ndv.inputPanel.switchDisplayMode('table'); + await n8n.ndv.outputPanel.switchDisplayMode('table'); + + // Switch to False Branch + await n8n.ndv.outputPanel.get().getByText('False Branch (2 items)').click(); + await expect(n8n.ndv.outputPanel.getTableRow(1)).toContainText('8888'); + await n8n.ndv.outputPanel.getTableRow(1).hover(); + + await expect(n8n.ndv.inputPanel.getTableRow(5)).toContainText('8888'); + await expect(n8n.ndv.inputPanel.getTableRow(5)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await expect(n8n.ndv.outputPanel.getTableRow(2)).toContainText('9999'); + await n8n.ndv.outputPanel.getTableRow(2).hover(); + + await expect(n8n.ndv.inputPanel.getTableRow(6)).toContainText('9999'); + await expect(n8n.ndv.inputPanel.getTableRow(6)).toHaveAttribute( + 'data-test-id', + 'hovering-item', + ); + + await n8n.ndv.close(); + + await n8n.canvas.openNode('Set5'); + + // Switch to True Branch for input + await n8n.ndv.inputPanel.get().getByText('True Branch').click(); + + await n8n.ndv.changeOutputRunSelector('(2 items)'); + await expect(n8n.ndv.outputPanel.getTableRow(1)).toContainText('8888'); + await n8n.ndv.outputPanel.getTableRow(1).hover(); + + // Should not have matching hover state when branches don't match + const hoveringItems = n8n.ndv.inputPanel.get().locator('[data-test-id="hovering-item"]'); + await expect(hoveringItems).toHaveCount(0); + + await expect(n8n.ndv.inputPanel.getTableRow(1)).toContainText('1111'); + await n8n.ndv.inputPanel.getTableRow(1).hover(); + const outputHoveringItems = n8n.ndv.outputPanel.get().locator('[data-test-id="hovering-item"]'); + await expect(outputHoveringItems).toHaveCount(0); + + // Switch to False Branch + await n8n.ndv.inputPanel.get().getByText('False Branch').click(); + await expect(n8n.ndv.inputPanel.getTableRow(1)).toContainText('8888'); + await n8n.ndv.inputPanel.getTableRow(1).hover(); + + await n8n.ndv.changeOutputRunSelector('(4 items)'); + await expect(n8n.ndv.outputPanel.getTableRow(1)).toContainText('1111'); + await n8n.ndv.outputPanel.getTableRow(1).hover(); + + await n8n.ndv.changeOutputRunSelector('(2 items)'); + await expect(n8n.ndv.inputPanel.getTableRow(1)).toContainText('8888'); + await n8n.ndv.inputPanel.getTableRow(1).hover(); + await expect(n8n.ndv.outputPanel.get().locator('[data-test-id="hovering-item"]')).toContainText( + '8888', + ); + }); + + test('can resolve expression with paired item in multi-input node', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('expression_with_paired_item_in_multi_input_node.json'); + + await n8n.canvas.clickZoomToFitButton(); + + const PINNED_DATA = [ + { + id: 'abc', + historyId: 'def', + messages: [ + { + id: 'abc', + }, + ], + }, + { + id: 'abc', + historyId: 'def', + messages: [ + { + id: 'abc', + }, + { + id: 'abc', + }, + { + id: 'abc', + }, + ], + }, + { + id: 'abc', + historyId: 'def', + messages: [ + { + id: 'abc', + }, + ], + }, + ]; + + await n8n.canvas.openNode('Get thread details1'); + await n8n.ndv.setPinnedData(PINNED_DATA); + await n8n.ndv.close(); + + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + await n8n.canvas.openNode('Switch1'); + await n8n.ndv.execute(); + + await expect(n8n.ndv.getParameterExpressionPreviewOutput()).toContainText('1'); + + await n8n.ndv.getInlineExpressionEditorInput().click(); + await expect(n8n.ndv.getInlineExpressionEditorPreview()).toContainText('1'); + + // Select next item + await n8n.ndv.expressionSelectNextItem(); + await expect(n8n.ndv.getInlineExpressionEditorPreview()).toContainText('3'); + + // Select next item again + await n8n.ndv.expressionSelectNextItem(); + await expect(n8n.ndv.getInlineExpressionEditorPreview()).toContainText('1'); + + // Next button should be disabled + await expect(n8n.ndv.getInlineExpressionEditorItemNextButton()).toBeDisabled(); + }); +}); diff --git a/packages/testing/playwright/workflows/expression_with_paired_item_in_multi_input_node.json b/packages/testing/playwright/workflows/expression_with_paired_item_in_multi_input_node.json new file mode 100644 index 0000000000..56bae70e0d --- /dev/null +++ b/packages/testing/playwright/workflows/expression_with_paired_item_in_multi_input_node.json @@ -0,0 +1,138 @@ +{ + "meta": { + "instanceId": "abc" + }, + "nodes": [ + { + "parameters": {}, + "id": "bcb6abdf-d34b-4ea7-a8ed-58155b708c43", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [20, 260] + }, + { + "parameters": { + "jsCode": "// Loop over input items and add a new field\n// called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\nitem.json.message_count = Math.min(item.json.messages.length, 3);\n}\n\nreturn $input.all();" + }, + "id": "59c3889c-3671-4f49-b258-6131df8587d8", + "name": "Set thread properties1", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [500, 520] + }, + { + "parameters": { + "resource": "thread", + "operation": "get", + "threadId": "={{ $json.id }}", + "options": {} + }, + "id": "e102b72e-1e47-4004-a6b9-38cef75f44a1", + "name": "Get thread details1", + "type": "n8n-nodes-base.gmail", + "typeVersion": 2, + "position": [300, 520] + }, + { + "parameters": { + "mode": "expression", + "output": "={{ $('Set thread properties1').item.json.message_count }}" + }, + "id": "f3e42f07-df82-42ba-8e99-97cda707a9d9", + "name": "Switch1", + "type": "n8n-nodes-base.switch", + "typeVersion": 1, + "position": [1220, 540] + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": true, + "value2": true + } + ] + } + }, + "id": "c7fe521e-8c02-44bf-8a14-482b39749508", + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [720, 520] + }, + { + "parameters": {}, + "id": "3b9f6a05-7f19-46c5-95d1-5dec732f00ae", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [960, 400] + } + ], + "connections": { + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Get thread details1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set thread properties1": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get thread details1": { + "main": [ + [ + { + "node": "Set thread properties1", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Switch1", + "type": "main", + "index": 0 + } + ] + ] + }, + "No Operation, do nothing": { + "main": [ + [ + { + "node": "Switch1", + "type": "main", + "index": 0 + } + ] + ] + } + } +}