test: Migrate 12-canvas-actions tests to Playwright (#18442)

This commit is contained in:
shortstacked
2025-08-18 10:32:38 +01:00
committed by GitHub
parent b3a9a0d097
commit cbf935af91
8 changed files with 378 additions and 494 deletions

View File

@@ -12,4 +12,29 @@ export class CanvasComposer {
await this.n8n.ndv.togglePinData();
await this.n8n.ndv.close();
}
/**
* Execute a node and wait for success toast notification
* @param nodeName - The node to execute
*/
async executeNodeAndWaitForToast(nodeName: string): Promise<void> {
await this.n8n.canvas.executeNode(nodeName);
await this.n8n.notifications.waitForNotificationAndClose('Node executed successfully');
}
/**
* Copy selected nodes and verify success toast
*/
async copySelectedNodesWithToast(): Promise<void> {
await this.n8n.canvas.copyNodes();
await this.n8n.notifications.waitForNotificationAndClose('Copied to clipboard');
}
/**
* Select all nodes and copy them
*/
async selectAllAndCopy(): Promise<void> {
await this.n8n.canvas.selectAll();
await this.copySelectedNodesWithToast();
}
}

View File

@@ -10,7 +10,7 @@ export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';
export const SET_NODE_NAME = 'Set';
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields';
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields (Set)';
export const LOOP_OVER_ITEMS_NODE_NAME = 'Loop Over Items';
export const IF_NODE_NAME = 'If';
export const MERGE_NODE_NAME = 'Merge';

View File

