From b4aaec51f6e503a4cdb5e8cc2415ed5d7ca2357f Mon Sep 17 00:00:00 2001 From: yehorkardash Date: Fri, 12 Sep 2025 10:20:40 +0000 Subject: [PATCH] test(editor): Migrate inline-expression-editor to playwright (#19411) --- cypress/e2e/11-inline-expression-editor.cy.ts | 173 ------------------ .../testing/playwright/config/constants.ts | 2 + .../playwright/pages/NodeDetailsViewPage.ts | 19 +- .../ui/11-inline-expression-editor.spec.ts | 159 ++++++++++++++++ 4 files changed, 175 insertions(+), 178 deletions(-) delete mode 100644 cypress/e2e/11-inline-expression-editor.cy.ts create mode 100644 packages/testing/playwright/tests/ui/11-inline-expression-editor.spec.ts diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts deleted file mode 100644 index bfbbbcaf16..0000000000 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { EDIT_FIELDS_SET_NODE_NAME } from '../constants'; -import { NDV } from '../pages/ndv'; -import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; - -const ndv = new NDV(); -const WorkflowPage = new WorkflowPageClass(); - -describe('Inline expression editor', () => { - beforeEach(() => { - WorkflowPage.actions.visit(); - WorkflowPage.actions.addInitialNodeToCanvas('Schedule'); - cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError'); - }); - - describe('Basic UI functionality', () => { - it('should open and close inline expression preview', () => { - WorkflowPage.actions.zoomToFit(); - WorkflowPage.actions.openNode('Schedule'); - WorkflowPage.actions.openInlineExpressionEditor(); - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().type('123'); - WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^123$/); - // click outside to close - ndv.getters.outputPanel().click(); - WorkflowPage.getters.inlineExpressionEditorOutput().should('not.exist'); - }); - - it('should switch between expression and fixed using keyboard', () => { - WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME); - WorkflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME); - - // Should switch to expression with = - ndv.getters.assignmentCollectionAdd('assignments').click(); - ndv.actions.typeIntoParameterInput('value', '='); - - // Should complete {{ --> {{ | }} - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().should('have.text', '{{ }}'); - - // Should switch back to fixed with backspace on empty expression - ndv.actions.typeIntoParameterInput('value', '{selectall}{backspace}'); - ndv.getters.parameterInput('value').click(); - ndv.actions.typeIntoParameterInput('value', '{backspace}'); - ndv.getters.inlineExpressionEditorInput().should('not.exist'); - }); - }); - - describe('Static data', () => { - beforeEach(() => { - WorkflowPage.actions.addNodeToCanvas('Hacker News'); - WorkflowPage.actions.zoomToFit(); - WorkflowPage.actions.openNode('Get many items'); - WorkflowPage.actions.openInlineExpressionEditor(); - }); - - it('should resolve primitive resolvables', () => { - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().type('1 + 2'); - WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^3$/); - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().type('"ab"'); - WorkflowPage.getters.inlineExpressionEditorInput().type('{rightArrow}+'); - WorkflowPage.getters.inlineExpressionEditorInput().type('"cd"'); - WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^abcd$/); - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().type('true && false'); - WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^false$/); - }); - - it('should resolve object resolvables', () => { - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters - .inlineExpressionEditorInput() - .type('{ a: 1 }', { parseSpecialCharSequences: false }); - WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Object: \{"a": 1\}\]$/); - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters - .inlineExpressionEditorInput() - .type('{ a: 1 }.a', { parseSpecialCharSequences: false }); - WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/); - }); - - it('should resolve array resolvables', () => { - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]'); - WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Array: \[1,2,3\]\]$/); - - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]'); - WorkflowPage.getters.inlineExpressionEditorInput().type('[0]'); - WorkflowPage.getters.inlineExpressionEditorOutput().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.openInlineExpressionEditor(); - }); - - it('should resolve $parameter[]', () => { - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - // Resolving $parameter is slow, especially on CI runner - WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]'); - WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'getAll'); - }); - - it('should resolve input: $json,$input,$(nodeName)', () => { - // Previous nodes have not run, input is empty - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr'); - WorkflowPage.getters - .inlineExpressionEditorOutput() - .should('have.text', '[Execute previous nodes for preview]'); - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr'); - WorkflowPage.getters - .inlineExpressionEditorOutput() - .should('have.text', '[Execute previous nodes for preview]'); - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters - .inlineExpressionEditorInput() - .type("$('Schedule Trigger').item.json.myStr"); - WorkflowPage.getters - .inlineExpressionEditorOutput() - .should('have.text', '[Execute previous nodes for preview]'); - - // Run workflow - ndv.actions.close(); - WorkflowPage.actions.executeNode('No Operation, do nothing', { anchor: 'topLeft' }); - WorkflowPage.actions.openNode('Get many items'); - WorkflowPage.actions.openInlineExpressionEditor(); - - // Previous nodes have run, input can be resolved - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr'); - WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday'); - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr'); - WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday'); - WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); - WorkflowPage.getters - .inlineExpressionEditorInput() - .type("$('Schedule Trigger').item.json.myStr"); - WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday'); - }); - }); -}); diff --git a/packages/testing/playwright/config/constants.ts b/packages/testing/playwright/config/constants.ts index dcad2f99c9..a59c5bafff 100644 --- a/packages/testing/playwright/config/constants.ts +++ b/packages/testing/playwright/config/constants.ts @@ -33,6 +33,8 @@ export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory'; export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser'; export const WEBHOOK_NODE_NAME = 'Webhook'; export const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Workflow'; +export const NO_OPERATION_NODE_NAME = 'No Operation, do nothing'; +export const HACKER_NEWS_NODE_NAME = 'Hacker News'; export const NEW_GOOGLE_ACCOUNT_NAME = 'Gmail account'; export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account'; diff --git a/packages/testing/playwright/pages/NodeDetailsViewPage.ts b/packages/testing/playwright/pages/NodeDetailsViewPage.ts index 725dfc7d1a..1a99b97468 100644 --- a/packages/testing/playwright/pages/NodeDetailsViewPage.ts +++ b/packages/testing/playwright/pages/NodeDetailsViewPage.ts @@ -141,7 +141,16 @@ export class NodeDetailsViewPage extends BasePage { .getByTestId('assignment-value'); } - getInlineExpressionEditorInput() { + /** + * Get the inline expression editor input + * @param parameterName - The name of the parameter to get the inline expression editor input for. If not set, gets the first inline expression editor input on page + * @returns The inline expression editor input + */ + getInlineExpressionEditorInput(parameterName?: string) { + if (parameterName) { + const parameterInput = this.getParameterInput(parameterName); + return parameterInput.getByTestId('inline-expression-editor-input'); + } return this.page.getByTestId('inline-expression-editor-input'); } @@ -165,15 +174,15 @@ export class NodeDetailsViewPage extends BasePage { return this.page.locator('.el-popper:visible'); } - async clearExpressionEditor() { - const editor = this.getInlineExpressionEditorInput(); + async clearExpressionEditor(parameterName?: string) { + const editor = this.getInlineExpressionEditorInput(parameterName); await editor.click(); await this.page.keyboard.press('ControlOrMeta+A'); await this.page.keyboard.press('Delete'); } - async typeInExpressionEditor(text: string) { - const editor = this.getInlineExpressionEditorInput(); + async typeInExpressionEditor(text: string, parameterName?: string) { + const editor = this.getInlineExpressionEditorInput(parameterName); await editor.click(); await editor.type(text); } diff --git a/packages/testing/playwright/tests/ui/11-inline-expression-editor.spec.ts b/packages/testing/playwright/tests/ui/11-inline-expression-editor.spec.ts new file mode 100644 index 0000000000..34189357da --- /dev/null +++ b/packages/testing/playwright/tests/ui/11-inline-expression-editor.spec.ts @@ -0,0 +1,159 @@ +import { + EDIT_FIELDS_SET_NODE_NAME, + SCHEDULE_TRIGGER_NODE_NAME, + NO_OPERATION_NODE_NAME, + HACKER_NEWS_NODE_NAME, +} from '../../config/constants'; +import { test, expect } from '../../fixtures/base'; + +const SCHEDULE_PARAMETER_NAME = 'daysInterval'; +const HACKER_NEWS_ACTION = 'Get many items'; +const HACKER_NEWS_PARAMETER_NAME = 'limit'; + +test.describe('Inline expression editor', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.start.fromBlankCanvas(); + }); + + test.describe('Basic UI functionality', () => { + test('should open and close inline expression preview', async ({ n8n }) => { + await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME); + await n8n.ndv.activateParameterExpressionEditor(SCHEDULE_PARAMETER_NAME); + + await n8n.ndv.getInlineExpressionEditorInput(SCHEDULE_PARAMETER_NAME).click(); + await n8n.ndv.clearExpressionEditor(SCHEDULE_PARAMETER_NAME); + await n8n.ndv.typeInExpressionEditor('{{ 123', SCHEDULE_PARAMETER_NAME); + + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('123'); + + // Click outside to close + await n8n.ndv.outputPanel.get().click(); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toBeHidden(); + }); + + test('should switch between expression and fixed using keyboard', async ({ n8n }) => { + await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME); + + // Should switch to expression with = + await n8n.ndv.getAssignmentCollectionAdd('assignments').click(); + await n8n.ndv.fillParameterInputByName('value', '='); + + // Should complete {{ --> {{ | }} + await n8n.ndv.getInlineExpressionEditorInput().click(); + await n8n.ndv.typeInExpressionEditor('{{'); + await expect(n8n.ndv.getInlineExpressionEditorInput()).toHaveText('{{ }}'); + + // Should switch back to fixed with backspace on empty expression + await n8n.ndv.clearExpressionEditor('value'); + const parameterInput = n8n.ndv.getParameterInput('value'); + await parameterInput.click(); + await parameterInput.focus(); + await parameterInput.press('Backspace'); + // eslint-disable-next-line playwright/no-wait-for-timeout + await n8n.page.waitForTimeout(1000); + await expect(n8n.ndv.getInlineExpressionEditorInput()).toBeHidden(); + }); + }); + + test.describe('Static data', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.start.fromBlankCanvas(); + await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME); + await n8n.ndv.activateParameterExpressionEditor(SCHEDULE_PARAMETER_NAME); + }); + + test('should resolve primitive resolvables', async ({ n8n }) => { + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ 1 + 2'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('3'); + + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ "ab" + "cd"'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('abcd'); + + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ true && false'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('false'); + }); + + test('should resolve object resolvables', async ({ n8n }) => { + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ { a: 1 }'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText( + /^\[Object: \{"a": 1\}\]$/, + ); + + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ { a: 1 }.a'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('1'); + }); + + test('should resolve array resolvables', async ({ n8n }) => { + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ [1, 2, 3]'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText(/^\[Array: \[1,2,3\]\]$/); + + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ [1, 2, 3][0]'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('1'); + }); + }); + + test.describe('Dynamic data', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME); + await n8n.ndv.setPinnedData([{ myStr: 'Monday' }]); + await n8n.ndv.close(); + await n8n.canvas.addNode(NO_OPERATION_NODE_NAME, { closeNDV: true }); + await n8n.canvas.addNode(HACKER_NEWS_NODE_NAME, { action: HACKER_NEWS_ACTION }); + await n8n.ndv.activateParameterExpressionEditor(HACKER_NEWS_PARAMETER_NAME); + }); + + test('should resolve $parameter[]', async ({ n8n }) => { + await n8n.ndv.clearExpressionEditor(); + // Resolving $parameter is slow, especially on CI runner + await n8n.ndv.typeInExpressionEditor('{{ $parameter["operation"]'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('getAll'); + }); + + test('should resolve input: $json,$input,$(nodeName)', async ({ n8n }) => { + // Previous nodes have not run, input is empty + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ $json.myStr'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText( + '[Execute previous nodes for preview]', + ); + + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ $input.item.json.myStr'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText( + '[Execute previous nodes for preview]', + ); + + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor("{{ $('No Operation, do nothing').item.json.myStr"); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText( + '[Execute previous nodes for preview]', + ); + + // Run workflow + await n8n.ndv.close(); + await n8n.canvas.executeNode(NO_OPERATION_NODE_NAME); + await n8n.canvas.openNode(HACKER_NEWS_ACTION); + await n8n.ndv.activateParameterExpressionEditor(HACKER_NEWS_PARAMETER_NAME); + + // Previous nodes have run, input can be resolved + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ $json.myStr'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('Monday'); + + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ $input.item.json.myStr'); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('Monday'); + + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor("{{ $('No Operation, do nothing').item.json.myStr"); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('Monday'); + }); + }); +});