test: Migrate data pinning tests from Cypress to Playwright (#18462)

This commit is contained in:
shortstacked
2025-08-18 12:28:32 +01:00
committed by GitHub
parent 08d82491c8
commit 7c80086a6b
12 changed files with 603 additions and 252 deletions

View File

@@ -1,210 +0,0 @@
import {
HTTP_REQUEST_NODE_NAME,
MANUAL_TRIGGER_NODE_NAME,
PIPEDRIVE_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
BACKEND_BASE_URL,
} from '../constants';
import { WorkflowPage, NDV } from '../pages';
import { errorToast } from '../pages/notifications';
import { getVisiblePopper } from '../utils';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('Data pinning', () => {
const maxPinnedDataSize = 16384;
beforeEach(() => {
workflowPage.actions.visit();
cy.window().then((win) => {
win.maxPinnedDataSize = maxPinnedDataSize;
});
});
it('Should be able to pin node output', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger', { keepNdvOpen: true });
ndv.getters.container().should('be.visible');
ndv.getters.pinDataButton().should('not.exist');
ndv.getters.editPinnedDataButton().should('be.visible');
ndv.actions.execute();
ndv.getters.outputDataContainer().should('be.visible');
// We hover over the table to get rid of the pinning tooltip which would overlay the table
// slightly and cause the test to fail
ndv.getters.outputDataContainer().get('table').realHover().should('be.visible');
ndv.getters.outputTableRows().should('have.length', 2);
ndv.getters.outputTableHeaders().should('have.length.at.least', 10);
ndv.getters.outputTableHeaders().first().should('include.text', 'timestamp');
ndv.getters.outputTableHeaders().eq(1).should('include.text', 'Readable date');
ndv.getters
.outputTbodyCell(1, 0)
.invoke('text')
.then((prevValue) => {
ndv.actions.pinData();
ndv.actions.close();
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Schedule Trigger');
ndv.getters.outputTbodyCell(1, 0).invoke('text').should('eq', prevValue);
});
});
it('Should be able to set pinned data', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger', { keepNdvOpen: true });
ndv.getters.container().should('be.visible');
ndv.getters.pinDataButton().should('not.exist');
ndv.getters.editPinnedDataButton().should('be.visible');
ndv.actions.setPinnedData([{ test: 1 }]);
ndv.getters.outputTableRows().should('have.length', 2);
ndv.getters.outputTableHeaders().should('have.length', 2);
ndv.getters.outputTableHeaders().first().should('include.text', 'test');
ndv.getters.outputTbodyCell(1, 0).should('include.text', 1);
ndv.actions.close();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.openNode('Schedule Trigger');
ndv.getters.outputTableHeaders().first().should('include.text', 'test');
ndv.getters.outputTbodyCell(1, 0).should('include.text', 1);
});
it('should display pin data edit button for Webhook node', () => {
workflowPage.actions.addInitialNodeToCanvas('Webhook', { keepNdvOpen: true });
ndv.getters
.runDataPaneHeader()
.find('button')
.filter(':visible')
.should('have.attr', 'title', 'Edit Output');
});
it('Should be duplicating pin data when duplicating node', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
ndv.getters.container().should('be.visible');
ndv.getters.pinDataButton().should('not.exist');
ndv.getters.editPinnedDataButton().should('be.visible');
ndv.actions.setPinnedData([{ test: 1 }]);
ndv.actions.close();
workflowPage.actions.duplicateNode(EDIT_FIELDS_SET_NODE_NAME);
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.openNode('Edit Fields1');
ndv.getters.outputTableHeaders().first().should('include.text', 'test');
ndv.getters.outputTbodyCell(1, 0).should('include.text', 1);
});
it('Should show an error when maximum pin data size is exceeded', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
ndv.getters.container().should('be.visible');
ndv.getters.pinDataButton().should('not.exist');
ndv.getters.editPinnedDataButton().should('be.visible');
ndv.actions.pastePinnedData([
{
test: '1'.repeat(maxPinnedDataSize),
},
]);
errorToast().should('contain', 'Workflow has reached the maximum allowed pinned data size');
});
it('Should show an error when pin data JSON in invalid', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
ndv.getters.container().should('be.visible');
ndv.getters.pinDataButton().should('not.exist');
ndv.getters.editPinnedDataButton().should('be.visible');
ndv.actions.setPinnedData('[ { "name": "First item", "code": 2dsa }]');
errorToast().should('contain', 'Unable to save due to invalid JSON');
});
it('Should be able to reference paired items in a node located before pinned data', () => {
workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true);
ndv.actions.setPinnedData([{ http: 123 }]);
ndv.actions.close();
workflowPage.actions.addNodeToCanvas(PIPEDRIVE_NODE_NAME, true, true);
ndv.actions.setPinnedData(Array(3).fill({ pipedrive: 123 }));
ndv.actions.close();
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
setExpressionOnStringValueInSet(`{{ $('${HTTP_REQUEST_NODE_NAME}').item`);
const output = '[Object: {"json": {"http": 123}, "pairedItem": {"item": 0}}]';
cy.get('div').contains(output).should('be.visible');
});
it('should use pin data in manual executions that are started by a webhook', () => {
cy.createFixtureWorkflow('Test_workflow_webhook_with_pin_data.json', 'Test');
workflowPage.actions.executeWorkflow();
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/b0d79ddb-df2d-49b1-8555-9fa2b482608f`).then(
(response) => {
expect(response.status).to.eq(200);
},
);
workflowPage.actions.openNode('End');
ndv.getters.outputTableRow(1).should('exist');
ndv.getters.outputTableRow(1).should('have.text', 'pin-overwritten');
});
it('should not use pin data in production executions that are started by a webhook', () => {
cy.createFixtureWorkflow('Test_workflow_webhook_with_pin_data.json', 'Test');
workflowPage.actions.activateWorkflow();
cy.request('GET', `${BACKEND_BASE_URL}/webhook/b0d79ddb-df2d-49b1-8555-9fa2b482608f`).then(
(response) => {
expect(response.status).to.eq(200);
// Assert that we get the data hard coded in the edit fields node,
// instead of the data pinned in said node.
expect(response.body).to.deep.equal({
nodeData: 'pin',
});
},
);
});
it('should not show pinned data tooltip', () => {
cy.createFixtureWorkflow('Pinned_webhook_node.json', 'Test');
workflowPage.actions.executeWorkflow();
// hide other visible popper on workflow execute button
workflowPage.getters.canvasNodes().eq(0).click();
getVisiblePopper().should('have.length', 0);
});
});
function setExpressionOnStringValueInSet(expression: string) {
cy.get('button').contains('Execute step').click();
ndv.getters.assignmentCollectionAdd('assignments').click();
ndv.getters.assignmentValue('assignments').contains('Expression').invoke('show').click();
ndv.getters
.inlineExpressionEditorInput()
.clear()
.type(expression, { parseSpecialCharSequences: false })
// hide autocomplete
.type('{esc}');
}

View File

@@ -37,6 +37,10 @@ export class CanvasPage extends BasePage {
await this.clickByTestId('canvas-plus-button'); await this.clickByTestId('canvas-plus-button');
} }
getCanvasNodes() {
return this.page.getByTestId('canvas-node');
}
async clickNodeCreatorPlusButton(): Promise<void> { async clickNodeCreatorPlusButton(): Promise<void> {
await this.clickByTestId('node-creator-plus-button'); await this.clickByTestId('node-creator-plus-button');
} }
@@ -81,6 +85,10 @@ export class CanvasPage extends BasePage {
await this.clickSaveWorkflowButton(); await this.clickSaveWorkflowButton();
} }
getExecuteWorkflowButton(): Locator {
return this.page.getByTestId('execute-workflow-button');
}
async clickExecuteWorkflowButton(): Promise<void> { async clickExecuteWorkflowButton(): Promise<void> {
await this.page.getByTestId('execute-workflow-button').click(); await this.page.getByTestId('execute-workflow-button').click();
} }
@@ -256,6 +264,11 @@ export class CanvasPage extends BasePage {
await this.getProductionChecklistActionItem(actionText).click(); await this.getProductionChecklistActionItem(actionText).click();
} }
async duplicateNode(nodeName: string): Promise<void> {
await this.nodeByName(nodeName).click({ button: 'right' });
await this.page.getByTestId('context-menu').getByText('Duplicate').click();
}
getCanvasNodes(): Locator { getCanvasNodes(): Locator {
return this.page.getByTestId('canvas-node'); return this.page.getByTestId('canvas-node');
} }

View File

@@ -44,10 +44,136 @@ export class NodeDisplayViewPage extends BasePage {
return this.page.getByTestId('output-panel'); return this.page.getByTestId('output-panel');
} }
getContainer() {
return this.page.getByTestId('ndv');
}
getParameterExpressionPreviewValue() { getParameterExpressionPreviewValue() {
return this.page.getByTestId('parameter-expression-preview-value'); return this.page.getByTestId('parameter-expression-preview-value');
} }
getEditPinnedDataButton() {
return this.page.getByTestId('ndv-edit-pinned-data');
}
getPinDataButton() {
return this.getOutputPanel().getByTestId('ndv-pin-data');
}
getRunDataPaneHeader() {
return this.page.getByTestId('run-data-pane-header');
}
getOutputTable() {
return this.getOutputPanel().getByTestId('ndv-data-container').locator('table');
}
getOutputDataContainer() {
return this.getOutputPanel().getByTestId('ndv-data-container');
}
getOutputTableRows() {
return this.getOutputTable().locator('tr');
}
getOutputTableHeaders() {
return this.getOutputTable().locator('thead th');
}
getOutputTableRow(row: number) {
return this.getOutputTableRows().nth(row);
}
getOutputTableCell(row: number, col: number) {
return this.getOutputTableRow(row).locator('td').nth(col);
}
getOutputTbodyCell(row: number, col: number) {
return this.getOutputTableRow(row).locator('td').nth(col);
}
// Pin data operations
async setPinnedData(data: object | string) {
const pinnedData = typeof data === 'string' ? data : JSON.stringify(data);
await this.getEditPinnedDataButton().click();
// Wait for editor to appear and use broader selector
const editor = this.getOutputPanel().locator('[contenteditable="true"]');
await editor.waitFor();
await editor.click();
await editor.fill(pinnedData);
await this.savePinnedData();
}
async pastePinnedData(data: object) {
await this.getEditPinnedDataButton().click();
const editor = this.getOutputPanel().locator('[contenteditable="true"]');
await editor.waitFor();
await editor.click();
await editor.fill('');
// Set clipboard data and paste
await this.page.evaluate(async (jsonData) => {
await navigator.clipboard.writeText(JSON.stringify(jsonData));
}, data);
await this.page.keyboard.press('ControlOrMeta+V');
await this.savePinnedData();
}
async savePinnedData() {
await this.getRunDataPaneHeader().locator('button:visible').filter({ hasText: 'Save' }).click();
}
// Assignment collection methods for advanced tests
getAssignmentCollectionAdd(paramName: string) {
return this.page
.getByTestId(`assignment-collection-${paramName}`)
.getByTestId('assignment-collection-drop-area');
}
getAssignmentValue(paramName: string) {
return this.page
.getByTestId(`assignment-collection-${paramName}`)
.getByTestId('assignment-value');
}
getInlineExpressionEditorInput() {
return this.page.getByTestId('inline-expression-editor-input');
}
getNodeParameters() {
return this.page.getByTestId('node-parameters');
}
getParameterInputHint() {
return this.page.getByTestId('parameter-input-hint');
}
async makeWebhookRequest(path: string) {
return await this.page.request.get(path);
}
getVisiblePoppers() {
return this.page.locator('.el-popper:visible');
}
async clearExpressionEditor() {
const editor = this.getInlineExpressionEditorInput();
await editor.click();
await this.page.keyboard.press('ControlOrMeta+A');
await this.page.keyboard.press('Delete');
}
async typeInExpressionEditor(text: string) {
const editor = this.getInlineExpressionEditorInput();
await editor.click();
// We have to use type() instead of fill() because the editor is a CodeMirror editor
await editor.type(text);
}
/** /**
* Get parameter input by name (for Code node and similar) * Get parameter input by name (for Code node and similar)
* @param parameterName - The name of the parameter e.g 'jsCode', 'mode' * @param parameterName - The name of the parameter e.g 'jsCode', 'mode'

View File

@@ -8,16 +8,52 @@ export class NotificationsPage {
} }
/** /**
* Gets the main container locator for a notification by its visible text. * Gets the main container locator for a notification by searching in its title text.
* @param text The text or a regular expression to find within the notification's title. * @param text The text or a regular expression to find within the notification's title.
* @returns A Locator for the notification container element. * @returns A Locator for the notification container element.
*/ */
notificationContainerByText(text: string | RegExp): Locator { getNotificationByTitle(text: string | RegExp): Locator {
return this.page.getByRole('alert').filter({ return this.page.getByRole('alert').filter({
has: this.page.locator('.el-notification__title').filter({ hasText: text }), has: this.page.locator('.el-notification__title').filter({ hasText: text }),
}); });
} }
/**
* Gets the main container locator for a notification by searching in its content/body text.
* This is useful for finding notifications where the detailed message is in the content
* rather than the title (e.g., error messages with detailed descriptions).
* @param text The text or a regular expression to find within the notification's content.
* @returns A Locator for the notification container element.
*/
getNotificationByContent(text: string | RegExp): Locator {
return this.page.getByRole('alert').filter({
has: this.page.locator('.el-notification__content').filter({ hasText: text }),
});
}
/**
* Gets the main container locator for a notification by searching in both title and content.
* This is the most flexible method as it will find notifications regardless of whether
* the text appears in the title or content section.
* @param text The text or a regular expression to find within the notification's title or content.
* @returns A Locator for the notification container element.
*/
getNotificationByTitleOrContent(text: string | RegExp): Locator {
return this.page.getByRole('alert').filter({ hasText: text });
}
/**
* Gets the main container locator for a notification by searching in both title and content,
* filtered to a specific node name. This is useful when multiple notifications might be present
* and you want to ensure you're checking the right one for a specific node.
* @param text The text or a regular expression to find within the notification's title or content.
* @param nodeName The name of the node to filter notifications for.
* @returns A Locator for the notification container element.
*/
getNotificationByTitleOrContentForNode(text: string | RegExp, nodeName: string): Locator {
return this.page.getByRole('alert').filter({ hasText: text }).filter({ hasText: nodeName });
}
/** /**
* Clicks the close button on the FIRST notification matching the text. * Clicks the close button on the FIRST notification matching the text.
* Fast execution with short timeouts for snappy notifications. * Fast execution with short timeouts for snappy notifications.
@@ -31,7 +67,7 @@ export class NotificationsPage {
const { timeout = 2000 } = options; const { timeout = 2000 } = options;
try { try {
const notification = this.notificationContainerByText(text).first(); const notification = this.getNotificationByTitle(text).first();
await notification.waitFor({ state: 'visible', timeout }); await notification.waitFor({ state: 'visible', timeout });
const closeBtn = notification.locator('.el-notification__closeBtn'); const closeBtn = notification.locator('.el-notification__closeBtn');
@@ -61,7 +97,7 @@ export class NotificationsPage {
while (retries < maxRetries) { while (retries < maxRetries) {
try { try {
const notifications = this.notificationContainerByText(text); const notifications = this.getNotificationByTitle(text);
const count = await notifications.count(); const count = await notifications.count();
if (count === 0) { if (count === 0) {
@@ -105,7 +141,7 @@ export class NotificationsPage {
const { timeout = 500 } = options; const { timeout = 500 } = options;
try { try {
const notification = this.notificationContainerByText(text).first(); const notification = this.getNotificationByTitle(text).first();
await notification.waitFor({ state: 'visible', timeout }); await notification.waitFor({ state: 'visible', timeout });
return true; return true;
} catch { } catch {
@@ -126,7 +162,7 @@ export class NotificationsPage {
const { timeout = 5000 } = options; const { timeout = 5000 } = options;
try { try {
const notification = this.notificationContainerByText(text).first(); const notification = this.getNotificationByTitle(text).first();
await notification.waitFor({ state: 'visible', timeout }); await notification.waitFor({ state: 'visible', timeout });
return true; return true;
} catch { } catch {
@@ -186,9 +222,7 @@ export class NotificationsPage {
*/ */
async getNotificationCount(text?: string | RegExp): Promise<number> { async getNotificationCount(text?: string | RegExp): Promise<number> {
try { try {
const notifications = text const notifications = text ? this.getNotificationByTitle(text) : this.page.getByRole('alert');
? this.notificationContainerByText(text)
: this.page.getByRole('alert');
return await notifications.count(); return await notifications.count();
} catch { } catch {
return 0; return 0;
@@ -202,7 +236,7 @@ export class NotificationsPage {
*/ */
async quickClose(text: string | RegExp): Promise<void> { async quickClose(text: string | RegExp): Promise<void> {
try { try {
const notification = this.notificationContainerByText(text).first(); const notification = this.getNotificationByTitle(text).first();
if (await notification.isVisible({ timeout: 100 })) { if (await notification.isVisible({ timeout: 100 })) {
await notification.locator('.el-notification__closeBtn').click({ timeout: 200 }); await notification.locator('.el-notification__closeBtn').click({ timeout: 200 });
} }

View File

@@ -29,9 +29,7 @@ test.describe('Workflows', () => {
await n8n.canvas.setWorkflowName(workflowName); await n8n.canvas.setWorkflowName(workflowName);
await n8n.canvas.clickSaveWorkflowButton(); await n8n.canvas.clickSaveWorkflowButton();
await expect( await expect(n8n.notifications.getNotificationByTitle(NOTIFICATIONS.CREATED)).toBeVisible();
n8n.notifications.notificationContainerByText(NOTIFICATIONS.CREATED),
).toBeVisible();
}); });
test('should search for workflows', async ({ n8n }) => { test('should search for workflows', async ({ n8n }) => {
@@ -72,18 +70,14 @@ test.describe('Workflows', () => {
const workflow = n8n.workflows.getWorkflowByName(workflowName); const workflow = n8n.workflows.getWorkflowByName(workflowName);
await n8n.workflows.archiveWorkflow(workflow); await n8n.workflows.archiveWorkflow(workflow);
await expect( await expect(n8n.notifications.getNotificationByTitle(NOTIFICATIONS.ARCHIVED)).toBeVisible();
n8n.notifications.notificationContainerByText(NOTIFICATIONS.ARCHIVED),
).toBeVisible();
await expect(workflow).toBeHidden(); await expect(workflow).toBeHidden();
await n8n.workflows.toggleShowArchived(); await n8n.workflows.toggleShowArchived();
await expect(workflow).toBeVisible(); await expect(workflow).toBeVisible();
await n8n.workflows.unarchiveWorkflow(workflow); await n8n.workflows.unarchiveWorkflow(workflow);
await expect( await expect(n8n.notifications.getNotificationByTitle(NOTIFICATIONS.UNARCHIVED)).toBeVisible();
n8n.notifications.notificationContainerByText(NOTIFICATIONS.UNARCHIVED),
).toBeVisible();
}); });
test('should delete an archived workflow', async ({ n8n }) => { test('should delete an archived workflow', async ({ n8n }) => {
@@ -95,16 +89,12 @@ test.describe('Workflows', () => {
const workflow = n8n.workflows.getWorkflowByName(workflowName); const workflow = n8n.workflows.getWorkflowByName(workflowName);
await n8n.workflows.archiveWorkflow(workflow); await n8n.workflows.archiveWorkflow(workflow);
await expect( await expect(n8n.notifications.getNotificationByTitle(NOTIFICATIONS.ARCHIVED)).toBeVisible();
n8n.notifications.notificationContainerByText(NOTIFICATIONS.ARCHIVED),
).toBeVisible();
await n8n.workflows.toggleShowArchived(); await n8n.workflows.toggleShowArchived();
await n8n.workflows.deleteWorkflow(workflow); await n8n.workflows.deleteWorkflow(workflow);
await expect( await expect(n8n.notifications.getNotificationByTitle(NOTIFICATIONS.DELETED)).toBeVisible();
n8n.notifications.notificationContainerByText(NOTIFICATIONS.DELETED),
).toBeVisible();
await expect(workflow).toBeHidden(); await expect(workflow).toBeHidden();
}); });

View File

@@ -109,7 +109,7 @@ test.describe('Canvas Actions', () => {
await n8n.canvas.executeNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME); await n8n.canvas.executeNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await expect( await expect(
n8n.notifications.notificationContainerByText('Node executed successfully'), n8n.notifications.getNotificationByTitle('Node executed successfully'),
).toHaveCount(1); ).toHaveCount(1);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1); await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
}); });

View File

@@ -0,0 +1,234 @@
import { test, expect } from '../../fixtures/base';
import type { TestRequirements } from '../../Types';
const NODES = {
MANUAL_TRIGGER: 'Manual Trigger',
SCHEDULE_TRIGGER: 'Schedule Trigger',
WEBHOOK: 'Webhook',
HTTP_REQUEST: 'HTTP Request',
PIPEDRIVE: 'Pipedrive',
EDIT_FIELDS: 'Edit Fields (Set)', // Use the full node name that appears in the Node List, although when it's added to the canvas it's called "Edit Fields"
CODE: 'Code',
END: 'End',
};
const webhookTestRequirements: TestRequirements = {
workflow: {
'Test_workflow_webhook_with_pin_data.json': 'Test',
},
};
const pinnedWebhookRequirements: TestRequirements = {
workflow: {
'Pinned_webhook_node.json': 'Test',
},
};
test.describe('Data pinning', () => {
const maxPinnedDataSize = 16384;
test.beforeEach(async ({ n8n }) => {
await n8n.goHome();
});
test.describe('Pin data operations', () => {
test('should be able to pin node output', async ({ n8n }) => {
await n8n.workflows.clickAddWorkflowButton();
await n8n.canvas.addNode(NODES.SCHEDULE_TRIGGER);
await n8n.ndv.execute();
await expect(n8n.ndv.getOutputPanel()).toBeVisible();
const prevValue = await n8n.ndv.getOutputTbodyCell(1, 0).textContent();
await n8n.ndv.togglePinData();
await n8n.ndv.close();
// Execute workflow and verify pinned data persists
await n8n.canvas.clickExecuteWorkflowButton();
await n8n.canvas.openNode(NODES.SCHEDULE_TRIGGER);
await expect(n8n.ndv.getOutputTbodyCell(1, 0)).toHaveText(prevValue ?? '');
});
test('should be able to set custom pinned data', async ({ n8n }) => {
await n8n.workflows.clickAddWorkflowButton();
await n8n.canvas.addNode(NODES.SCHEDULE_TRIGGER);
await expect(n8n.ndv.getEditPinnedDataButton()).toBeVisible();
await expect(n8n.ndv.getPinDataButton()).toBeHidden();
await n8n.ndv.setPinnedData([{ test: 1 }]);
await expect(n8n.ndv.getOutputTableRows()).toHaveCount(2);
await expect(n8n.ndv.getOutputTableHeaders()).toHaveCount(2);
await expect(n8n.ndv.getOutputTableHeaders().first()).toContainText('test');
await expect(n8n.ndv.getOutputTbodyCell(1, 0)).toContainText('1');
await n8n.ndv.close();
await n8n.canvas.clickSaveWorkflowButton();
await n8n.canvas.openNode(NODES.SCHEDULE_TRIGGER);
await expect(n8n.ndv.getOutputTableHeaders().first()).toContainText('test');
await expect(n8n.ndv.getOutputTbodyCell(1, 0)).toContainText('1');
});
test('should display pin data edit button for Webhook node', async ({ n8n }) => {
await n8n.workflows.clickAddWorkflowButton();
await n8n.canvas.addNode(NODES.WEBHOOK);
const runDataHeader = n8n.ndv.getRunDataPaneHeader();
const editButton = runDataHeader.getByRole('button', { name: 'Edit Output' });
await expect(editButton).toBeVisible();
});
test('should duplicate pinned data when duplicating node', async ({ n8n }) => {
await n8n.workflows.clickAddWorkflowButton();
await n8n.canvas.addNode(NODES.SCHEDULE_TRIGGER);
await n8n.ndv.close();
await n8n.canvas.addNode(NODES.EDIT_FIELDS);
await expect(n8n.ndv.getContainer()).toBeVisible();
await expect(n8n.ndv.getEditPinnedDataButton()).toBeVisible();
await expect(n8n.ndv.getPinDataButton()).toBeHidden();
await n8n.ndv.setPinnedData([{ test: 1 }]);
await n8n.ndv.close();
await n8n.canvas.duplicateNode('Edit Fields');
await n8n.canvas.clickSaveWorkflowButton();
await n8n.canvas.openNode('Edit Fields1');
await expect(n8n.ndv.getOutputTableHeaders().first()).toContainText('test');
await expect(n8n.ndv.getOutputTbodyCell(1, 0)).toContainText('1');
});
});
test.describe('Error handling', () => {
test('should show error when maximum pin data size is exceeded', async ({ n8n }) => {
await n8n.page.evaluate((maxSize) => {
(window as { maxPinnedDataSize?: number }).maxPinnedDataSize = maxSize;
}, maxPinnedDataSize);
const actualMaxSize = await n8n.page.evaluate(() => {
return (window as { maxPinnedDataSize?: number }).maxPinnedDataSize;
});
expect(actualMaxSize).toBe(maxPinnedDataSize);
await n8n.workflows.clickAddWorkflowButton();
await n8n.canvas.addNode(NODES.SCHEDULE_TRIGGER);
await n8n.ndv.close();
await n8n.canvas.addNode(NODES.EDIT_FIELDS);
await expect(n8n.ndv.getContainer()).toBeVisible();
await expect(n8n.ndv.getEditPinnedDataButton()).toBeVisible();
await expect(n8n.ndv.getPinDataButton()).toBeHidden();
const largeData = [{ test: '1'.repeat(maxPinnedDataSize + 1000) }];
await n8n.ndv.setPinnedData(largeData);
await expect(
n8n.notifications.getNotificationByContent(
'Workflow has reached the maximum allowed pinned data size',
),
).toBeVisible();
});
test('should show error when pin data JSON is invalid', async ({ n8n }) => {
await n8n.workflows.clickAddWorkflowButton();
await n8n.canvas.addNode(NODES.SCHEDULE_TRIGGER);
await n8n.ndv.close();
await n8n.canvas.addNode(NODES.EDIT_FIELDS);
await expect(n8n.ndv.getContainer()).toBeVisible();
await expect(n8n.ndv.getEditPinnedDataButton()).toBeVisible();
await expect(n8n.ndv.getPinDataButton()).toBeHidden();
await n8n.ndv.setPinnedData('[ { "name": "First item", "code": 2dsa }]');
await expect(
n8n.notifications.getNotificationByTitle('Unable to save due to invalid JSON'),
).toBeVisible();
});
});
test.describe('Advanced pinning scenarios', () => {
test('should be able to reference paired items in node before pinned data', async ({ n8n }) => {
await n8n.workflows.clickAddWorkflowButton();
await n8n.canvas.addNode(NODES.MANUAL_TRIGGER);
await n8n.canvas.addNode(NODES.HTTP_REQUEST);
await n8n.ndv.setPinnedData([{ http: 123 }]);
await n8n.ndv.close();
await n8n.canvas.addNodeWithSubItem(NODES.PIPEDRIVE, 'Create an activity');
await n8n.ndv.setPinnedData(Array(3).fill({ pipedrive: 123 }));
await n8n.ndv.close();
await n8n.canvas.addNode(NODES.EDIT_FIELDS);
await n8n.ndv.execute();
await expect(n8n.ndv.getNodeParameters()).toBeVisible();
await expect(n8n.ndv.getAssignmentCollectionAdd('assignments')).toBeVisible();
await n8n.ndv.getAssignmentCollectionAdd('assignments').click();
await n8n.ndv.getAssignmentValue('assignments').getByText('Expression').click();
const expressionInput = n8n.ndv.getInlineExpressionEditorInput();
await expressionInput.click();
await n8n.ndv.clearExpressionEditor();
await n8n.ndv.typeInExpressionEditor(`{{ $('${NODES.HTTP_REQUEST}').item`);
await n8n.page.keyboard.press('Escape');
const expectedOutput = '[Object: {"json": {"http": 123}, "pairedItem": {"item": 0}}]';
await expect(n8n.ndv.getParameterInputHint().getByText(expectedOutput)).toBeVisible();
});
test('should use pin data in manual webhook executions', async ({ n8n, setupRequirements }) => {
await setupRequirements(webhookTestRequirements);
await n8n.canvas.clickExecuteWorkflowButton();
await expect(n8n.canvas.getExecuteWorkflowButton()).toHaveText(
'Waiting for trigger event from Webhook',
);
const webhookPath = '/webhook-test/b0d79ddb-df2d-49b1-8555-9fa2b482608f';
const response = await n8n.ndv.makeWebhookRequest(webhookPath);
expect(response.status()).toBe(200);
await n8n.canvas.openNode(NODES.END);
await expect(n8n.ndv.getOutputTableRow(1)).toBeVisible();
await expect(n8n.ndv.getOutputTableRow(1)).toContainText('pin-overwritten');
});
test('should not use pin data in production webhook executions', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(webhookTestRequirements);
await n8n.canvas.activateWorkflow();
const webhookUrl = '/webhook/b0d79ddb-df2d-49b1-8555-9fa2b482608f';
const response = await n8n.ndv.makeWebhookRequest(webhookUrl);
expect(response.status()).toBe(200);
const responseBody = await response.json();
expect(responseBody).toEqual({ nodeData: 'pin' });
});
test('should not show pinned data tooltip', async ({ n8n, setupRequirements }) => {
await setupRequirements(pinnedWebhookRequirements);
await n8n.canvas.clickExecuteWorkflowButton();
await n8n.canvas.getCanvasNodes().first().click();
const poppers = n8n.ndv.getVisiblePoppers();
await expect(poppers).toHaveCount(0);
});
});
});

View File

@@ -36,7 +36,7 @@ test.describe('Code node', () => {
await n8n.ndv.execute(); await n8n.ndv.execute();
await expect( await expect(
n8n.notifications.notificationContainerByText('Node executed successfully').first(), n8n.notifications.getNotificationByTitle('Node executed successfully').first(),
).toBeVisible(); ).toBeVisible();
await n8n.ndv.getParameterInput('mode').click(); await n8n.ndv.getParameterInput('mode').click();
@@ -45,7 +45,7 @@ test.describe('Code node', () => {
await n8n.ndv.execute(); await n8n.ndv.execute();
await expect( await expect(
n8n.notifications.notificationContainerByText('Node executed successfully').first(), n8n.notifications.getNotificationByTitle('Node executed successfully').first(),
).toBeVisible(); ).toBeVisible();
}); });

View File

@@ -7,7 +7,7 @@ test.describe('PDF Test', () => {
await n8n.canvas.importWorkflow('test_pdf_workflow.json', 'PDF Workflow'); await n8n.canvas.importWorkflow('test_pdf_workflow.json', 'PDF Workflow');
await n8n.canvas.clickExecuteWorkflowButton(); await n8n.canvas.clickExecuteWorkflowButton();
await expect( await expect(
n8n.notifications.notificationContainerByText('Workflow executed successfully'), n8n.notifications.getNotificationByTitle('Workflow executed successfully'),
).toBeVisible(); ).toBeVisible();
}); });
}); });

View File

@@ -129,9 +129,7 @@ test.describe('Security Notifications', () => {
await n8n.goHome(); await n8n.goHome();
// Verify security notification appears with default message // Verify security notification appears with default message
const notification = n8n.notifications.notificationContainerByText( const notification = n8n.notifications.getNotificationByTitle('Critical update available');
'Critical update available',
);
await expect(notification).toBeVisible(); await expect(notification).toBeVisible();
await expect(notification).toContainText('Please update to latest version.'); await expect(notification).toContainText('Please update to latest version.');
await expect(notification).toContainText('More info'); await expect(notification).toContainText('More info');
@@ -153,7 +151,7 @@ test.describe('Security Notifications', () => {
await n8n.goHome(); await n8n.goHome();
// Verify notification shows specific fix version (dynamically generated) // Verify notification shows specific fix version (dynamically generated)
const notificationWithFixVersion = n8n.notifications.notificationContainerByText( const notificationWithFixVersion = n8n.notifications.getNotificationByTitle(
'Critical update available', 'Critical update available',
); );
await expect(notificationWithFixVersion).toBeVisible(); await expect(notificationWithFixVersion).toBeVisible();
@@ -174,9 +172,7 @@ test.describe('Security Notifications', () => {
await n8n.goHome(); await n8n.goHome();
// Wait for and click the security notification // Wait for and click the security notification
const notification = n8n.notifications.notificationContainerByText( const notification = n8n.notifications.getNotificationByTitle('Critical update available');
'Critical update available',
);
await expect(notification).toBeVisible(); await expect(notification).toBeVisible();
await notification.click(); await notification.click();
@@ -199,9 +195,7 @@ test.describe('Security Notifications', () => {
await n8n.goHome(); await n8n.goHome();
// Verify no security notification appears when no security issue // Verify no security notification appears when no security issue
const notification = n8n.notifications.notificationContainerByText( const notification = n8n.notifications.getNotificationByTitle('Critical update available');
'Critical update available',
);
await expect(notification).toBeHidden(); await expect(notification).toBeHidden();
}); });
@@ -212,9 +206,7 @@ test.describe('Security Notifications', () => {
await n8n.goHome(); await n8n.goHome();
// Verify no security notification appears on API failure // Verify no security notification appears on API failure
const notification = n8n.notifications.notificationContainerByText( const notification = n8n.notifications.getNotificationByTitle('Critical update available');
'Critical update available',
);
await expect(notification).toBeHidden(); await expect(notification).toBeHidden();
// Verify the app still functions normally // Verify the app still functions normally

View File

@@ -0,0 +1,36 @@
{
"nodes": [
{
"parameters": {
"path": "FwrbSiaua2Xmvn6-Z-7CQ",
"options": {}
},
"id": "8fcc7e5f-2cef-4938-9564-eea504c20aa0",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [360, 220],
"webhookId": "9c778f2a-e882-46ed-a0e4-c8e2f76ccd65"
}
],
"connections": {},
"pinData": {
"Webhook": [
{
"headers": {
"connection": "keep-alive",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"accept": "*/*",
"cookie": "n8n-auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjNiM2FhOTE5LWRhZDgtNDE5MS1hZWZiLTlhZDIwZTZkMjJjNiIsImhhc2giOiJ1ZVAxR1F3U2paIiwiaWF0IjoxNzI4OTE1NTQyLCJleHAiOjE3Mjk1MjAzNDJ9.fV02gpUnSiUoMxHwfB0npBjcjct7Mv9vGfj-jRTT3-I",
"host": "localhost:5678",
"accept-encoding": "gzip, deflate"
},
"params": {},
"query": {},
"body": {},
"webhookUrl": "http://localhost:5678/webhook-test/FwrbSiaua2Xmvn6-Z-7CQ",
"executionMode": "test"
}
]
}
}

View File

@@ -0,0 +1,136 @@
{
"name": "PinData Test",
"nodes": [
{
"parameters": {},
"id": "0a60e507-7f34-41c0-a0f9-697d852033b6",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [780, 320]
},
{
"parameters": {
"path": "b0d79ddb-df2d-49b1-8555-9fa2b482608f",
"responseMode": "lastNode",
"options": {}
},
"id": "66425ce3-450d-4aa6-a53b-a701ab89c2de",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [780, 540],
"webhookId": "b0d79ddb-df2d-49b1-8555-9fa2b482608f"
},
{
"parameters": {
"fields": {
"values": [
{
"name": "nodeData",
"stringValue": "init"
}
]
},
"include": "none",
"options": {}
},
"id": "3211b3c5-49e9-4694-8f86-7a5783bc653a",
"name": "Init Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [1000, 320]
},
{
"parameters": {
"fields": {
"values": [
{
"name": "nodeData",
"stringValue": "pin"
}
]
},
"options": {}
},
"id": "97b31120-4720-4632-9d35-356f345119f7",
"name": "Pin Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [1240, 320]
},
{
"parameters": {},
"id": "1ee7be4f-7006-43bf-bb0c-29db3058a399",
"name": "End",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1460, 320]
}
],
"pinData": {
"Pin Data": [
{
"json": {
"nodeData": "pin-overwritten"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Init Data",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Init Data",
"type": "main",
"index": 0
}
]
]
},
"Init Data": {
"main": [
[
{
"node": "Pin Data",
"type": "main",
"index": 0
}
]
]
},
"Pin Data": {
"main": [
[
{
"node": "End",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "ded8577a-3ed2-4611-842c-a7922ec58b98",
"id": "weofVLZo0ssmPDrV",
"meta": {
"instanceId": "021d3c82ba2d3bc090cbf4fc81c9312668bcc34297e022bb3438c5c88a43a5ff"
},
"tags": []
}