From a5c18c3c2dfd50d37001bbc37171564bde0a0453 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Mon, 15 Sep 2025 11:43:09 +0200 Subject: [PATCH] test: Migrate expression editor modal tests from cypress -> playwright (#19535) --- cypress/e2e/9-expression-editor-modal.cy.ts | 146 ------------------ .../playwright/pages/NodeDetailsViewPage.ts | 25 +++ .../ui/9-expression-editor-modal.spec.ts | 117 ++++++++++++++ 3 files changed, 142 insertions(+), 146 deletions(-) delete mode 100644 cypress/e2e/9-expression-editor-modal.cy.ts create mode 100644 packages/testing/playwright/tests/ui/9-expression-editor-modal.spec.ts diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts deleted file mode 100644 index 5cd9b7e2e2..0000000000 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { META_KEY } from '../constants'; -import { NDV } from '../pages/ndv'; -import { successToast } from '../pages/notifications'; -import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; - -const WorkflowPage = new WorkflowPageClass(); -const ndv = new NDV(); - -describe('Expression editor modal', () => { - beforeEach(() => { - WorkflowPage.actions.visit(); - WorkflowPage.actions.addInitialNodeToCanvas('Schedule'); - cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError'); - }); - - describe('Keybinds', () => { - beforeEach(() => { - WorkflowPage.actions.addNodeToCanvas('Hacker News'); - WorkflowPage.actions.zoomToFit(); - WorkflowPage.actions.openNode('Get many items'); - WorkflowPage.actions.openExpressionEditorModal(); - }); - - it('should save the workflow with save keybind', () => { - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().click().type('{{ "hello"'); - WorkflowPage.getters.expressionModalOutput().contains('hello'); - WorkflowPage.getters.expressionModalInput().click().type(`{${META_KEY}+s}`); - successToast().should('be.visible'); - }); - }); - - describe('Static data', () => { - beforeEach(() => { - WorkflowPage.actions.addNodeToCanvas('Hacker News'); - WorkflowPage.actions.zoomToFit(); - WorkflowPage.actions.openNode('Get many items'); - WorkflowPage.actions.openExpressionEditorModal(); - }); - - it('should resolve primitive resolvables', () => { - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().click().type('{{ 1 + 2'); - WorkflowPage.getters.expressionModalOutput().contains(/^3$/); - WorkflowPage.getters.expressionModalInput().clear(); - - WorkflowPage.getters.expressionModalInput().click().type('{{ "ab" + "cd"'); - WorkflowPage.getters.expressionModalOutput().contains(/^abcd$/); - - WorkflowPage.getters.expressionModalInput().clear(); - - WorkflowPage.getters.expressionModalInput().click().type('{{ true && false'); - WorkflowPage.getters.expressionModalOutput().contains(/^false$/); - }); - - it('should resolve object resolvables', () => { - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters - .expressionModalInput() - .click() - .type('{{ { a : 1 }', { parseSpecialCharSequences: false }); - WorkflowPage.getters.expressionModalOutput().contains(/^\[Object: \{"a": 1\}\]$/); - - WorkflowPage.getters.expressionModalInput().clear(); - - WorkflowPage.getters - .expressionModalInput() - .click() - .type('{{ { a : 1 }.a', { parseSpecialCharSequences: false }); - WorkflowPage.getters.expressionModalOutput().contains(/^1$/); - }); - - it('should resolve array resolvables', () => { - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().click().type('{{ [1, 2, 3]'); - WorkflowPage.getters.expressionModalOutput().contains(/^\[Array: \[1,2,3\]\]$/); - - WorkflowPage.getters.expressionModalInput().clear(); - - WorkflowPage.getters.expressionModalInput().click().type('{{ [1, 2, 3][0]'); - WorkflowPage.getters.expressionModalOutput().contains(/^1$/); - }); - }); - - describe('Dynamic data', () => { - beforeEach(() => { - WorkflowPage.actions.openNode('Schedule Trigger'); - ndv.actions.setPinnedData([{ myStr: 'Monday' }]); - ndv.actions.close(); - WorkflowPage.actions.addNodeToCanvas('No Operation'); - WorkflowPage.actions.addNodeToCanvas('Hacker News'); - WorkflowPage.actions.zoomToFit(); - WorkflowPage.actions.openNode('Get many items'); - WorkflowPage.actions.openExpressionEditorModal(); - }); - - it('should resolve $parameter[]', () => { - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().click().type('{{ $parameter["operation"]'); - WorkflowPage.getters.expressionModalOutput().should('have.text', 'getAll'); - }); - - it('should resolve input: $json,$input,$(nodeName)', () => { - // Previous nodes have not run, input is empty - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().click().type('{{ $json.myStr'); - WorkflowPage.getters - .expressionModalOutput() - .should('have.text', '[Execute previous nodes for preview]'); - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().click().type('{{ $input.item.json.myStr'); - WorkflowPage.getters - .expressionModalOutput() - .should('have.text', '[Execute previous nodes for preview]'); - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters - .expressionModalInput() - .click() - .type("{{ $('Schedule Trigger').item.json.myStr"); - WorkflowPage.getters - .expressionModalOutput() - .should('have.text', '[Execute previous nodes for preview]'); - - // Run workflow - cy.get('body').type('{esc}'); - ndv.actions.close(); - WorkflowPage.actions.executeNode('No Operation, do nothing', { anchor: 'topLeft' }); - WorkflowPage.actions.openNode('Get many items'); - WorkflowPage.actions.openExpressionEditorModal(); - - // Previous nodes have run, input can be resolved - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().click().type('{{ $json.myStr'); - WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday'); - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().click().type('{{ $input.item.json.myStr'); - WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday'); - WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters - .expressionModalInput() - .click() - .type("{{ $('Schedule Trigger').item.json.myStr"); - WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday'); - }); - }); -}); diff --git a/packages/testing/playwright/pages/NodeDetailsViewPage.ts b/packages/testing/playwright/pages/NodeDetailsViewPage.ts index f20f4dc486..7cd6682472 100644 --- a/packages/testing/playwright/pages/NodeDetailsViewPage.ts +++ b/packages/testing/playwright/pages/NodeDetailsViewPage.ts @@ -513,6 +513,31 @@ export class NodeDetailsViewPage extends BasePage { await this.getInlineExpressionEditorItemPrevButton().click(); } + async openExpressionEditorModal(parameterName: string) { + await this.activateParameterExpressionEditor(parameterName); + const parameter = this.getParameterInput(parameterName); + await parameter.click(); + const expander = parameter.getByTestId('expander'); + await expander.click(); + + await this.page.getByTestId('expression-modal-input').waitFor({ state: 'visible' }); + } + + getExpressionEditorModalInput() { + return this.page.getByTestId('expression-modal-input').getByRole('textbox'); + } + + async fillExpressionEditorModalInput(text: string) { + const input = this.getExpressionEditorModalInput(); + await input.clear(); + await input.click(); + await input.fill(text); + } + + getExpressionEditorModalOutput() { + return this.page.getByTestId('expression-modal-output'); + } + async typeIntoParameterInput(parameterName: string, content: string): Promise { const input = this.getParameterInput(parameterName); await input.type(content); diff --git a/packages/testing/playwright/tests/ui/9-expression-editor-modal.spec.ts b/packages/testing/playwright/tests/ui/9-expression-editor-modal.spec.ts new file mode 100644 index 0000000000..e9262e7aeb --- /dev/null +++ b/packages/testing/playwright/tests/ui/9-expression-editor-modal.spec.ts @@ -0,0 +1,117 @@ +import { test, expect } from '../../fixtures/base'; + +test.describe('Expression editor modal', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.start.fromBlankCanvas(); + await n8n.canvas.addInitialNodeToCanvas('Schedule Trigger'); + await n8n.ndv.close(); + }); + + test.describe('Keybinds', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.canvas.addNode('Hacker News', { action: 'Get many items' }); + await n8n.ndv.openExpressionEditorModal('limit'); + }); + + test('should save the workflow with save keybind', async ({ n8n }) => { + const input = n8n.ndv.getExpressionEditorModalInput(); + await n8n.ndv.fillExpressionEditorModalInput('{{ "hello"'); + await expect(n8n.ndv.getExpressionEditorModalOutput()).toContainText('hello'); + + await input.press('ControlOrMeta+s'); + await n8n.notifications.waitForNotificationAndClose('Saved successfully'); + }); + }); + + test.describe('Static data', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.canvas.addNode('Hacker News', { action: 'Get many items' }); + await n8n.ndv.openExpressionEditorModal('limit'); + }); + + test('should resolve primitive resolvables', async ({ n8n }) => { + const output = n8n.ndv.getExpressionEditorModalOutput(); + + // Test number addition + await n8n.ndv.fillExpressionEditorModalInput('{{ 1 + 2 }}'); + await expect(output).toContainText(/^3$/); + + // Test string concatenation + await n8n.ndv.fillExpressionEditorModalInput('{{ "ab" + "cd" }}'); + await expect(output).toContainText(/^abcd$/); + + // Test boolean logic + await n8n.ndv.fillExpressionEditorModalInput('{{ true && false }}'); + await expect(output).toContainText(/^false$/); + }); + + test('should resolve object resolvables', async ({ n8n }) => { + const output = n8n.ndv.getExpressionEditorModalOutput(); + + // Test object creation + await n8n.ndv.fillExpressionEditorModalInput('{{ { a : 1 } }}'); + await expect(output).toContainText(/^\[Object: \{"a": 1\}\]$/); + + // Test object property access + await n8n.ndv.fillExpressionEditorModalInput('{{ { a : 1 }.a }}'); + await expect(output).toContainText(/^1$/); + }); + + test('should resolve array resolvables', async ({ n8n }) => { + const output = n8n.ndv.getExpressionEditorModalOutput(); + + // Test array creation + await n8n.ndv.fillExpressionEditorModalInput('{{ [1, 2, 3] }}'); + await expect(output).toContainText(/^\[Array: \[1,2,3\]\]$/); + + // Test array element access + await n8n.ndv.fillExpressionEditorModalInput('{{ [1, 2, 3][0] }}'); + await expect(output).toContainText(/^1$/); + }); + }); + + test.describe('Dynamic data', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.canvas.openNode('Schedule Trigger'); + await n8n.ndv.setPinnedData([{ myStr: 'Monday' }]); + await n8n.ndv.clickBackToCanvasButton(); + await n8n.canvas.addNode('No Operation, do nothing', { closeNDV: true }); + await n8n.canvas.addNode('Hacker News', { action: 'Get many items' }); + await n8n.ndv.openExpressionEditorModal('limit'); + }); + + test('should resolve $parameter[]', async ({ n8n }) => { + const output = n8n.ndv.getExpressionEditorModalOutput(); + await n8n.ndv.fillExpressionEditorModalInput('{{ $parameter["operation"] }}'); + await expect(output).toHaveText('getAll'); + }); + + test('should resolve input: $json,$input,$(nodeName)', async ({ n8n }) => { + const output = n8n.ndv.getExpressionEditorModalOutput(); + + // Previous nodes have not run, input is empty + await n8n.ndv.fillExpressionEditorModalInput('{{ $json.myStr }}'); + await expect(output).toHaveText('[Execute previous nodes for preview]'); + await n8n.ndv.fillExpressionEditorModalInput('{{ $input.item.json.myStr }}'); + await expect(output).toHaveText('[Execute previous nodes for preview]'); + await n8n.ndv.fillExpressionEditorModalInput("{{ $('Schedule Trigger').item.json.myStr }}"); + await expect(output).toHaveText('[Execute previous nodes for preview]'); + + // Run workflow + await output.click(); + await n8n.page.keyboard.press('Escape'); + await n8n.ndv.clickBackToCanvasButton(); + await n8n.canvas.executeNode('No Operation, do nothing'); + await n8n.canvas.openNode('Get many items'); + await n8n.ndv.openExpressionEditorModal('limit'); + + // Previous nodes have run, input can be resolved + await n8n.ndv.fillExpressionEditorModalInput('{{ $json.myStr }}'); + await expect(output).toHaveText('Monday'); + await n8n.ndv.fillExpressionEditorModalInput('{{ $input.item.json.myStr }}'); + await expect(output).toHaveText('Monday'); + await n8n.ndv.fillExpressionEditorModalInput("{{ $('Schedule Trigger').item.json.myStr }}"); + await expect(output).toHaveText('Monday'); + }); + }); +});