diff --git a/packages/testing/playwright/pages/CanvasPage.ts b/packages/testing/playwright/pages/CanvasPage.ts index e5a7e7272e..fb13c22d65 100644 --- a/packages/testing/playwright/pages/CanvasPage.ts +++ b/packages/testing/playwright/pages/CanvasPage.ts @@ -57,36 +57,63 @@ export class CanvasPage extends BasePage { await this.nodeCreatorItemByName(text).click(); } - async addNode(text: string): Promise { + /** + * Add a node to the canvas with flexible options + * @param nodeName - The name of the node to search for and add + * @param options - Configuration options for node addition + * @param options.closeNDV - Whether to close the NDV after adding (default: false, keeps open) + * @param options.action - Specific action to select (Actions tab is default) + * @param options.trigger - Specific trigger to select (will switch to Triggers) + * @example + * // Basic node addition + * await canvas.addNode('Code'); + * + * // Add with specific action + * await canvas.addNode('Linear', { action: 'Create an issue' }); + * + * // Add with trigger + * await canvas.addNode('Jira', { trigger: 'On issue created' }); + * + * // Add and explicitly close with back button + * await canvas.addNode('Code', { closeNDV: true }); + */ + async addNode( + nodeName: string, + options?: { + closeNDV?: boolean; + action?: string; + trigger?: string; + }, + ): Promise { + // Always start with canvas plus button await this.clickNodeCreatorPlusButton(); - await this.fillNodeCreatorSearchBar(text); - await this.clickNodeCreatorItemName(text); - } - async addNodeAndCloseNDV(text: string, subItemText?: string): Promise { - if (subItemText) { - await this.addNodeWithSubItem(text, subItemText); - } else { - await this.addNode(text); + // Search for and select the node, works on exact name match only + await this.fillNodeCreatorSearchBar(nodeName); + await this.clickNodeCreatorItemName(nodeName); + + if (options?.action) { + // Check if Actions category is collapsed and expand if needed + const actionsCategory = this.page + .getByTestId('node-creator-category-item') + .getByText('Actions'); + if ((await actionsCategory.getAttribute('data-category-collapsed')) === 'true') { + await actionsCategory.click(); + } + await this.nodeCreatorSubItem(options.action).click(); + } else if (options?.trigger) { + // Check if Triggers category is collapsed and expand if needed + const triggersCategory = this.page + .getByTestId('node-creator-category-item') + .getByText('Triggers'); + if ((await triggersCategory.getAttribute('data-category-collapsed')) === 'true') { + await triggersCategory.click(); + } + await this.nodeCreatorSubItem(options.trigger).click(); + } + if (options?.closeNDV) { + await this.page.getByTestId('back-to-canvas').click(); } - await this.page.keyboard.press('Escape'); - } - - async addNodeWithSubItem(searchText: string, subItemText: string): Promise { - await this.addNode(searchText); - await this.nodeCreatorSubItem(subItemText).click(); - } - - async addActionNode(searchText: string, subItemText: string): Promise { - await this.addNode(searchText); - await this.page.getByText('Actions').click(); - await this.nodeCreatorSubItem(subItemText).click(); - } - - async addTriggerNode(searchText: string, subItemText: string): Promise { - await this.addNode(searchText); - await this.page.getByText('Triggers').click(); - await this.nodeCreatorSubItem(subItemText).click(); } async deleteNodeByName(nodeName: string): Promise { @@ -301,7 +328,7 @@ export class CanvasPage extends BasePage { } nodeCreatorNodeItems(): Locator { - return this.page.getByTestId('node-creator-node-item'); + return this.page.getByTestId('node-creator-item-name'); } nodeCreatorActionItems(): Locator { diff --git a/packages/testing/playwright/tests/ui/02-canvas-actions.spec.ts b/packages/testing/playwright/tests/ui/02-canvas-actions.spec.ts new file mode 100644 index 0000000000..166feb70b0 --- /dev/null +++ b/packages/testing/playwright/tests/ui/02-canvas-actions.spec.ts @@ -0,0 +1,91 @@ +import { MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../../config/constants'; +import { test, expect } from '../../fixtures/base'; + +test.describe('Canvas Node Actions', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.goHome(); + await n8n.workflows.clickAddWorkflowButton(); + }); + + test.describe('Node Search and Add', () => { + test('should search and add a basic node', async ({ n8n }) => { + await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME); + + await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1); + await expect(n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME)).toBeVisible(); + }); + + test('should search and add Linear node with action', async ({ n8n }) => { + await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME); + await n8n.canvas.addNode('Linear', { action: 'Create an issue' }); + + await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2); + await expect(n8n.canvas.nodeConnections()).toHaveCount(1); + await expect(n8n.canvas.nodeByName('Create an issue')).toBeVisible(); + }); + + test('should search and add Webhook node (no actions)', async ({ n8n }) => { + await n8n.canvas.addNode('Webhook'); + + await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1); + await expect(n8n.canvas.nodeByName('Webhook')).toBeVisible(); + }); + + test('should search and add Jira node with trigger', async ({ n8n }) => { + await n8n.canvas.addNode('Jira Software', { trigger: 'On issue created' }); + await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1); + await expect(n8n.canvas.nodeByName('Jira Trigger')).toBeVisible(); + }); + + test('should clear search and show all nodes', async ({ n8n }) => { + await n8n.canvas.clickCanvasPlusButton(); + await n8n.canvas.fillNodeCreatorSearchBar('Linear'); + const searchCount = await n8n.canvas.nodeCreatorNodeItems().count(); + await expect(n8n.canvas.nodeCreatorNodeItems()).toHaveCount(1); + + await n8n.canvas.nodeCreatorSearchBar().clear(); + const nodeCount = await n8n.canvas.nodeCreatorNodeItems().count(); + expect(nodeCount).toBeGreaterThan(searchCount); + }); + + test('should add connected node via 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'); + 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 disconnected node when nothing selected', async ({ n8n }) => { + await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME); + await n8n.canvas.deselectAll(); + await n8n.canvas.addNode('Code', { closeNDV: true }); + await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2); + await expect(n8n.canvas.nodeConnections()).toHaveCount(0); + }); + }); + + test.describe('Node Creator Interactions', () => { + test('should close node creator with escape key', async ({ n8n }) => { + await n8n.canvas.clickCanvasPlusButton(); + await expect(n8n.canvas.nodeCreatorSearchBar()).toBeVisible(); + + await n8n.page.keyboard.press('Escape'); + await expect(n8n.canvas.nodeCreatorSearchBar()).toBeHidden(); + }); + + test('should filter nodes by search term', async ({ n8n }) => { + await n8n.canvas.clickCanvasPlusButton(); + const initialCount = await n8n.canvas.nodeCreatorNodeItems().count(); + await n8n.canvas.fillNodeCreatorSearchBar('HTTP'); + const filteredCount = await n8n.canvas.nodeCreatorNodeItems().count(); + + expect(filteredCount).toBeLessThan(initialCount); + expect(filteredCount).toBeGreaterThan(0); + }); + }); +}); diff --git a/packages/testing/playwright/tests/ui/12-canvas-actions.spec.ts b/packages/testing/playwright/tests/ui/12-canvas-actions.spec.ts index 3921a1d2ec..96f18dc3b7 100644 --- a/packages/testing/playwright/tests/ui/12-canvas-actions.spec.ts +++ b/packages/testing/playwright/tests/ui/12-canvas-actions.spec.ts @@ -61,7 +61,7 @@ test.describe('Canvas Actions', () => { 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 n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true }); await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2); await expect(n8n.canvas.nodeConnections()).toHaveCount(0); @@ -70,7 +70,7 @@ test.describe('Canvas Actions', () => { 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 n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true }); await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2); await expect(n8n.canvas.nodeConnections()).toHaveCount(1); @@ -96,7 +96,7 @@ test.describe('Canvas Actions', () => { 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.addNode(CODE_NODE_NAME, { closeNDV: true }); await n8n.canvas.deleteConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME); await expect(n8n.canvas.nodeConnections()).toHaveCount(0); @@ -116,7 +116,7 @@ test.describe('Canvas Actions', () => { test('should disable and enable node', async ({ n8n }) => { await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME); - await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME); + await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true }); const disableButton = n8n.canvas.nodeDisableButton(CODE_NODE_NAME); await disableButton.click(); @@ -130,7 +130,7 @@ test.describe('Canvas Actions', () => { test('should delete node', async ({ n8n }) => { await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME); - await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME); + await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true }); await n8n.canvas.deleteNodeByName(CODE_NODE_NAME); await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1); @@ -140,7 +140,7 @@ test.describe('Canvas Actions', () => { test('should copy selected nodes', async ({ n8n }) => { await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME); - await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME); + await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true }); await n8n.canvasComposer.selectAllAndCopy(); await n8n.canvas.nodeByName(CODE_NODE_NAME).click(); await n8n.canvasComposer.copySelectedNodesWithToast(); @@ -150,7 +150,7 @@ test.describe('Canvas Actions', () => { 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.addNode(CODE_NODE_NAME, { closeNDV: true }); await n8n.canvas.selectAll(); await expect(n8n.canvas.selectedNodes()).toHaveCount(2); @@ -162,7 +162,7 @@ test.describe('Canvas Actions', () => { 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.addNode(CODE_NODE_NAME, { closeNDV: true }); await n8n.canvas.getCanvasNodes().first().waitFor(); await n8n.canvas.navigateNodesWithArrows('left'); @@ -177,7 +177,7 @@ test.describe('Canvas Actions', () => { 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.addNode(CODE_NODE_NAME, { closeNDV: true }); await n8n.canvas.getCanvasNodes().first().waitFor(); await n8n.canvas.extendSelectionWithArrows('left'); diff --git a/packages/testing/playwright/tests/ui/13-pinning.spec.ts b/packages/testing/playwright/tests/ui/13-pinning.spec.ts index 398d648e58..6dd939a76f 100644 --- a/packages/testing/playwright/tests/ui/13-pinning.spec.ts +++ b/packages/testing/playwright/tests/ui/13-pinning.spec.ts @@ -165,7 +165,7 @@ test.describe('Data pinning', () => { await n8n.ndv.setPinnedData([{ http: 123 }]); await n8n.ndv.close(); - await n8n.canvas.addNodeWithSubItem(NODES.PIPEDRIVE, 'Create an activity'); + await n8n.canvas.addNode(NODES.PIPEDRIVE, { action: 'Create an activity' }); await n8n.ndv.setPinnedData(Array(3).fill({ pipedrive: 123 })); await n8n.ndv.close(); diff --git a/packages/testing/playwright/tests/ui/39-projects.spec.ts b/packages/testing/playwright/tests/ui/39-projects.spec.ts index ac45fe9bc2..ed23341b9f 100644 --- a/packages/testing/playwright/tests/ui/39-projects.spec.ts +++ b/packages/testing/playwright/tests/ui/39-projects.spec.ts @@ -63,7 +63,7 @@ test.describe('Projects', () => { n8n.page.getByText('Workflow successfully created', { exact: false }), ).toBeVisible(); - await n8n.canvas.addNodeWithSubItem(EXECUTE_WORKFLOW_NODE_NAME, 'Execute A Sub Workflow'); + await n8n.canvas.addNode(EXECUTE_WORKFLOW_NODE_NAME, { action: 'Execute A Sub Workflow' }); const subWorkflowPagePromise = n8n.page.waitForEvent('popup'); @@ -74,7 +74,7 @@ test.describe('Projects', () => { await subn8n.ndv.clickBackToCanvasButton(); await subn8n.canvas.deleteNodeByName('Replace me with your logic'); - await subn8n.canvas.addNodeWithSubItem(NOTION_NODE_NAME, 'Append a block'); + await subn8n.canvas.addNode(NOTION_NODE_NAME, { action: 'Append a block' }); await subn8n.credentials.createAndSaveNewCredential('apiKey', NOTION_API_KEY); diff --git a/packages/testing/playwright/tests/ui/53-workflow-production-checklist.spec.ts b/packages/testing/playwright/tests/ui/53-workflow-production-checklist.spec.ts index 11159b6e02..8dddc0065a 100644 --- a/packages/testing/playwright/tests/ui/53-workflow-production-checklist.spec.ts +++ b/packages/testing/playwright/tests/ui/53-workflow-production-checklist.spec.ts @@ -11,7 +11,7 @@ test.describe('Workflow Production Checklist', () => { test('should show suggested actions automatically when workflow is first activated', async ({ n8n, }) => { - await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME); + await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME, { closeNDV: true }); await n8n.canvas.saveWorkflow(); await expect(n8n.canvas.getProductionChecklistButton()).toBeHidden(); @@ -34,8 +34,8 @@ test.describe('Workflow Production Checklist', () => { }) => { await api.enableFeature('evaluation'); - await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME); - await n8n.canvas.addNodeAndCloseNDV('OpenAI', 'Create an assistant'); + await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME, { closeNDV: true }); + await n8n.canvas.addNode('OpenAI', { action: 'Create an assistant', closeNDV: true }); await n8n.canvas.nodeDisableButton('Create an assistant').click(); @@ -56,7 +56,7 @@ test.describe('Workflow Production Checklist', () => { test('should open workflow settings modal when error workflow action is clicked', async ({ n8n, }) => { - await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME); + await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME, { closeNDV: true }); await n8n.canvas.saveWorkflow(); await n8n.canvas.activateWorkflow(); await expect(n8n.workflowActivationModal.getModal()).toBeVisible(); @@ -73,7 +73,7 @@ test.describe('Workflow Production Checklist', () => { }); test('should open workflow settings modal when time saved action is clicked', async ({ n8n }) => { - await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME); + await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME, { closeNDV: true }); await n8n.canvas.saveWorkflow(); await n8n.canvas.activateWorkflow(); await expect(n8n.workflowActivationModal.getModal()).toBeVisible(); @@ -89,7 +89,7 @@ test.describe('Workflow Production Checklist', () => { }); test('should allow ignoring individual actions', async ({ n8n }) => { - await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME); + await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME, { closeNDV: true }); await n8n.canvas.saveWorkflow(); await n8n.canvas.activateWorkflow(); await expect(n8n.workflowActivationModal.getModal()).toBeVisible(); @@ -109,7 +109,7 @@ test.describe('Workflow Production Checklist', () => { }); test('should show completed state for configured actions', async ({ n8n }) => { - await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME); + await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME, { closeNDV: true }); await n8n.canvas.saveWorkflow(); await n8n.canvas.activateWorkflow(); await expect(n8n.workflowActivationModal.getModal()).toBeVisible(); @@ -134,7 +134,7 @@ test.describe('Workflow Production Checklist', () => { }); test('should allow ignoring all actions with confirmation', async ({ n8n }) => { - await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME); + await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME, { closeNDV: true }); await n8n.canvas.saveWorkflow(); await n8n.canvas.activateWorkflow(); await expect(n8n.workflowActivationModal.getModal()).toBeVisible();