From 7ded694ce76e929db5c7a210790515dda146533e Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Mon, 15 Sep 2025 13:59:58 +0200 Subject: [PATCH] test: Migrate form trigger test from cypress -> playwright (#19453) --- cypress/e2e/16-form-trigger-node.cy.ts | 101 ------------------ .../testing/playwright/pages/CanvasPage.ts | 4 + .../playwright/pages/NodeDetailsViewPage.ts | 40 ++++--- .../tests/ui/16-form-trigger-node.spec.ts | 60 +++++++++++ .../testing/playwright/utils/index-helper.ts | 10 ++ 5 files changed, 99 insertions(+), 116 deletions(-) delete mode 100644 cypress/e2e/16-form-trigger-node.cy.ts create mode 100644 packages/testing/playwright/tests/ui/16-form-trigger-node.spec.ts create mode 100644 packages/testing/playwright/utils/index-helper.ts diff --git a/cypress/e2e/16-form-trigger-node.cy.ts b/cypress/e2e/16-form-trigger-node.cy.ts deleted file mode 100644 index 1e57a0bd5a..0000000000 --- a/cypress/e2e/16-form-trigger-node.cy.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { WorkflowPage, NDV } from '../pages'; -import { getVisibleSelect } from '../utils'; - -const workflowPage = new WorkflowPage(); -const ndv = new NDV(); - -describe('n8n Form Trigger', () => { - beforeEach(() => { - workflowPage.actions.visit(); - }); - - it("add node by clicking on 'On form submission'", () => { - workflowPage.getters.canvasPlusButton().click(); - workflowPage.getters.nodeCreatorNodeItems().contains('On form submission').click(); - ndv.getters.parameterInput('formTitle').type('Test Form'); - ndv.getters.parameterInput('formDescription').type('Test Form Description'); - ndv.getters.backToCanvas().click(); - workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist'); - }); - - it('should fill up form fields', () => { - workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger', { - isTrigger: true, - action: 'On new n8n Form event', - }); - ndv.getters.parameterInput('formTitle').type('Test Form'); - ndv.getters.parameterInput('formDescription').type('Test Form Description'); - cy.get('[data-test-id="fixed-collection-add"]').click(); - ndv.getters.parameterInput('fieldLabel').type('Test Field 1'); - ndv.getters.parameterInput('fieldType').click(); - //fill up first field of type number - getVisibleSelect().contains('Number').click(); - cy.get( - '[data-test-id="parameter-input-requiredField"] > .parameter-input > .el-switch > .el-switch__core', - ).click(); - //fill up second field of type text - cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click(); - cy.get('.border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item') - .find('input[placeholder*="e.g. What is your name?"]') - .type('Test Field 2'); - //fill up second field of type date - cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click(); - cy.get( - ':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item', - ) - .find('input[placeholder*="e.g. What is your name?"]') - .type('Test Field 3'); - cy.get( - ':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item', - ).click(); - getVisibleSelect().contains('Date').click(); - // fill up second field of type dropdown - cy.get('.fixed-collection-parameter > :nth-child(2) > .button').click(); - cy.get( - ':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item', - ) - .find('input[placeholder*="e.g. What is your name?"]') - .type('Test Field 4'); - cy.get( - ':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item', - ).click(); - getVisibleSelect().contains('Dropdown').click(); - cy.contains('button', 'Add Field Option').click(); - cy.contains('label', 'Field Options') - .parent() - .parent() - .nextAll() - .find('[data-test-id="parameter-input-field"]') - .eq(0) - .type('Option 1'); - cy.contains('label', 'Field Options') - .parent() - .parent() - .nextAll() - .find('[data-test-id="parameter-input-field"]') - .eq(1) - .type('Option 2'); - - //add optional submitted message - cy.get('.param-options').click(); - // wait for selector debounce - getVisibleSelect().find('span').contains('Form Response').click().wait(300); - cy.contains('span', 'Text to Show') - .should('exist') - .parent() - .parent() - .next() - .children() - .children() - .children() - .children() - .children() - .children() - .children() - .first() - .type('Your test form was successfully submitted'); - - ndv.getters.backToCanvas().click(); - workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist'); - }); -}); diff --git a/packages/testing/playwright/pages/CanvasPage.ts b/packages/testing/playwright/pages/CanvasPage.ts index 4cabf62365..adad4d30fa 100644 --- a/packages/testing/playwright/pages/CanvasPage.ts +++ b/packages/testing/playwright/pages/CanvasPage.ts @@ -44,6 +44,10 @@ export class CanvasPage extends BasePage { return this.page.locator(`[data-test-id="canvas-node"][data-node-name="${nodeName}"]`); } + nodeIssuesBadge(nodeName: string) { + return this.nodeByName(nodeName).getByTestId('node-issues'); + } + nodeToolbar(nodeName: string): Locator { return this.nodeByName(nodeName).getByTestId('canvas-node-toolbar'); } diff --git a/packages/testing/playwright/pages/NodeDetailsViewPage.ts b/packages/testing/playwright/pages/NodeDetailsViewPage.ts index 7cd6682472..67ee5f7213 100644 --- a/packages/testing/playwright/pages/NodeDetailsViewPage.ts +++ b/packages/testing/playwright/pages/NodeDetailsViewPage.ts @@ -5,6 +5,7 @@ import { BasePage } from './BasePage'; import { RunDataPanel } from './components/RunDataPanel'; import { NodeParameterHelper } from '../helpers/NodeParameterHelper'; import { EditFieldsNode } from './nodes/EditFieldsNode'; +import { locatorByIndex } from '../utils/index-helper'; export class NodeDetailsViewPage extends BasePage { readonly setupHelper: NodeParameterHelper; @@ -46,8 +47,10 @@ export class NodeDetailsViewPage extends BasePage { return this.getContainer().locator('.parameter-item').filter({ hasText: labelName }); } - async fillParameterInput(labelName: string, value: string) { - await this.getParameterByLabel(labelName).getByTestId('parameter-input-field').fill(value); + async fillParameterInput(labelName: string, value: string, index?: number) { + await locatorByIndex(this.getParameterByLabel(labelName), index) + .getByTestId('parameter-input-field') + .fill(value); } async selectWorkflowResource(createItemText: string, searchText: string = '') { @@ -68,6 +71,10 @@ export class NodeDetailsViewPage extends BasePage { await this.clickBackToCanvasButton(); } + async addFixedCollectionItem() { + await this.clickByTestId('fixed-collection-add'); + } + async execute() { await this.clickByTestId('node-execute-button'); } @@ -211,31 +218,29 @@ export class NodeDetailsViewPage extends BasePage { await editor.type(text); } - getParameterInput(parameterName: string) { - return this.page.getByTestId(`parameter-input-${parameterName}`); + getParameterInput(parameterName: string, index?: number) { + return locatorByIndex(this.page.getByTestId(`parameter-input-${parameterName}`), index); } - getParameterInputField(parameterName: string) { - return this.getParameterInput(parameterName).locator('input'); + getParameterInputField(parameterName: string, index?: number) { + return this.getParameterInput(parameterName, index).locator('input'); } - async selectOptionInParameterDropdown(parameterName: string, optionText: string) { - const dropdown = this.getParameterInput(parameterName); - await dropdown.click(); - - await this.page.getByRole('option', { name: optionText }).click(); + async selectOptionInParameterDropdown(parameterName: string, optionText: string, index = 0) { + await this.clickParameterDropdown(parameterName, index); + await this.selectFromVisibleDropdown(optionText); } - async clickParameterDropdown(parameterName: string): Promise { - await this.clickByTestId(`parameter-input-${parameterName}`); + async clickParameterDropdown(parameterName: string, index = 0): Promise { + await locatorByIndex(this.page.getByTestId(`parameter-input-${parameterName}`), index).click(); } async selectFromVisibleDropdown(optionText: string): Promise { await this.page.getByRole('option', { name: optionText }).click(); } - async fillParameterInputByName(parameterName: string, value: string): Promise { - const input = this.getParameterInputField(parameterName); + async fillParameterInputByName(parameterName: string, value: string, index = 0): Promise { + const input = this.getParameterInputField(parameterName, index); await input.click(); await input.fill(value); } @@ -244,6 +249,11 @@ export class NodeDetailsViewPage extends BasePage { await this.page.locator('.param-options').click(); } + async addParameterOptionByName(optionName: string): Promise { + await this.clickParameterOptions(); + await this.selectFromVisibleDropdown(optionName); + } + getVisiblePopper() { return this.page.locator('.el-popper:visible'); } diff --git a/packages/testing/playwright/tests/ui/16-form-trigger-node.spec.ts b/packages/testing/playwright/tests/ui/16-form-trigger-node.spec.ts new file mode 100644 index 0000000000..c1092bc576 --- /dev/null +++ b/packages/testing/playwright/tests/ui/16-form-trigger-node.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '../../fixtures/base'; + +test.describe('Form Trigger', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.start.fromBlankCanvas(); + }); + + test("add node by clicking on 'On form submission'", async ({ n8n }) => { + await n8n.canvas.clickNodeCreatorPlusButton(); + await n8n.canvas.nodeCreatorItemByName('On form submission').click(); + + await n8n.ndv.fillParameterInput('Form Title', 'Test Form'); + await n8n.ndv.fillParameterInput('Form Description', 'Test Form Description'); + await n8n.ndv.clickBackToCanvasButton(); + + await expect(n8n.canvas.nodeByName('On form submission')).toBeVisible(); + await expect(n8n.canvas.nodeIssuesBadge('On form submission')).toBeHidden(); + }); + + test('should fill up form fields', async ({ n8n }) => { + await n8n.canvas.clickNodeCreatorPlusButton(); + await n8n.canvas.nodeCreatorItemByName('On form submission').click(); + + await n8n.ndv.fillParameterInput('Form Title', 'Test Form'); + await n8n.ndv.fillParameterInput('Form Description', 'Test Form Description'); + + // Add first field - Number type with required flag + await n8n.ndv.addFixedCollectionItem(); + await n8n.ndv.fillParameterInputByName('fieldLabel', 'Test Field 1'); + await n8n.ndv.selectOptionInParameterDropdown('fieldType', 'Number'); + await n8n.ndv.setParameterSwitch('requiredField', true); + + // Add second field - Text type + await n8n.ndv.addFixedCollectionItem(); + await n8n.ndv.fillParameterInputByName('fieldLabel', 'Test Field 2', 1); + + // Add third field - Date type + await n8n.ndv.addFixedCollectionItem(); + await n8n.ndv.fillParameterInputByName('fieldLabel', 'Test Field 3', 2); + await n8n.ndv.selectOptionInParameterDropdown('fieldType', 'Date', 2); + + // Add fourth field - Dropdown type with options + await n8n.ndv.addFixedCollectionItem(); + await n8n.ndv.fillParameterInputByName('fieldLabel', 'Test Field 4', 3); + await n8n.ndv.selectOptionInParameterDropdown('fieldType', 'Dropdown', 3); + + // Configure dropdown field options + await n8n.page.getByRole('button', { name: 'Add Field Option' }).click(); + await n8n.ndv.fillParameterInputByName('option', 'Option 1'); + await n8n.ndv.fillParameterInputByName('option', 'Option 2', 1); + + // Add optional submitted message + await n8n.ndv.addParameterOptionByName('Form Response'); + await n8n.ndv.fillParameterInput('Text to Show', 'Your test form was successfully submitted'); + + await n8n.ndv.clickBackToCanvasButton(); + await expect(n8n.canvas.nodeByName('On form submission')).toBeVisible(); + await expect(n8n.canvas.nodeIssuesBadge('On form submission')).toBeHidden(); + }); +}); diff --git a/packages/testing/playwright/utils/index-helper.ts b/packages/testing/playwright/utils/index-helper.ts new file mode 100644 index 0000000000..c6a2690435 --- /dev/null +++ b/packages/testing/playwright/utils/index-helper.ts @@ -0,0 +1,10 @@ +import type { Locator } from '@playwright/test'; + +/** + * Returns a locator for a specific element by index, or the original locator if no index is provided. + * Without an index, Playwright throws an error when multiple matching elements are found. + * @see https://playwright.dev/docs/locators#strictness + */ +export function locatorByIndex(locator: Locator, index?: number) { + return typeof index === 'number' ? locator.nth(index) : locator; +}