@@ -9,14 +9,6 @@ export class CanvasPage extends BasePage {
return this.page.getByRole('button', { name: 'Save' });
}
workflowSaveButton(): Locator {
return this.page.getByTestId('workflow-save-button');
}
canvasAddButton(): Locator {
return this.page.getByTestId('canvas-add-button');
}
nodeCreatorItemByName(text: string): Locator {
return this.page.getByTestId('node-creator-item-name').getByText(text, { exact: true });
}
@@ -50,11 +42,11 @@ export class CanvasPage extends BasePage {
}
async clickSaveWorkflowButton(): Promise<void> {
await this.clickButtonByName('Save');
await this.saveWorkflowButton().click();
}
async fillNodeCreatorSearchBar(text: string): Promise<void> {
await this.fillByTestId('node-creator-search-bar', text);
await this.nodeCreatorSearchBar().fill(text);
}
async clickNodeCreatorItemName(text: string): Promise<void> {
@@ -69,14 +61,14 @@ export class CanvasPage extends BasePage {
async addNodeAndCloseNDV(text: string, subItemText?: string): Promise<void> {
if (subItemText) {
await this.addNodeToCanvasWithSubItem(text, subItemText);
await this.addNodeWithSubItem(text, subItemText);
} else {
await this.addNode(text);
}
await this.page.keyboard.press('Escape');
}
async addNodeToCanvasWithSubItem(searchText: string, subItemText: string): Promise<void> {
async addNodeWithSubItem(searchText: string, subItemText: string): Promise<void> {
await this.addNode(searchText);
await this.nodeCreatorSubItem(subItemText).click();
}
@@ -97,12 +89,12 @@ export class CanvasPage extends BasePage {
await this.page.getByRole('button', { name: 'Debug in editor' }).click();
}
async pinNodeByNameUsingContextMenu(nodeName: string): Promise<void> {
async pinNode(nodeName: string): Promise<void> {
await this.nodeByName(nodeName).click({ button: 'right' });
await this.page.getByTestId('context-menu').getByText('Pin').click();
}
async unpinNodeByNameUsingContextMenu(nodeName: string): Promise<void> {
async unpinNode(nodeName: string): Promise<void> {
await this.nodeByName(nodeName).click({ button: 'right' });
await this.page.getByText('Unpin').click();
}
@@ -215,6 +207,10 @@ export class CanvasPage extends BasePage {
return tags;
}
getWorkflowSaveButton(): Locator {
return this.page.getByTestId('workflow-save-button');
}
// Production Checklist methods
getProductionChecklistButton(): Locator {
return this.page.getByTestId('suggested-action-count');
@@ -260,7 +256,159 @@ export class CanvasPage extends BasePage {
await this.getProductionChecklistActionItem(actionText).click();
}
getCanvasNodes() {
getCanvasNodes(): Locator {
return this.page.getByTestId('canvas-node');
}
nodeConnections(): Locator {
return this.page.locator('[data-test-id="edge"]');
}
canvasNodePlusEndpointByName(nodeName: string): Locator {
return this.page
.locator(
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`,
)
.first();
}
nodeCreatorSearchBar(): Locator {
return this.page.getByTestId('node-creator-search-bar');
}
nodeCreatorNodeItems(): Locator {
return this.page.getByTestId('node-creator-node-item');
}
nodeCreatorActionItems(): Locator {
return this.page.getByTestId('node-creator-action-item');
}
nodeCreatorCategoryItems(): Locator {
return this.page.getByTestId('node-creator-category-item');
}
selectedNodes(): Locator {
return this.page
.locator('[data-test-id="canvas-node"]')
.locator('xpath=..')
.locator('.selected');
}
disabledNodes(): Locator {
return this.page.locator('[data-canvas-node-render-type][class*="disabled"]');
}
nodeExecuteButton(nodeName: string): Locator {
return this.nodeToolbar(nodeName).getByTestId('execute-node-button');
}
canvasPane(): Locator {
return this.page.getByTestId('canvas-wrapper');
}
// Actions
async addInitialNodeToCanvas(nodeName: string): Promise<void> {
await this.clickCanvasPlusButton();
await this.fillNodeCreatorSearchBar(nodeName);
await this.clickNodeCreatorItemName(nodeName);
}
async clickNodePlusEndpoint(nodeName: string): Promise<void> {
await this.canvasNodePlusEndpointByName(nodeName).click();
}
async executeNode(nodeName: string): Promise<void> {
await this.nodeByName(nodeName).hover();
await this.nodeExecuteButton(nodeName).click();
}
async selectAll(): Promise<void> {
await this.page.keyboard.press('ControlOrMeta+a');
}
async copyNodes(): Promise<void> {
await this.page.keyboard.press('ControlOrMeta+c');
}
async deselectAll(): Promise<void> {
await this.canvasPane().click({ position: { x: 10, y: 10 } });
}
getNodeLeftPosition(nodeLocator: Locator): Promise<number> {
return nodeLocator.evaluate((el) => el.getBoundingClientRect().left);
}
// Connection helpers
connectionBetweenNodes(sourceNodeName: string, targetNodeName: string): Locator {
return this.page.locator(
`[data-test-id="edge"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`,
);
}
connectionToolbarBetweenNodes(sourceNodeName: string, targetNodeName: string): Locator {
return this.page.locator(
`[data-test-id="edge-label"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`,
);
}
// Canvas action helpers
async addNodeBetweenNodes(
sourceNodeName: string,
targetNodeName: string,
newNodeName: string,
): Promise<void> {
const specificConnection = this.connectionBetweenNodes(sourceNodeName, targetNodeName);
// eslint-disable-next-line playwright/no-force-option
await specificConnection.hover({ force: true });
const addNodeButton = this.connectionToolbarBetweenNodes(
sourceNodeName,
targetNodeName,
).getByTestId('add-connection-button');
await addNodeButton.click();
await this.fillNodeCreatorSearchBar(newNodeName);
await this.clickNodeCreatorItemName(newNodeName);
await this.page.keyboard.press('Escape');
}
async deleteConnectionBetweenNodes(
sourceNodeName: string,
targetNodeName: string,
): Promise<void> {
const specificConnection = this.connectionBetweenNodes(sourceNodeName, targetNodeName);
// eslint-disable-next-line playwright/no-force-option
await specificConnection.hover({ force: true });
const deleteButton = this.connectionToolbarBetweenNodes(
sourceNodeName,
targetNodeName,
).getByTestId('delete-connection-button');
await deleteButton.click();
}
async navigateNodesWithArrows(direction: 'left' | 'right' | 'up' | 'down'): Promise<void> {
const keyMap = {
left: 'ArrowLeft',
right: 'ArrowRight',
up: 'ArrowUp',
down: 'ArrowDown',
};
await this.canvasPane().focus();
await this.page.keyboard.press(keyMap[direction]);
}
async extendSelectionWithArrows(direction: 'left' | 'right' | 'up' | 'down'): Promise<void> {
const keyMap = {
left: 'Shift+ArrowLeft',
right: 'Shift+ArrowRight',
up: 'Shift+ArrowUp',
down: 'Shift+ArrowDown',
};
await this.canvasPane().focus();
await this.page.keyboard.press(keyMap[direction]);
}
}

View File

@@ -0,0 +1,186 @@
import {
MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
CODE_NODE_NAME,
HTTP_REQUEST_NODE_NAME,
} from '../../config/constants';
import { test, expect } from '../../fixtures/base';
test.describe('Canvas Actions', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.goHome();
await n8n.workflows.clickAddWorkflowButton();
});
test('should add first step', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
});
test('should add a connected node using plus endpoint', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
await n8n.page.keyboard.press('Enter');
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
});
test('should add a connected node dragging from node creator', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
const sourceElement = n8n.canvas
.nodeCreatorNodeItems()
.filter({ hasText: CODE_NODE_NAME })
.first();
await sourceElement.dragTo(n8n.canvas.canvasPane(), { targetPosition: { x: 100, y: 100 } });
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
});
test('should open a category when trying to drag and drop it on the canvas', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
const categoryItem = n8n.canvas.nodeCreatorActionItems().first();
await categoryItem.dragTo(n8n.canvas.canvasPane(), {
targetPosition: { x: 100, y: 100 },
});
await expect(n8n.canvas.nodeCreatorCategoryItems()).toHaveCount(1);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
});
test('should add disconnected node if nothing is selected', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.deselectAll();
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
});
test('should add node between two connected nodes', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
await n8n.canvas.addNodeBetweenNodes(
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
CODE_NODE_NAME,
HTTP_REQUEST_NODE_NAME,
);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
await expect(n8n.canvas.nodeConnections()).toHaveCount(2);
});
test('should delete node by pressing keyboard backspace', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
await n8n.page.keyboard.press('Backspace');
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
});
test('should delete connections by clicking on the delete button', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
await n8n.canvas.deleteConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME);
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
});
test.describe('Node hover actions', () => {
test('should execute node', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.executeNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await expect(
n8n.notifications.notificationContainerByText('Node executed successfully'),
).toHaveCount(1);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
});
test('should disable and enable node', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
const disableButton = n8n.canvas.nodeDisableButton(CODE_NODE_NAME);
await disableButton.click();
await expect(n8n.canvas.disabledNodes()).toHaveCount(1);
await disableButton.click();
await expect(n8n.canvas.disabledNodes()).toHaveCount(0);
});
test('should delete node', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
await n8n.canvas.deleteNodeByName(CODE_NODE_NAME);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
await expect(n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME)).toBeVisible();
});
});
test('should copy selected nodes', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
await n8n.canvasComposer.selectAllAndCopy();
await n8n.canvas.nodeByName(CODE_NODE_NAME).click();
await n8n.canvasComposer.copySelectedNodesWithToast();
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
});
test('should select/deselect all nodes', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
await n8n.canvas.selectAll();
await expect(n8n.canvas.selectedNodes()).toHaveCount(2);
await n8n.canvas.deselectAll();
await expect(n8n.canvas.selectedNodes()).toHaveCount(0);
});
test('should select nodes using arrow keys', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
await n8n.canvas.getCanvasNodes().first().waitFor();
await n8n.canvas.navigateNodesWithArrows('left');
const selectedNodes = n8n.canvas.selectedNodes();
await expect(selectedNodes.first()).toHaveClass(/selected/);
await n8n.canvas.navigateNodesWithArrows('right');
await expect(selectedNodes.last()).toHaveClass(/selected/);
});
test('should select nodes using shift and arrow keys', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
await n8n.canvas.getCanvasNodes().first().waitFor();
await n8n.canvas.extendSelectionWithArrows('left');
await expect(n8n.canvas.selectedNodes()).toHaveCount(2);
});
});

View File

@@ -15,6 +15,6 @@ test.describe('ADO-2270 Save button resets on webhook node open', () => {
await n8n.ndv.clickBackToCanvasButton();
await expect(n8n.canvas.workflowSaveButton()).toContainText('Saved');
await expect(n8n.canvas.getWorkflowSaveButton()).toContainText('Saved');
});
});

View File

@@ -63,10 +63,7 @@ test.describe('Projects @db:reset', () => {
n8n.page.getByText('Workflow successfully created', { exact: false }),
).toBeVisible();
await n8n.canvas.addNodeToCanvasWithSubItem(
EXECUTE_WORKFLOW_NODE_NAME,
'Execute A Sub Workflow',
);
await n8n.canvas.addNodeWithSubItem(EXECUTE_WORKFLOW_NODE_NAME, 'Execute A Sub Workflow');
const subWorkflowPagePromise = n8n.page.waitForEvent('popup');
@@ -77,7 +74,7 @@ test.describe('Projects @db:reset', () => {
await subn8n.ndv.clickBackToCanvasButton();
await subn8n.canvas.deleteNodeByName('Replace me with your logic');
await subn8n.canvas.addNodeToCanvasWithSubItem(NOTION_NODE_NAME, 'Append a block');
await subn8n.canvas.addNodeWithSubItem(NOTION_NODE_NAME, 'Append a block');
await subn8n.credentials.createAndSaveNewCredential('apiKey', NOTION_API_KEY);