test: Create addNode convenience method (#18750)

This commit is contained in:
shortstacked
2025-08-26 10:23:57 +01:00
committed by GitHub
parent 9bc4f07b79
commit ede6f5b739
6 changed files with 166 additions and 48 deletions

View File

@@ -57,36 +57,63 @@ export class CanvasPage extends BasePage {
await this.nodeCreatorItemByName(text).click();
}
async addNode(text: string): Promise<void> {
/**
* 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<void> {
// Always start with canvas plus button
await this.clickNodeCreatorPlusButton();
await this.fillNodeCreatorSearchBar(text);
await this.clickNodeCreatorItemName(text);
}
async addNodeAndCloseNDV(text: string, subItemText?: string): Promise<void> {
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<void> {
await this.addNode(searchText);
await this.nodeCreatorSubItem(subItemText).click();
}
async addActionNode(searchText: string, subItemText: string): Promise<void> {
await this.addNode(searchText);
await this.page.getByText('Actions').click();
await this.nodeCreatorSubItem(subItemText).click();
}
async addTriggerNode(searchText: string, subItemText: string): Promise<void> {
await this.addNode(searchText);
await this.page.getByText('Triggers').click();
await this.nodeCreatorSubItem(subItemText).click();
}
async deleteNodeByName(nodeName: string): Promise<void> {
@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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