test(editor): Migrate Paired item e2e tests to playwright (#19536)

This commit is contained in:
Raúl Gómez Morales
2025-09-15 10:29:34 +02:00
committed by GitHub
parent c6e147550c
commit 66e3f2b639
3 changed files with 462 additions and 0 deletions

View File

@@ -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');
}

View File

@@ -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();
});
});

View File

@@ -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
}
]
]
}
}
}