diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts deleted file mode 100644 index 6dad7d1619..0000000000 --- a/cypress/e2e/5-ndv.cy.ts +++ /dev/null @@ -1,966 +0,0 @@ -import { setCredentialValues } from '../composables/modals/credential-modal'; -import { - clickCreateNewCredential, - clickGetBackToCanvas, - setParameterSelectByContent, -} from '../composables/ndv'; -import { openNode } from '../composables/workflow'; -import { - EDIT_FIELDS_SET_NODE_NAME, - MANUAL_TRIGGER_NODE_DISPLAY_NAME, - NOTION_NODE_NAME, -} from '../constants'; -import { NDV, WorkflowPage } from '../pages'; -import { NodeCreator } from '../pages/features/node-creator'; - -const workflowPage = new WorkflowPage(); -const ndv = new NDV(); - -describe('NDV', () => { - beforeEach(() => { - workflowPage.actions.visit(); - workflowPage.actions.renameWithUniqueName(); - workflowPage.actions.saveWorkflowOnButtonClick(); - }); - - it('should show up when double clicked on a node and close when Back to canvas clicked', () => { - workflowPage.actions.addInitialNodeToCanvas('Manual'); - workflowPage.getters.canvasNodes().first().dblclick(); - ndv.getters.container().should('be.visible'); - ndv.getters.backToCanvas().click(); - ndv.getters.container().should('not.be.visible'); - }); - - it('should show input panel when node is not connected', () => { - workflowPage.actions.addInitialNodeToCanvas('Manual'); - workflowPage.actions.deselectAll(); - workflowPage.actions.addNodeToCanvas('Set'); - workflowPage.getters.canvasNodes().last().dblclick(); - ndv.getters.container().should('be.visible').should('contain', 'Wire me up'); - }); - - it('should test webhook node', () => { - workflowPage.actions.addInitialNodeToCanvas('Webhook'); - workflowPage.getters.canvasNodes().first().dblclick(); - - ndv.actions.execute(); - ndv.getters.copyInput().click(); - - cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); - - cy.readClipboard().then((url) => { - cy.request({ - method: 'GET', - url, - }).then((resp) => { - expect(resp.status).to.eq(200); - }); - }); - - ndv.getters.outputDisplayMode().should('have.length.at.least', 1).and('be.visible'); - }); - - it('should change input and go back to canvas', () => { - cy.createFixtureWorkflow('NDV-test-select-input.json', 'NDV test select input'); - workflowPage.actions.zoomToFit(); - workflowPage.getters.canvasNodes().last().dblclick(); - ndv.actions.switchInputMode('Table'); - ndv.getters.inputSelect().click(); - ndv.getters.inputOption().last().click(); - ndv.getters.inputDataContainer().should('be.visible'); - ndv.getters.inputDataContainer().should('contain', 'start'); - ndv.getters.backToCanvas().click(); - ndv.getters.container().should('not.be.visible'); - cy.shouldNotHaveConsoleErrors(); - }); - - it('should show correct validation state for resource locator params', () => { - workflowPage.actions.addNodeToCanvas('Typeform', true, true); - ndv.getters.container().should('be.visible'); - cy.get('.has-issues').should('have.length', 0); - cy.get('[class*=hasIssues]').should('have.length', 0); - ndv.getters.backToCanvas().click(); - // Both credentials and resource locator errors should be visible - workflowPage.actions.openNode('Typeform'); - cy.get('.has-issues').should('have.length', 1); - cy.get('[class*=hasIssues]').should('have.length', 1); - }); - - it('should show validation errors only after blur or re-opening of NDV', () => { - workflowPage.actions.addNodeToCanvas('Manual'); - workflowPage.actions.addNodeToCanvas('Airtable', true, true, 'Search records'); - ndv.getters.container().should('be.visible'); - cy.get('.has-issues').should('have.length', 0); - ndv.getters.parameterInput('table').find('input').eq(1).focus().blur(); - ndv.getters.parameterInput('base').find('input').eq(1).focus().blur(); - cy.get('.has-issues').should('have.length', 2); - ndv.getters.backToCanvas().click(); - workflowPage.actions.openNode('Search records'); - cy.get('.has-issues').should('have.length', 2); - cy.get('[class*=hasIssues]').should('have.length', 1); - }); - - // Correctly failing in V2 - node issues are only shows after execution - it('should show all validation errors when opening pasted node', () => { - cy.createFixtureWorkflow('Test_workflow_ndv_errors.json', 'Validation errors'); - workflowPage.getters.canvasNodes().should('have.have.length', 1); - workflowPage.actions.openNode('Airtable'); - cy.get('.has-issues').should('have.length', 3); - cy.get('[class*=hasIssues]').should('have.length', 1); - }); - - it('should render run errors correctly', () => { - cy.createFixtureWorkflow('Test_workflow_ndv_run_error.json', 'Run error'); - workflowPage.actions.openNode('Error'); - ndv.actions.execute(); - ndv.getters - .nodeRunErrorMessage() - .should( - 'have.text', - "Paired item data for item from node 'Break pairedItem chain' is unavailable. Ensure 'Break pairedItem chain' is providing the required output.", - ); - ndv.getters - .nodeRunErrorDescription() - .should( - 'contains.text', - "An expression here won't work because it uses .item and n8n can't figure out the matching item.", - ); - ndv.getters.nodeRunErrorIndicator().should('be.visible'); - ndv.getters.nodeRunTooltipIndicator().should('be.visible'); - // The error details should be hidden behind a tooltip - ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Start Time'); - ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Execution Time'); - }); - - it('should save workflow using keyboard shortcut from NDV', () => { - workflowPage.actions.addNodeToCanvas('Manual'); - workflowPage.actions.addNodeToCanvas('Set', true, true); - ndv.getters.container().should('be.visible'); - workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); - workflowPage.getters.isWorkflowSaved(); - }); - - describe('test output schema view', () => { - const schemaKeys = [ - 'id', - 'name', - 'email', - 'notes', - 'country', - 'created', - 'objectValue', - 'prop1', - 'prop2', - ]; - function setupSchemaWorkflow() { - cy.createFixtureWorkflow('Test_workflow_schema_test.json'); - workflowPage.actions.zoomToFit(); - workflowPage.actions.openNode('Set'); - ndv.actions.execute(); - } - - it('should switch to output schema view and validate it', () => { - setupSchemaWorkflow(); - ndv.getters.outputDisplayMode().children().should('have.length', 3); - ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table'); - ndv.actions.switchOutputMode('Schema'); - ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema'); - - schemaKeys.forEach((key) => { - ndv.getters - .outputPanel() - .find('[data-test-id=run-data-schema-item]') - .contains(key) - .should('exist'); - }); - }); - it('should preserve schema view after execution', () => { - setupSchemaWorkflow(); - ndv.actions.switchOutputMode('Schema'); - ndv.actions.execute(); - ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema'); - }); - it('should collapse and expand nested schema object', () => { - setupSchemaWorkflow(); - const expandedObjectProps = ['prop1', 'prop2']; - const getObjectValueItem = () => - ndv.getters - .outputPanel() - .find('[data-test-id=run-data-schema-item]') - .filter(':contains("objectValue")'); - ndv.actions.switchOutputMode('Schema'); - - expandedObjectProps.forEach((key) => { - ndv.getters - .outputPanel() - .find('[data-test-id=run-data-schema-item]') - .contains(key) - .should('be.visible'); - }); - getObjectValueItem().find('.toggle').click({ force: true }); - expandedObjectProps.forEach((key) => { - ndv.getters - .outputPanel() - .find('[data-test-id=run-data-schema-item]') - .contains(key) - .should('not.be.visible'); - }); - }); - - it('should not display pagination for schema', () => { - setupSchemaWorkflow(); - ndv.getters.backToCanvas().click(); - workflowPage.actions.deselectAll(); - workflowPage.getters.canvasNodeByName('Set').click(); - workflowPage.actions.addNodeToCanvas( - 'Customer Datastore (n8n training)', - true, - true, - 'Get All People', - ); - ndv.actions.execute(); - ndv.getters.outputPanel().contains('25 items').should('exist'); - ndv.getters.outputPanel().find('[class*=_pagination]').should('exist'); - ndv.actions.switchOutputMode('Schema'); - ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist'); - ndv.actions.switchOutputMode('JSON'); - ndv.getters.outputPanel().find('[class*=_pagination]').should('exist'); - }); - it('should display large schema', () => { - cy.createFixtureWorkflow( - 'Test_workflow_schema_test_pinned_data.json', - 'NDV test schema view 2', - ); - workflowPage.actions.zoomToFit(); - workflowPage.actions.openNode('Set'); - - ndv.getters.outputPanel().contains('20 items').should('exist'); - ndv.getters.outputPanel().find('[class*=_pagination]').should('exist'); - ndv.actions.switchOutputMode('Schema'); - ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist'); - ndv.getters - .outputPanel() - .find('[data-test-id=run-data-schema-item]') - .should('have.length.above', 10); - }); - }); - - it('can link and unlink run selectors between input and output', () => { - cy.createFixtureWorkflow('Test_workflow_5.json', 'Test'); - workflowPage.actions.zoomToFit(); - workflowPage.actions.executeWorkflow(); - workflowPage.actions.openNode('Set3'); - - ndv.actions.switchInputMode('Table'); - ndv.actions.switchOutputMode('Table'); - - // Start from linked state - ndv.getters.outputLinkRun().then(($el) => { - const classList = Array.from($el[0].classList); - if (!classList.includes('linked')) { - ndv.actions.toggleOutputRunLinking(); - ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip - } - }); - - ndv.getters - .inputRunSelector() - .should('exist') - .find('input') - .should('include.value', '2 of 2 (6 items)'); - ndv.getters - .outputRunSelector() - .should('exist') - .find('input') - .should('include.value', '2 of 2 (6 items)'); - - ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); - ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 2 (6 items)'); - ndv.getters.inputTbodyCell(1, 0).should('have.text', '1111'); - ndv.getters.outputTbodyCell(1, 0).should('have.text', '1111'); - - ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip - ndv.actions.changeInputRunSelector('2 of 2 (6 items)'); - ndv.getters.outputRunSelector().find('input').should('include.value', '2 of 2 (6 items)'); - - // unlink - ndv.actions.toggleOutputRunLinking(); - ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip - ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); - ndv.getters - .inputRunSelector() - .should('exist') - .find('input') - .should('include.value', '2 of 2 (6 items)'); - - // link again - ndv.actions.toggleOutputRunLinking(); - ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip - ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 2 (6 items)'); - - // unlink again - ndv.actions.toggleInputRunLinking(); - ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip - ndv.actions.changeInputRunSelector('2 of 2 (6 items)'); - ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 2 (6 items)'); - - // link again - ndv.actions.toggleInputRunLinking(); - ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip - ndv.getters.outputRunSelector().find('input').should('include.value', '2 of 2 (6 items)'); - }); - - it('should display parameter hints correctly', () => { - workflowPage.actions.visit(); - - cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow 1'); - workflowPage.actions.openNode('Set1'); - - ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions - - [ - { - input: 'hello', - }, - { - input: '', - output: '[empty]', - }, - { - input: ' test', - }, - { - input: ' ', - }, - { - input: '
', - }, - ].forEach(({ input, output }) => { - if (input) { - ndv.actions.typeIntoParameterInput('value', input); - } - ndv.getters.parameterInput('name').click(); // remove focus from input, hide expression preview - - ndv.actions.validateExpressionPreview('value', output ?? input); - ndv.getters.parameterInput('value').clear(); - }); - }); - - it('webhook shoul fallback to webhookId if path is empty', () => { - workflowPage.actions.addInitialNodeToCanvas('Webhook'); - workflowPage.getters.canvasNodes().first().dblclick(); - - workflowPage.getters.nodeIssuesByName('Webhook').should('not.exist'); - ndv.getters.nodeExecuteButton().should('not.be.disabled'); - ndv.getters.triggerPanelExecuteButton().should('exist'); - - ndv.getters.parameterInput('path').clear(); - - const urrWithUUID = - /https?:\/\/[^\/]+\/webhook-test\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i; - - cy.contains('Webhook URLs') - .parent() - .invoke('text') - .then((text) => { - const match = text.match(urrWithUUID); - expect(match, 'Should contain dynamic URL with UUID').to.not.be.null; - }); - - ndv.actions.close(); - - workflowPage.getters.canvasNodes().first().dblclick(); - ndv.getters.parameterInput('path').type('test-path'); - - const urlWithCustomPath = /https?:\/\/[^\/]+\/webhook-test\/test-path/i; - - cy.contains('Webhook URLs') - .parent() - .invoke('text') - .then((text) => { - const match = text.match(urlWithCustomPath); - expect(match, 'Should contain URL with custom path').to.not.be.null; - }); - - ndv.getters.nodeExecuteButton().should('not.be.disabled'); - ndv.getters.triggerPanelExecuteButton().should('exist'); - - ndv.actions.close(); - workflowPage.getters.nodeIssuesByName('Webhook').should('not.exist'); - }); - - it('should not push NDV header out with a lot of code in Code node editor', () => { - workflowPage.actions.addInitialCodeNodeToCanvas({ keepNdvOpen: true }); - ndv.getters.parameterInput('jsCode').get('.cm-content').type('{selectall}').type('{backspace}'); - cy.fixture('Dummy_javascript.txt').then((code) => { - ndv.getters.parameterInput('jsCode').get('.cm-content').paste(code); - }); - ndv.getters.nodeExecuteButton().should('be.visible'); - }); - - it('should allow editing code in fullscreen in the code editors', () => { - // Code (JavaScript) - workflowPage.actions.addInitialCodeNodeToCanvas({ keepNdvOpen: true }); - ndv.actions.openCodeEditorFullscreen(); - - ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('foo()'); - ndv.getters.codeEditorFullscreen().should('contain.text', 'foo()'); - ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); - ndv.getters.parameterInput('jsCode').get('.cm-content').should('contain.text', 'foo()'); - ndv.actions.close(); - - // SQL - workflowPage.actions.addNodeToCanvas('Postgres', true, true, 'Execute a SQL query'); - ndv.actions.openCodeEditorFullscreen(); - - ndv.getters - .codeEditorFullscreen() - .type('{selectall}') - .type('{backspace}') - .paste('SELECT * FROM workflows'); - ndv.getters.codeEditorFullscreen().should('contain.text', 'SELECT * FROM workflows'); - ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); - ndv.getters - .parameterInput('query') - .get('.cm-content') - .should('contain.text', 'SELECT * FROM workflows'); - ndv.actions.close(); - - // HTML - workflowPage.actions.addNodeToCanvas('HTML', true, true, 'Generate HTML template'); - ndv.actions.openCodeEditorFullscreen(); - - ndv.getters - .codeEditorFullscreen() - .type('{selectall}') - .type('{backspace}') - .type('
Hello World
'); - ndv.getters.codeEditorFullscreen().should('contain.text', '
Hello World
'); - ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); - ndv.getters - .parameterInput('html') - .get('.cm-content') - .should('contain.text', '
Hello World
'); - ndv.actions.close(); - - // JSON - workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true); - setParameterSelectByContent('mode', 'JSON'); - ndv.actions.openCodeEditorFullscreen(); - ndv.getters - .codeEditorFullscreen() - .type('{selectall}') - .type('{backspace}') - .type('{ "key": "value" }', { parseSpecialCharSequences: false }); - ndv.getters.codeEditorFullscreen().should('contain.text', '{ "key": "value" }'); - cy.wait(200); - ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); - ndv.getters - .parameterInput('jsonOutput') - .get('.cm-content') - .should('contain.text', '{ "key": "value" }'); - }); - - it('should not retrieve remote options when a parameter value changes', () => { - cy.intercept( - 'POST', - '/rest/dynamic-node-parameters/options', - cy.spy().as('fetchParameterOptions'), - ); - workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' }); - // Type something into the field - ndv.actions.typeIntoParameterInput('otherField', 'test'); - // Should call the endpoint only once (on mount), not for every keystroke - cy.get('@fetchParameterOptions').should('have.been.calledOnce'); - }); - - describe('floating nodes', () => { - function getFloatingNodeByPosition( - position: 'inputMain' | 'outputMain' | 'outputSub' | 'inputSub', - ) { - return cy.get(`[data-node-placement=${position}]`); - } - - it('should traverse floating nodes with mouse', () => { - cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes'); - - workflowPage.actions.deselectAll(); - - workflowPage.getters.canvasNodes().first().dblclick(); - getFloatingNodeByPosition('inputMain').should('not.exist'); - getFloatingNodeByPosition('outputMain').should('exist'); - // Traverse 4 connected node forwards - Array.from(Array(4).keys()).forEach((i) => { - getFloatingNodeByPosition('outputMain').click({ force: true }); - ndv.getters.nodeNameContainer().should('contain', `Node ${i + 1}`); - getFloatingNodeByPosition('inputMain').should('exist'); - getFloatingNodeByPosition('outputMain').should('exist'); - ndv.actions.close(); - // These two lines are broken in V2 - workflowPage.getters.selectedNodes().should('have.length', 1); - workflowPage.getters - .selectedNodes() - .first() - .should('contain', `Node ${i + 1}`); - workflowPage.getters.selectedNodes().first().dblclick(); - }); - getFloatingNodeByPosition('outputMain').click({ force: true }); - ndv.getters.nodeNameContainer().should('contain', 'Chain'); - // Traverse 4 connected node backwards - Array.from(Array(4).keys()).forEach((i) => { - getFloatingNodeByPosition('inputMain').click({ force: true }); - ndv.getters.nodeNameContainer().should('contain', `Node ${4 - i}`); - getFloatingNodeByPosition('outputMain').should('exist'); - getFloatingNodeByPosition('inputMain').should('exist'); - }); - getFloatingNodeByPosition('inputMain').click({ force: true }); - workflowPage.getters - .selectedNodes() - .first() - .should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); - getFloatingNodeByPosition('inputMain').should('not.exist'); - getFloatingNodeByPosition('inputSub').should('not.exist'); - getFloatingNodeByPosition('outputSub').should('not.exist'); - ndv.actions.close(); - workflowPage.getters.selectedNodes().should('have.length', 1); - workflowPage.getters - .selectedNodes() - .first() - .should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); - }); - - it('should traverse floating nodes with keyboard', () => { - cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes'); - workflowPage.actions.deselectAll(); - - workflowPage.getters.canvasNodes().first().dblclick(); - getFloatingNodeByPosition('inputMain').should('not.exist'); - getFloatingNodeByPosition('outputMain').should('exist'); - // Traverse 4 connected node forwards - Array.from(Array(4).keys()).forEach((i) => { - cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']); - ndv.getters.nodeNameContainer().should('contain', `Node ${i + 1}`); - getFloatingNodeByPosition('inputMain').should('exist'); - getFloatingNodeByPosition('outputMain').should('exist'); - ndv.actions.close(); - workflowPage.getters.selectedNodes().should('have.length', 1); - workflowPage.getters - .selectedNodes() - .first() - .should('contain', `Node ${i + 1}`); - workflowPage.getters.selectedNodes().first().dblclick(); - }); - - cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']); - ndv.getters.nodeNameContainer().should('contain', 'Chain'); - - // Traverse 4 connected node backwards - Array.from(Array(4).keys()).forEach((i) => { - cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowLeft']); - ndv.getters.nodeNameContainer().should('contain', `Node ${4 - i}`); - getFloatingNodeByPosition('outputMain').should('exist'); - getFloatingNodeByPosition('inputMain').should('exist'); - }); - cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowLeft']); - workflowPage.getters - .selectedNodes() - .first() - .should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); - getFloatingNodeByPosition('inputMain').should('not.exist'); - getFloatingNodeByPosition('inputSub').should('not.exist'); - getFloatingNodeByPosition('outputSub').should('not.exist'); - ndv.actions.close(); - workflowPage.getters.selectedNodes().should('have.length', 1); - workflowPage.getters - .selectedNodes() - .first() - .should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); - }); - - it('should connect floating sub-nodes', () => { - const nodeCreator = new NodeCreator(); - const connectionGroups = [ - { - title: 'Language Models', - id: 'ai_languageModel', - index: 0, - }, - { - title: 'Tools', - id: 'ai_tool', - index: 0, - }, - ]; - - workflowPage.actions.addInitialNodeToCanvas('AI Agent', { keepNdvOpen: true }); - - connectionGroups.forEach((group) => { - cy.getByTestId(`add-subnode-${group.id}-${group.index}`).should('exist'); - cy.getByTestId(`add-subnode-${group.id}-${group.index}`).click(); - - cy.getByTestId('nodes-list-header').contains(group.title).should('exist'); - // Add HTTP Request tool - nodeCreator.getters.getNthCreatorItem(2).click(); - getFloatingNodeByPosition('outputSub').should('exist'); - getFloatingNodeByPosition('outputSub').click({ force: true }); - - if (group.id === 'ai_languageModel') { - cy.getByTestId(`add-subnode-${group.id}-${group.index}`).should('not.exist'); - } else { - cy.getByTestId(`add-subnode-${group.id}-${group.index}`).should('exist'); - // Expand the subgroup - cy.getByTestId('subnode-connection-group-ai_tool-0').click(); - cy.getByTestId(`add-subnode-${group.id}-${group.index}`).click(); - // Add HTTP Request tool - nodeCreator.getters.getNthCreatorItem(2).click(); - getFloatingNodeByPosition('outputSub').click({ force: true }); - cy.getByTestId('subnode-connection-group-ai_tool-0') - .findChildByTestId('floating-subnode') - .should('have.length', 2); - } - }); - - // Since language model has no credentials set, it should show an error - // Since HTTP Request tool requires URL it would also show an error(2 errors, 1 for each tool node) - cy.get('[class*=hasIssues]').should('have.length', 3); - }); - - it('should have the floating nodes in correct order', () => { - cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes'); - - workflowPage.actions.deselectAll(); - - // The first merge node has the wires crossed, so `Edit Fields1` is first in the order of connected nodes - openNode('Merge'); - getFloatingNodeByPosition('inputMain').should('exist'); - getFloatingNodeByPosition('inputMain').should('have.length', 2); - getFloatingNodeByPosition('inputMain') - .first() - .should('have.attr', 'data-node-name', 'Edit Fields1'); - getFloatingNodeByPosition('inputMain') - .last() - .should('have.attr', 'data-node-name', 'Edit Fields0'); - - clickGetBackToCanvas(); - - // The second merge node does not have wires crossed, so `Edit Fields0` is first - openNode('Merge1'); - getFloatingNodeByPosition('inputMain').should('exist'); - getFloatingNodeByPosition('inputMain').should('have.length', 2); - getFloatingNodeByPosition('inputMain') - .first() - .should('have.attr', 'data-node-name', 'Edit Fields0'); - getFloatingNodeByPosition('inputMain') - .last() - .should('have.attr', 'data-node-name', 'Edit Fields1'); - }); - }); - - it('should show node name and version in settings', () => { - cy.createFixtureWorkflow('Test_workflow_ndv_version.json', 'NDV test version'); - - workflowPage.actions.openNode('Edit Fields (old)'); - ndv.actions.openSettings(); - ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.4)'); - ndv.actions.close(); - - workflowPage.actions.openNode('Edit Fields (latest)'); - ndv.actions.openSettings(); - ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.4 (Latest)'); - ndv.actions.close(); - - workflowPage.actions.openNode('Edit Fields (no typeVersion)'); - ndv.actions.openSettings(); - ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.4 (Latest)'); - ndv.actions.close(); - - workflowPage.actions.openNode('Function'); - ndv.actions.openSettings(); - ndv.getters.nodeVersion().should('have.text', 'Function node version 1 (Deprecated)'); - ndv.actions.close(); - }); - - it('Should render xml and html tags as strings and can search', () => { - cy.createFixtureWorkflow('Test_workflow_xml_output.json', 'test'); - - workflowPage.actions.executeWorkflow(); - - workflowPage.actions.openNode('Edit Fields'); - - ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table'); - - ndv.getters - .outputTableRow(1) - .should('include.text', ' '); - - cy.document().trigger('keyup', { key: '/' }); - ndv.getters.searchInput().filter(':focus').type(' Introduction to XML John Doe 2020 1234567890 Data Science Basics Jane Smith 2019 0987654321 Programming in Python Bob Johnson 2021 5432109876 "}]', - ); - ndv.getters.outputDataContainer().find('mark').should('have.text', ''); - }); - - it('should properly show node execution indicator', () => { - workflowPage.actions.addInitialCodeNodeToCanvas(); - // Should not show run info before execution - ndv.getters.nodeRunSuccessIndicator().should('not.exist'); - ndv.getters.nodeRunErrorIndicator().should('not.exist'); - ndv.getters.nodeRunTooltipIndicator().should('not.exist'); - ndv.getters.nodeExecuteButton().click(); - ndv.getters.nodeRunSuccessIndicator().should('exist'); - ndv.getters.nodeRunTooltipIndicator().should('exist'); - }); - - it('should properly show node execution indicator for multiple nodes', () => { - workflowPage.actions.addInitialCodeNodeToCanvas(); - ndv.actions.typeIntoParameterInput('jsCode', 'testets'); - ndv.getters.backToCanvas().click(); - workflowPage.actions.executeWorkflow(); - // Manual tigger node should show success indicator - workflowPage.actions.openNode('When clicking ‘Execute workflow’'); - ndv.getters.nodeRunSuccessIndicator().should('exist'); - ndv.getters.nodeRunTooltipIndicator().should('exist'); - // Code node should show error - ndv.getters.backToCanvas().click(); - workflowPage.actions.openNode('Code'); - ndv.getters.nodeRunErrorIndicator().should('exist'); - }); - - it('Should clear mismatched collection parameters', () => { - workflowPage.actions.addInitialNodeToCanvas('LDAP', { - keepNdvOpen: true, - action: 'Create a new entry', - }); - // Add some attributes in Create operation - cy.getByTestId('parameter-item').contains('Add Attributes').click(); - ndv.actions.changeNodeOperation('Update'); - // Attributes should be empty after operation change - cy.getByTestId('parameter-item').contains('Currently no items exist').should('exist'); - }); - - it('Should keep RLC values after operation change', () => { - const TEST_DOC_ID = '1111'; - workflowPage.actions.addInitialNodeToCanvas('Google Sheets', { - keepNdvOpen: true, - action: 'Append row in sheet', - }); - ndv.actions.setRLCValue('documentId', TEST_DOC_ID); - ndv.actions.changeNodeOperation('Update Row'); - ndv.getters.resourceLocatorInput('documentId').find('input').should('have.value', TEST_DOC_ID); - }); - - it('Should not clear resource/operation after credential change', () => { - workflowPage.actions.addInitialNodeToCanvas('Discord', { - keepNdvOpen: true, - action: 'Delete a message', - }); - - clickCreateNewCredential(); - setCredentialValues({ - botToken: 'sk_test_123', - }); - - ndv.getters.parameterInput('resource').find('input').should('have.value', 'Message'); - ndv.getters.parameterInput('operation').find('input').should('have.value', 'Delete'); - }); - - it('Should show a notice when remote options cannot be fetched because of missing credentials', () => { - cy.intercept('POST', '/rest/dynamic-node-parameters/options', { statusCode: 403 }).as( - 'parameterOptions', - ); - - workflowPage.actions.addInitialNodeToCanvas(NOTION_NODE_NAME, { - keepNdvOpen: true, - action: 'Update a database page', - }); - - ndv.actions.addItemToFixedCollection('propertiesUi'); - ndv.getters - .parameterInput('key') - .find('input') - .should('have.value', 'Set up credential to see options'); - }); - - it('Should show error state when remote options cannot be fetched', () => { - cy.intercept('POST', '/rest/dynamic-node-parameters/options', { statusCode: 500 }).as( - 'parameterOptions', - ); - - workflowPage.actions.addInitialNodeToCanvas(NOTION_NODE_NAME, { - keepNdvOpen: true, - action: 'Update a database page', - }); - - clickCreateNewCredential(); - setCredentialValues({ - apiKey: 'sk_test_123', - }); - - ndv.actions.addItemToFixedCollection('propertiesUi'); - ndv.getters - .parameterInput('key') - .find('input') - .should('have.value', 'Error fetching options from Notion'); - }); - - // Correctly failing in V2 - NodeCreator is not opened after clicking on the link - it('Should open appropriate node creator after clicking on connection hint link', () => { - const nodeCreator = new NodeCreator(); - const hintMapper = { - Memory: 'AI Nodes', - 'Output Parser': 'AI Nodes', - 'Token Splitter': 'Document Loaders', - Tool: 'AI Nodes', - Embeddings: 'Vector Stores', - 'Vector Store': 'Retrievers', - }; - cy.createFixtureWorkflow( - 'open_node_creator_for_connection.json', - 'open_node_creator_for_connection', - ); - - Object.entries(hintMapper).forEach(([node, group]) => { - workflowPage.actions.openNode(node); - // This fails to open the NodeCreator - cy.get('[data-action=openSelectiveNodeCreator]').contains('Insert one').click(); - nodeCreator.getters.activeSubcategory().should('contain', group); - cy.realPress('Escape'); - }); - }); - - it('should allow selecting item for expressions', () => { - workflowPage.actions.visit(); - - cy.createFixtureWorkflow('Test_workflow_3.json', 'My test workflow 2'); - workflowPage.actions.openNode('Set'); - - ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions - ndv.actions.typeIntoParameterInput('value', '{{', { - parseSpecialCharSequences: false, - }); - ndv.actions.typeIntoParameterInput('value', '$json.input[0].count'); - ndv.getters.inlineExpressionEditorOutput().should('have.text', '0'); - - ndv.actions.expressionSelectNextItem(); - ndv.getters.inlineExpressionEditorOutput().should('have.text', '1'); - ndv.getters.inlineExpressionEditorItemInput().should('have.value', '1'); - ndv.getters.inlineExpressionEditorItemNextButton().should('be.disabled'); - - ndv.actions.expressionSelectPrevItem(); - ndv.getters.inlineExpressionEditorOutput().should('have.text', '0'); - ndv.getters.inlineExpressionEditorItemInput().should('have.value', '0'); - ndv.getters.inlineExpressionEditorItemPrevButton().should('be.disabled'); - - ndv.actions.expressionSelectItem(1); - ndv.getters.inlineExpressionEditorOutput().should('have.text', '1'); - }); - - it('should show data from the correct output in schema view', () => { - cy.createFixtureWorkflow('Test_workflow_multiple_outputs.json'); - workflowPage.actions.zoomToFit(); - - workflowPage.actions.executeWorkflow(); - workflowPage.actions.openNode('Only Item 1'); - ndv.getters.inputPanel().should('be.visible'); - ndv.getters - .inputPanel() - .find('[data-test-id=run-data-schema-item]') - .should('contain.text', 'onlyOnItem1'); - ndv.actions.close(); - - workflowPage.actions.openNode('Only Item 2'); - ndv.getters.inputPanel().should('be.visible'); - ndv.getters - .inputPanel() - .find('[data-test-id=run-data-schema-item]') - .should('contain.text', 'onlyOnItem2'); - ndv.actions.close(); - - workflowPage.actions.openNode('Only Item 3'); - ndv.getters.inputPanel().should('be.visible'); - ndv.getters - .inputPanel() - .find('[data-test-id=run-data-schema-item]') - .should('contain.text', 'onlyOnItem3'); - }); - - it('should keep search expanded after Execute step node run', () => { - cy.createFixtureWorkflow('Test_ndv_search.json'); - workflowPage.actions.zoomToFit(); - workflowPage.actions.executeWorkflow(); - workflowPage.actions.openNode('Edit Fields'); - ndv.getters.outputPanel().should('be.visible'); - ndv.getters.outputPanel().findChildByTestId('ndv-search').click().type('US'); - ndv.getters.outputTableRow(1).find('mark').should('have.text', 'US'); - - ndv.actions.execute(); - ndv.getters - .outputPanel() - .findChildByTestId('ndv-search') - .should('be.visible') - .should('have.value', 'US'); - }); - - it('should not show items count when seaching in schema view', () => { - cy.createFixtureWorkflow('Test_ndv_search.json'); - workflowPage.actions.zoomToFit(); - workflowPage.actions.openNode('Edit Fields'); - ndv.getters.outputPanel().should('be.visible'); - ndv.actions.execute(); - ndv.actions.switchOutputMode('Schema'); - ndv.getters.outputPanel().find('[data-test-id=ndv-search]').click().type('US'); - ndv.getters.outputPanel().find('[data-test-id=ndv-items-count]').should('not.exist'); - }); - - it('should show additional tooltip when seaching in schema view if no matches', () => { - cy.createFixtureWorkflow('Test_ndv_search.json'); - workflowPage.actions.zoomToFit(); - workflowPage.actions.openNode('Edit Fields'); - ndv.getters.outputPanel().should('be.visible'); - ndv.actions.execute(); - ndv.actions.switchOutputMode('Schema'); - ndv.getters.outputPanel().find('[data-test-id=ndv-search]').click().type('foo'); - ndv.getters - .outputPanel() - .contains('To search field values, switch to table or JSON view.') - .should('exist'); - }); - - it('ADO-2931 - should handle multiple branches of the same input with the first branch empty correctly', () => { - cy.createFixtureWorkflow('Test_ndv_two_branches_of_same_parent_false_populated.json'); - workflowPage.actions.zoomToFit(); - workflowPage.actions.openNode('DebugHelper'); - ndv.getters.inputPanel().should('be.visible'); - ndv.getters.outputPanel().should('be.visible'); - ndv.actions.execute(); - // This ensures we rendered the inputPanel - ndv.getters - .inputPanel() - .find('[data-test-id=run-data-schema-item]') - .should('contain.text', 'a1'); - }); -}); diff --git a/packages/testing/playwright/fixtures/Dummy_javascript.txt b/packages/testing/playwright/fixtures/Dummy_javascript.txt new file mode 100644 index 0000000000..9ccfe6c5d1 --- /dev/null +++ b/packages/testing/playwright/fixtures/Dummy_javascript.txt @@ -0,0 +1,42 @@ +var File = function(url, object){ + File.list = Array.isArray(File.list)? File.list : []; + File.progress = File.progress || 0; + this.progress = 0; + this.object = object; + this.url = url; +}; + +File.indexOf = function(term){ + for(var index in File.list){ + var file = File.list[index]; + if (file.equals(term) || file.url === term || file.object === term) { + return index; + } + } + return -1; +}; + +File.find = function(term){ + var index = File.indexOf(term); + return ~index && File.list[index]; +}; + +File.prototype.equals = function(file){ + var isFileType = file instanceof File; + return isFileType && this.url === file.url && this.object === file.object; +}; + +File.prototype.save = function(update){ + update = typeof update === 'undefined'? true : update; + if(Array.isArray(File.list)){ + var index = File.indexOf(this); + if(~index && update) { + File.list[index] = this; + console.warn('File `%s` has been loaded before and updated now for: %O.', this.url, this); + }else File.list.push(this); + console.log(File.list) + }else{ + File.list = [this]; + } + return this; +}; diff --git a/packages/testing/playwright/fixtures/Test_workflow_schema_test.json b/packages/testing/playwright/fixtures/Test_workflow_schema_test.json new file mode 100644 index 0000000000..5eb5b8b4d2 --- /dev/null +++ b/packages/testing/playwright/fixtures/Test_workflow_schema_test.json @@ -0,0 +1,83 @@ +{ + "name": "My workflow 8", + "nodes": [ + { + "parameters": { + "operation": "getAllPeople", + "limit": 10 + }, + "id": "39cd80ce-5a8f-4339-b3d5-c4af969dd330", + "name": "Customer Datastore (n8n training)", + "type": "n8n-nodes-base.n8nTrainingCustomerDatastore", + "typeVersion": 1, + "position": [940, 680] + }, + { + "parameters": { + "values": { + "number": [ + { + "name": "objectValue.prop1", + "value": 123 + } + ], + "string": [ + { + "name": "objectValue.prop2", + "value": "someText" + } + ] + }, + "options": { + "dotNotation": true + } + }, + "id": "6e4490f6-ba95-4400-beec-2caefdd4895a", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [1300, 680] + }, + { + "parameters": {}, + "id": "58512a93-dabf-4584-817f-27c608c1bdd5", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [720, 680] + } + ], + "pinData": {}, + "connections": { + "Customer Datastore (n8n training)": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Customer Datastore (n8n training)", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "4a4f292a-92be-427c-848a-9582527f5ed3", + "id": "8", + "meta": { + "instanceId": "032eceae7493054b723340499be69ecbf4cbe28a7ec6df676b759000750b968d" + }, + "tags": [] +} diff --git a/packages/testing/playwright/pages/CanvasPage.ts b/packages/testing/playwright/pages/CanvasPage.ts index 985aeb1bd8..c7364d83ae 100644 --- a/packages/testing/playwright/pages/CanvasPage.ts +++ b/packages/testing/playwright/pages/CanvasPage.ts @@ -17,6 +17,19 @@ export class CanvasPage extends BasePage { return this.page.getByTestId('node-creator-item-name').getByText(subItemText, { exact: true }); } + getNthCreatorItem(index: number): Locator { + return this.page.getByTestId('node-creator-item').nth(index); + } + + getNodeCreatorHeader(text?: string) { + const header = this.page.getByTestId('nodes-list-header'); + return text ? header.filter({ hasText: text }) : header.first(); + } + + async selectNodeCreatorItemByText(nodeName: string) { + await this.page.getByText(nodeName).click(); + } + nodeByName(nodeName: string): Locator { return this.page.locator(`[data-test-id="canvas-node"][data-node-name="${nodeName}"]`); } @@ -546,6 +559,12 @@ export class CanvasPage extends BasePage { await this.getManualChatInput().fill(message); await this.getManualChatModal().locator('.chat-input-send-button').click(); } + /** + * Get all currently selected nodes on the canvas + */ + getSelectedNodes() { + return this.page.locator('[data-test-id="canvas-node"].selected'); + } async openExecutions() { await this.page.getByTestId('radio-button-executions').click(); diff --git a/packages/testing/playwright/pages/NodeDetailsViewPage.ts b/packages/testing/playwright/pages/NodeDetailsViewPage.ts index 788857b699..59ffa32084 100644 --- a/packages/testing/playwright/pages/NodeDetailsViewPage.ts +++ b/packages/testing/playwright/pages/NodeDetailsViewPage.ts @@ -23,11 +23,6 @@ export class NodeDetailsViewPage extends BasePage { return this.getContainer().locator('.parameter-item').filter({ hasText: labelName }); } - /** - * Fill a parameter input field - * @param labelName - The label of the parameter e.g URL - * @param value - The value to fill in the input field e.g https://foo.bar - */ async fillParameterInput(labelName: string, value: string) { await this.getParameterByLabel(labelName).getByTestId('parameter-input-field').fill(value); } @@ -119,21 +114,14 @@ export class NodeDetailsViewPage extends BasePage { return this.getOutputTableRow(row).locator('td').nth(col); } - /** - * Get a cell from the output table body, this doesn't include the header row - * @param row - The row index - * @param col - The column index - */ getOutputTbodyCell(row: number, col: number) { return this.getOutputTable().locator('tbody tr').nth(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(); @@ -150,7 +138,6 @@ export class NodeDetailsViewPage extends BasePage { 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); @@ -163,7 +150,6 @@ export class NodeDetailsViewPage extends BasePage { 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}`) @@ -192,6 +178,10 @@ export class NodeDetailsViewPage extends BasePage { return await this.page.request.get(path); } + getWebhookUrl() { + return this.page.locator('.webhook-url').textContent(); + } + getVisiblePoppers() { return this.page.locator('.el-popper:visible'); } @@ -206,104 +196,56 @@ export class NodeDetailsViewPage extends BasePage { 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) - * @param parameterName - The name of the parameter e.g 'jsCode', 'mode' - */ getParameterInput(parameterName: string) { return this.page.getByTestId(`parameter-input-${parameterName}`); } - /** - * Get parameter input field - * @param parameterName - The name of the parameter - */ getParameterInputField(parameterName: string) { return this.getParameterInput(parameterName).locator('input'); } - /** - * Select option in parameter dropdown (improved with Playwright best practices) - * @param parameterName - The parameter name - * @param optionText - The text of the option to select - */ async selectOptionInParameterDropdown(parameterName: string, optionText: string) { const dropdown = this.getParameterInput(parameterName); await dropdown.click(); - // Wait for dropdown to be visible and select option - following Playwright best practices await this.page.getByRole('option', { name: optionText }).click(); } - /** - * Click parameter dropdown by name (test-id based selector) - * @param parameterName - The parameter name e.g 'httpMethod', 'authentication' - */ async clickParameterDropdown(parameterName: string): Promise { await this.clickByTestId(`parameter-input-${parameterName}`); } - /** - * Select option from visible dropdown using Playwright role-based selectors - * This follows the pattern used in working n8n tests - * @param optionText - The text of the option to select - */ async selectFromVisibleDropdown(optionText: string): Promise { - // Use Playwright's role-based selector - this is more reliable than CSS selectors await this.page.getByRole('option', { name: optionText }).click(); } - /** - * Fill parameter input field by parameter name - * @param parameterName - The parameter name e.g 'path', 'url' - * @param value - The value to fill - */ async fillParameterInputByName(parameterName: string, value: string): Promise { const input = this.getParameterInputField(parameterName); await input.click(); await input.fill(value); } - /** - * Click parameter options expansion (e.g. for Response Code) - */ async clickParameterOptions(): Promise { await this.page.locator('.param-options').click(); } - /** - * Get visible Element UI popper (dropdown/popover) - * Ported from Cypress pattern with Playwright selectors - */ getVisiblePopper() { return this.page.locator('.el-popper:visible'); } - /** - * Wait for parameter dropdown to be visible and ready for interaction - * @param parameterName - The parameter name - */ async waitForParameterDropdown(parameterName: string): Promise { const dropdown = this.getParameterInput(parameterName); await dropdown.waitFor({ state: 'visible' }); await expect(dropdown).toBeEnabled(); } - /** - * Click on a floating node in the NDV (for switching between connected nodes) - * @param nodeName - The name of the node to click - */ async clickFloatingNode(nodeName: string) { await this.page.locator(`[data-test-id="floating-node"][data-node-name="${nodeName}"]`).click(); } - /** - * Execute the previous node (useful for providing input data) - */ async executePrevious() { await this.clickByTestId('execute-previous-node'); } @@ -378,9 +320,14 @@ export class NodeDetailsViewPage extends BasePage { async setParameterDropdown(parameterName: string, optionText: string): Promise { await this.getParameterInput(parameterName).click(); + await this.page.getByRole('option', { name: optionText }).click(); } + async changeNodeOperation(operationName: string): Promise { + await this.setParameterDropdown('operation', operationName); + } + async setParameterInput(parameterName: string, value: string): Promise { await this.fillParameterInputByName(parameterName, value); } @@ -423,31 +370,21 @@ export class NodeDetailsViewPage extends BasePage { case 'switch': return await this.getSwitchParameterValue(parameterName); default: - // Fallback for unknown types return (await this.getParameterInput(parameterName).textContent()) ?? ''; } } - /** - * Get value from a text parameter - simplified approach - */ private async getTextParameterValue(parameterName: string): Promise { const parameterContainer = this.getParameterInput(parameterName); const input = parameterContainer.locator('input').first(); return await input.inputValue(); } - /** - * Get value from a dropdown parameter - */ private async getDropdownParameterValue(parameterName: string): Promise { const selectedOption = this.getParameterInput(parameterName).locator('.el-select__tags-text'); return (await selectedOption.textContent()) ?? ''; } - /** - * Get value from a switch parameter - */ private async getSwitchParameterValue(parameterName: string): Promise { const switchElement = this.getParameterInput(parameterName).locator('.el-switch'); const isEnabled = (await switchElement.getAttribute('aria-checked')) === 'true'; @@ -503,8 +440,12 @@ export class NodeDetailsViewPage extends BasePage { .first(); } + getNodeInputOptions() { + return this.getInputPanel().getByTestId('ndv-input-select'); + } + async selectInputNode(nodeName: string) { - const inputSelect = this.getInputPanel().getByTestId('ndv-input-select'); + const inputSelect = this.getNodeInputOptions(); await inputSelect.click(); await this.page.getByRole('option', { name: nodeName }).click(); } @@ -572,6 +513,35 @@ export class NodeDetailsViewPage extends BasePage { return this.getInlineExpressionEditorInput().locator('.cm-content'); } + getInlineExpressionEditorOutput() { + return this.page.getByTestId('inline-expression-editor-output'); + } + + getInlineExpressionEditorItemInput() { + return this.page.getByTestId('inline-expression-editor-item-input').locator('input'); + } + + getInlineExpressionEditorItemPrevButton() { + return this.page.getByTestId('inline-expression-editor-item-prev'); + } + + getInlineExpressionEditorItemNextButton() { + return this.page.getByTestId('inline-expression-editor-item-next'); + } + + async expressionSelectNextItem() { + await this.getInlineExpressionEditorItemNextButton().click(); + } + + async expressionSelectPrevItem() { + await this.getInlineExpressionEditorItemPrevButton().click(); + } + + async typeIntoParameterInput(parameterName: string, content: string): Promise { + const input = this.getParameterInput(parameterName); + await input.type(content); + } + getInputTable() { return this.getInputPanel().locator('table'); } @@ -587,12 +557,10 @@ export class NodeDetailsViewPage extends BasePage { async toggleCodeMode(switchTo: 'Run Once for Each Item' | 'Run Once for All Items') { await this.getParameterInput('mode').click(); await this.page.getByRole('option', { name: switchTo }).click(); - // This is a workaround to wait for the code editor to reinitialize after the mode switch // eslint-disable-next-line playwright/no-wait-for-timeout await this.page.waitForTimeout(2500); } - // Pagination methods for output panel getOutputPagination() { return this.getOutputPanel().getByTestId('ndv-data-pagination'); } @@ -616,17 +584,297 @@ export class NodeDetailsViewPage extends BasePage { return (await this.getOutputTbodyCell(row, col).textContent()) ?? ''; } - /** - * Set parameter input value by clearing and filling (for parameters without standard test-id) - * @param parameterName - The parameter name - * @param value - The value to set - */ async setParameterInputValue(parameterName: string, value: string): Promise { const input = this.getParameterInput(parameterName).locator('input'); await input.clear(); await input.fill(value); } + getNodeRunErrorMessage() { + return this.page.getByTestId('node-error-message'); + } + + getNodeRunErrorDescription() { + return this.page.getByTestId('node-error-description'); + } + + getInputRunSelector() { + return this.getInputPanel().getByTestId('run-selector'); + } + + getOutputRunSelector() { + return this.getOutputPanel().getByTestId('run-selector'); + } + + async toggleOutputRunLinking() { + await this.getOutputPanel().getByTestId('link-run').click(); + } + + async toggleInputRunLinking() { + await this.getInputPanel().getByTestId('link-run').click(); + } + + getOutputLinkRun() { + return this.getOutputPanel().getByTestId('link-run'); + } + + getInputLinkRun() { + return this.getInputPanel().getByTestId('link-run'); + } + + async isOutputRunLinkingEnabled() { + const linkButton = this.getOutputLinkRun(); + const classList = await linkButton.getAttribute('class'); + return classList?.includes('linked') ?? false; + } + + async ensureOutputRunLinking(shouldBeLinked: boolean = true) { + const isLinked = await this.isOutputRunLinkingEnabled(); + if (isLinked !== shouldBeLinked) { + await this.toggleOutputRunLinking(); + } + } + + async changeInputRunSelector(value: string) { + const selector = this.getInputRunSelector(); + await selector.click(); + await this.page.getByRole('option', { name: value }).click(); + } + + async changeOutputRunSelector(value: string) { + const selector = this.getOutputRunSelector(); + await selector.click(); + await this.page.getByRole('option', { name: value }).click(); + } + + async getInputRunSelectorValue() { + return await this.getInputRunSelector().locator('input').inputValue(); + } + + async getOutputRunSelectorValue() { + return await this.getOutputRunSelector().locator('input').inputValue(); + } + + getOutputDisplayMode() { + return this.getOutputPanel().getByTestId('ndv-output-display-mode'); + } + + getSchemaViewItems() { + return this.getOutputPanel().locator('[data-test-id="run-data-schema-item"]'); + } + + getSchemaItem(key: string) { + return this.getSchemaViewItems().filter({ hasText: key }); + } + + async expandSchemaItem(itemText: string) { + const item = this.getSchemaItem(itemText); + await item.locator('.toggle').click(); + } + + getPaginationContainer() { + return this.getOutputPanel().locator('[class*="_pagination"]'); + } + + getExecuteNodeButton() { + return this.page.getByTestId('node-execute-button'); + } + + getTriggerPanelExecuteButton() { + return this.page.getByTestId('trigger-execute-button'); + } + + async openCodeEditorFullscreen() { + await this.page.getByTestId('code-editor-fullscreen-button').click(); + } + + getCodeEditorFullscreen() { + return this.page.getByTestId('code-editor-fullscreen').locator('.cm-content'); + } + + getCodeEditorDialog() { + return this.page.locator('.el-dialog'); + } + + async closeCodeEditorDialog() { + await this.getCodeEditorDialog().locator('.el-dialog__close').click(); + } + + getWebhookTriggerListening() { + return this.page.getByTestId('trigger-listening'); + } + + getNodeRunSuccessIndicator() { + return this.page.getByTestId('node-run-status-success'); + } + + getNodeRunErrorIndicator() { + return this.page.getByTestId('node-run-status-danger'); + } + + getNodeRunTooltipIndicator() { + return this.page.getByTestId('node-run-info'); + } + + async openSettings() { + await this.page.getByTestId('tab-settings').click(); + } + + getNodeVersion() { + return this.page.getByTestId('node-version'); + } + + getOutputSearchInput() { + return this.getOutputPanel().getByTestId('ndv-search'); + } + + getInputSearchInput() { + return this.getInputPanel().getByTestId('ndv-search'); + } + + async searchOutputData(searchTerm: string) { + const searchInput = this.getOutputSearchInput(); + await searchInput.click(); + await searchInput.fill(searchTerm); + } + + async searchInputData(searchTerm: string) { + const searchInput = this.getInputSearchInput(); + await searchInput.click(); + await searchInput.fill(searchTerm); + } + + getOutputItemsCount() { + return this.getOutputPanel().getByTestId('ndv-items-count'); + } + + getInputItemsCount() { + return this.getInputPanel().getByTestId('ndv-items-count'); + } + + /** + * Type multiple values into the first available text parameter field + * Useful for testing multiple parameter changes + */ + async fillFirstAvailableTextParameterMultipleTimes(values: string[]) { + const firstTextField = this.getNodeParameters().locator('input[type="text"]').first(); + await firstTextField.click(); + + for (const value of values) { + await firstTextField.fill(value); + } + } + + getFloatingNodeByPosition(position: 'inputMain' | 'outputMain' | 'inputSub' | 'outputSub') { + return this.page.locator(`[data-node-placement="${position}"]`); + } + + getNodeNameContainer() { + return this.getContainer().getByTestId('node-title-container'); + } + + async clickFloatingNodeByPosition( + position: 'inputMain' | 'outputMain' | 'inputSub' | 'outputSub', + ) { + // eslint-disable-next-line playwright/no-force-option + await this.getFloatingNodeByPosition(position).click({ force: true }); + } + + async navigateToNextFloatingNodeWithKeyboard() { + await this.page.keyboard.press('Shift+Meta+Alt+ArrowRight'); + } + + async navigateToPreviousFloatingNodeWithKeyboard() { + await this.page.keyboard.press('Shift+Meta+Alt+ArrowLeft'); + } + + async verifyFloatingNodeName( + position: 'inputMain' | 'outputMain' | 'inputSub' | 'outputSub', + nodeName: string, + index: number = 0, + ) { + const floatingNode = this.getFloatingNodeByPosition(position).nth(index); + await expect(floatingNode).toHaveAttribute('data-node-name', nodeName); + } + + async getFloatingNodeCount(position: 'inputMain' | 'outputMain' | 'inputSub' | 'outputSub') { + return await this.getFloatingNodeByPosition(position).count(); + } + + getAddSubNodeButton(connectionType: string, index: number = 0) { + return this.page.getByTestId(`add-subnode-${connectionType}-${index}`); + } + + getSubNodeConnectionGroup(connectionType: string, index: number = 0) { + return this.page.getByTestId(`subnode-connection-group-${connectionType}-${index}`); + } + + getFloatingSubNodes(connectionType: string, index: number = 0) { + return this.getSubNodeConnectionGroup(connectionType, index).getByTestId('floating-subnode'); + } + + getNodesWithIssues() { + return this.page.locator('[class*="hasIssues"]'); + } + + async connectAISubNode(connectionType: string, nodeName: string, index: number = 0) { + await this.getAddSubNodeButton(connectionType, index).click(); + await this.page.getByText(nodeName).click(); + await this.getFloatingNode().click(); + } + + getFloatingNode() { + return this.page.getByTestId('floating-node'); + } + + async getNodesWithIssuesCount() { + return await this.getNodesWithIssues().count(); + } + + async addItemToFixedCollection(collectionName: string) { + await this.page.getByTestId(`fixed-collection-${collectionName}`).click(); + } + + async clickParameterItemAction(actionText: string) { + await this.page.getByTestId('parameter-item').getByText(actionText).click(); + } + + getParameterItemWithText(text: string) { + return this.page.getByTestId('parameter-item').getByText(text); + } + + getParameterInputWithIssues(parameterPath: string) { + return this.page.locator( + `[data-test-id="parameter-input-field"][title*="${parameterPath}"][title*="has issues"]`, + ); + } + + getResourceLocator(paramName: string) { + return this.page.getByTestId(`resource-locator-${paramName}`); + } + + getResourceLocatorInput(paramName: string) { + return this.getResourceLocator(paramName).getByTestId('rlc-input-container'); + } + + getResourceLocatorModeSelector(paramName: string) { + return this.getResourceLocator(paramName).getByTestId('rlc-mode-selector'); + } + + async setRLCValue(paramName: string, value: string): Promise { + await this.getResourceLocatorModeSelector(paramName).click(); + + const visibleOptions = this.page.locator('.el-popper:visible .el-select-dropdown__item'); + await visibleOptions.last().click(); + + const input = this.getResourceLocatorInput(paramName).locator('input'); + await input.fill(value); + } + + async clickNodeCreatorInsertOneButton() { + await this.page.getByText('Insert one').click(); + } + getInputSelect() { return this.page.getByTestId('ndv-input-select').locator('input'); } diff --git a/packages/testing/playwright/tests/ui/1-workflows.spec.ts b/packages/testing/playwright/tests/ui/1-workflows.spec.ts index 439e0498d2..99f64c05d8 100644 --- a/packages/testing/playwright/tests/ui/1-workflows.spec.ts +++ b/packages/testing/playwright/tests/ui/1-workflows.spec.ts @@ -52,7 +52,6 @@ test.describe('Workflows', () => { // Search for specific workflow await n8n.workflows.searchWorkflows(specificName); - await expect(n8n.workflows.getWorkflowItems()).toHaveCount(1); await expect(n8n.workflows.getWorkflowByName(specificName)).toBeVisible(); // Search with partial term diff --git a/packages/testing/playwright/tests/ui/5-ndv.spec.ts b/packages/testing/playwright/tests/ui/5-ndv.spec.ts new file mode 100644 index 0000000000..7ac9e01686 --- /dev/null +++ b/packages/testing/playwright/tests/ui/5-ndv.spec.ts @@ -0,0 +1,800 @@ +import { + CODE_NODE_NAME, + CODE_NODE_DISPLAY_NAME, + MANUAL_TRIGGER_NODE_DISPLAY_NAME, +} from '../../config/constants'; +import { test, expect } from '../../fixtures/base'; +import type { n8nPage } from '../../pages/n8nPage'; + +test.describe('NDV', () => { + test.beforeEach(async ({ n8n }) => { + await n8n.start.fromBlankCanvas(); + }); + + test('should show up when double clicked on a node and close when Back to canvas clicked', async ({ + n8n, + }) => { + await n8n.canvas.addNode('Manual Trigger'); + const canvasNodes = n8n.canvas.getCanvasNodes(); + await canvasNodes.first().dblclick(); + await expect(n8n.ndv.getContainer()).toBeVisible(); + await n8n.ndv.clickBackToCanvasButton(); + await expect(n8n.ndv.getContainer()).toBeHidden(); + }); + + test('should show input panel when node is not connected', async ({ n8n }) => { + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.deselectAll(); + await n8n.canvas.addNode('Edit Fields (Set)', { closeNDV: true }); + const canvasNodes = n8n.canvas.getCanvasNodes(); + await canvasNodes.last().dblclick(); + await expect(n8n.ndv.getContainer()).toBeVisible(); + await expect(n8n.ndv.getInputPanel()).toContainText('Wire me up'); + }); + + test('should test webhook node', async ({ n8n }) => { + await n8n.canvas.addNode('Webhook', { closeNDV: false }); + + await n8n.ndv.execute(); + + const webhookUrl = await n8n.ndv.getWebhookUrl(); + await expect(n8n.ndv.getWebhookTriggerListening()).toBeVisible(); + const response = await n8n.ndv.makeWebhookRequest(webhookUrl as string); + expect(response.status()).toBe(200); + + await expect(n8n.ndv.getOutputPanel()).toBeVisible(); + await expect(n8n.ndv.getOutputDataContainer()).toBeVisible(); + }); + + test('should change input and go back to canvas', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('NDV-test-select-input.json'); + await n8n.canvas.clickZoomToFitButton(); + await n8n.canvas.getCanvasNodes().last().dblclick(); + await n8n.ndv.execute(); + + await n8n.ndv.switchInputMode('Table'); + + await n8n.ndv.getNodeInputOptions().last().click(); + + await expect(n8n.ndv.getInputPanel()).toContainText('start'); + + await n8n.ndv.clickBackToCanvasButton(); + await expect(n8n.ndv.getContainer()).toBeHidden(); + }); + + test('should show correct validation state for resource locator params', async ({ n8n }) => { + await n8n.canvas.addNode('Typeform Trigger', { closeNDV: false }); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await n8n.ndv.clickBackToCanvasButton(); + + await n8n.canvas.openNode('Typeform Trigger'); + await expect(n8n.canvas.getNodeIssuesByName('Typeform Trigger')).toBeVisible(); + }); + + test('should show validation errors only after blur or re-opening of NDV', async ({ n8n }) => { + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.addNode('Airtable', { closeNDV: false, action: 'Search records' }); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await expect(n8n.canvas.getNodeIssuesByName('Airtable')).toBeHidden(); + + await n8n.ndv.getParameterInputField('table').nth(1).focus(); + await n8n.ndv.getParameterInputField('table').nth(1).blur(); + await n8n.ndv.getParameterInputField('base').nth(1).focus(); + await n8n.ndv.getParameterInputField('base').nth(1).blur(); + + await expect(n8n.ndv.getParameterInput('base')).toHaveClass(/has-issues|error|invalid/); + await expect(n8n.ndv.getParameterInput('table')).toHaveClass(/has-issues|error|invalid/); + + await n8n.ndv.clickBackToCanvasButton(); + + await n8n.canvas.openNode('Search records'); + await expect(n8n.canvas.getNodeIssuesByName('Search records')).toBeVisible(); + }); + + test('should show all validation errors when opening pasted node', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_ndv_errors.json'); + const canvasNodes = n8n.canvas.getCanvasNodes(); + await expect(canvasNodes).toHaveCount(1); + + await n8n.canvas.openNode('Airtable'); + await expect(n8n.canvas.getNodeIssuesByName('Airtable')).toBeVisible(); + }); + + test('should render run errors correctly', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_ndv_run_error.json'); + await n8n.canvas.openNode('Error'); + await n8n.ndv.execute(); + + await expect(n8n.ndv.getNodeRunErrorMessage()).toHaveText( + "Paired item data for item from node 'Break pairedItem chain' is unavailable. Ensure 'Break pairedItem chain' is providing the required output.", + ); + + await expect(n8n.ndv.getNodeRunErrorDescription()).toContainText( + "An expression here won't work because it uses .item and n8n can't figure out the matching item.", + ); + + await expect(n8n.ndv.getNodeRunErrorMessage()).toBeVisible(); + await expect(n8n.ndv.getNodeRunErrorDescription()).toBeVisible(); + }); + + test('should save workflow using keyboard shortcut from NDV', async ({ n8n }) => { + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.addNode('Edit Fields (Set)', { closeNDV: false }); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await n8n.page.keyboard.press('ControlOrMeta+s'); + + await expect(n8n.canvas.getWorkflowSaveButton()).toBeHidden(); + }); + + test('webhook should fallback to webhookId if path is empty', async ({ n8n }) => { + await n8n.canvas.addNode('Webhook', { closeNDV: false }); + + await expect(n8n.canvas.getNodeIssuesByName('Webhook')).toBeHidden(); + await expect(n8n.ndv.getExecuteNodeButton()).toBeEnabled(); + await expect(n8n.ndv.getTriggerPanelExecuteButton()).toBeVisible(); + + await n8n.ndv.getParameterInputField('path').clear(); + + const webhookUrlsContainer = n8n.ndv.getContainer().getByText('Webhook URLs').locator('..'); + const urlText = await webhookUrlsContainer.textContent(); + const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i; + expect(urlText).toMatch(uuidRegex); + + await n8n.ndv.close(); + + await n8n.canvas.openNode('Webhook'); + await n8n.ndv.fillParameterInput('path', 'test-path'); + + const updatedUrlText = await webhookUrlsContainer.textContent(); + expect(updatedUrlText).toContain('test-path'); + expect(updatedUrlText).not.toMatch(uuidRegex); + }); + + test.describe('test output schema view', () => { + const schemaKeys = [ + 'id', + 'name', + 'email', + 'notes', + 'country', + 'created', + 'objectValue', + 'prop1', + 'prop2', + ]; + + const setupSchemaWorkflow = async (n8n: n8nPage) => { + await n8n.start.fromImportedWorkflow('Test_workflow_schema_test.json'); + await n8n.canvas.clickZoomToFitButton(); + await n8n.canvas.openNode('Set'); + await n8n.ndv.execute(); + }; + + test('should switch to output schema view and validate it', async ({ n8n }) => { + await setupSchemaWorkflow(n8n); + await n8n.ndv.switchOutputMode('Schema'); + + for (const key of schemaKeys) { + await expect(n8n.ndv.getSchemaViewItems().filter({ hasText: key })).toBeVisible(); + } + }); + + test('should preserve schema view after execution', async ({ n8n }) => { + await setupSchemaWorkflow(n8n); + await n8n.ndv.switchOutputMode('Schema'); + await n8n.ndv.execute(); + + for (const key of schemaKeys) { + await expect(n8n.ndv.getSchemaViewItems().filter({ hasText: key })).toBeVisible(); + } + }); + + test('should collapse and expand nested schema object', async ({ n8n }) => { + await setupSchemaWorkflow(n8n); + const expandedObjectProps = ['prop1', 'prop2']; + + await n8n.ndv.switchOutputMode('Schema'); + + for (const key of expandedObjectProps) { + await expect(n8n.ndv.getSchemaViewItems().filter({ hasText: key })).toBeVisible(); + } + + const objectValueItem = n8n.ndv.getSchemaViewItems().filter({ hasText: 'objectValue' }); + await objectValueItem.locator('.toggle').click(); + + for (const key of expandedObjectProps) { + await expect(n8n.ndv.getSchemaViewItems().filter({ hasText: key })).not.toBeInViewport(); + } + }); + + test('should not display pagination for schema', async ({ n8n }) => { + await setupSchemaWorkflow(n8n); + + await n8n.ndv.clickBackToCanvasButton(); + await n8n.canvas.deselectAll(); + await n8n.canvas.nodeByName('Set').click(); + await n8n.canvas.addNode('Customer Datastore (n8n training)'); + + await n8n.canvas.openNode('Customer Datastore (n8n training)'); + + await n8n.ndv.execute(); + + await expect(n8n.ndv.getOutputPanel().getByText('5 items')).toBeVisible(); + + await n8n.ndv.switchOutputMode('Schema'); + + const schemaItemsCount = await n8n.ndv.getSchemaViewItems().count(); + expect(schemaItemsCount).toBeGreaterThan(0); + + await n8n.ndv.switchOutputMode('JSON'); + }); + + test('should display large schema', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_schema_test_pinned_data.json'); + await n8n.canvas.clickZoomToFitButton(); + await n8n.canvas.openNode('Set'); + + await expect(n8n.ndv.getOutputPanel().getByText('20 items')).toBeVisible(); + await expect(n8n.ndv.getOutputPanel().locator('[class*="_pagination"]')).toBeVisible(); + + await n8n.ndv.switchOutputMode('Schema'); + + await expect(n8n.ndv.getOutputPanel().locator('[class*="_pagination"]')).toBeHidden(); + }); + }); + + test('should display parameter hints correctly', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_3.json'); + await n8n.canvas.openNode('Set1'); + + await n8n.ndv.getParameterInputField('value').clear(); + await n8n.ndv.getParameterInputField('value').fill('='); + + await n8n.ndv.getInlineExpressionEditorContent().fill('hello'); + await n8n.ndv.getParameterInputField('name').click(); + await expect(n8n.ndv.getParameterExpressionPreviewValue()).toContainText('hello'); + + await n8n.ndv.getInlineExpressionEditorContent().fill(''); + await n8n.ndv.getParameterInputField('name').click(); + await expect(n8n.ndv.getParameterExpressionPreviewValue()).toContainText('[empty]'); + + await n8n.ndv.getInlineExpressionEditorContent().fill(' test'); + await n8n.ndv.getParameterInputField('name').click(); + await expect(n8n.ndv.getParameterExpressionPreviewValue()).toContainText(' test'); + + await n8n.ndv.getInlineExpressionEditorContent().fill(' '); + await n8n.ndv.getParameterInputField('name').click(); + await expect(n8n.ndv.getParameterExpressionPreviewValue()).toContainText(' '); + + await n8n.ndv.getInlineExpressionEditorContent().fill('
'); + await n8n.ndv.getParameterInputField('name').click(); + await expect(n8n.ndv.getParameterExpressionPreviewValue()).toContainText('
'); + }); + + test('should properly show node execution indicator', async ({ n8n }) => { + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.addNode('Code', { action: 'Code in JavaScript', closeNDV: false }); + + await expect(n8n.ndv.getNodeRunSuccessIndicator()).toBeHidden(); + await expect(n8n.ndv.getNodeRunErrorIndicator()).toBeHidden(); + await expect(n8n.ndv.getNodeRunTooltipIndicator()).toBeHidden(); + + await n8n.ndv.execute(); + + await expect(n8n.ndv.getNodeRunSuccessIndicator()).toBeVisible(); + await expect(n8n.ndv.getNodeRunTooltipIndicator()).toBeVisible(); + }); + + test('should show node name and version in settings', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_ndv_version.json'); + + await n8n.canvas.openNode('Edit Fields (old)'); + await n8n.ndv.openSettings(); + await expect(n8n.ndv.getNodeVersion()).toContainText('Set node version 2'); + await expect(n8n.ndv.getNodeVersion()).toContainText('Latest version: 3.4'); + await n8n.ndv.close(); + + await n8n.canvas.openNode('Edit Fields (latest)'); + await n8n.ndv.openSettings(); + await expect(n8n.ndv.getNodeVersion()).toContainText('Edit Fields (Set) node version 3.4'); + await expect(n8n.ndv.getNodeVersion()).toContainText('Latest'); + await n8n.ndv.close(); + + await n8n.canvas.openNode('Function'); + await n8n.ndv.openSettings(); + await expect(n8n.ndv.getNodeVersion()).toContainText('Function node version 1'); + await expect(n8n.ndv.getNodeVersion()).toContainText('Deprecated'); + await n8n.ndv.close(); + }); + + test('should not push NDV header out with a lot of code in Code node editor', async ({ n8n }) => { + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.addNode('Code', { action: 'Code in JavaScript', closeNDV: false }); + + const codeEditor = n8n.ndv.getParameterInput('jsCode').locator('.cm-content'); + await codeEditor.click(); + await n8n.page.keyboard.press('ControlOrMeta+a'); + await n8n.page.keyboard.press('Delete'); + + const dummyCode = Array(50) + .fill( + 'console.log("This is a very long line of dummy JavaScript code that should not push the NDV header out of view");', + ) + .join('\n'); + + await codeEditor.fill(dummyCode); + + await expect(n8n.ndv.getExecuteNodeButton()).toBeVisible(); + }); + + test('should allow editing code in fullscreen in the code editors', async ({ n8n }) => { + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.addNode('Code', { action: 'Code in JavaScript', closeNDV: false }); + + await n8n.ndv.openCodeEditorFullscreen(); + + const fullscreenEditor = n8n.ndv.getCodeEditorFullscreen(); + await fullscreenEditor.click(); + await n8n.page.keyboard.press('ControlOrMeta+a'); + await fullscreenEditor.fill('foo()'); + + await expect(fullscreenEditor).toContainText('foo()'); + + await n8n.ndv.closeCodeEditorDialog(); + + await expect(n8n.ndv.getParameterInput('jsCode').locator('.cm-content')).toContainText('foo()'); + }); + + test('should keep search expanded after Execute step node run', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_ndv_search.json'); + await n8n.canvas.clickZoomToFitButton(); + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + + await n8n.canvas.openNode('Edit Fields'); + await expect(n8n.ndv.getOutputPanel()).toBeVisible(); + + await n8n.ndv.searchOutputData('US'); + + await expect(n8n.ndv.getOutputTableRow(1).locator('mark')).toContainText('US'); + + await n8n.ndv.execute(); + + await expect(n8n.ndv.getOutputSearchInput()).toBeVisible(); + await expect(n8n.ndv.getOutputSearchInput()).toHaveValue('US'); + }); + + test('Should render xml and html tags as strings and can search', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_xml_output.json'); + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + await n8n.canvas.openNode('Edit Fields'); + + await expect(n8n.ndv.getOutputPanel().locator('[class*="active"]')).toContainText('Table'); + + await expect(n8n.ndv.getOutputTableRow(1)).toContainText( + ' ', + ); + + await n8n.page.keyboard.press('/'); + + const searchInput = n8n.ndv.getOutputSearchInput(); + await expect(searchInput).toBeFocused(); + await searchInput.fill(' { + test('can link and unlink run selectors between input and output', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Test_workflow_5.json'); + await n8n.canvas.clickZoomToFitButton(); + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + await n8n.canvas.openNode('Set3'); + + await n8n.ndv.switchInputMode('Table'); + await n8n.ndv.switchOutputMode('Table'); + + await n8n.ndv.ensureOutputRunLinking(true); + await n8n.ndv.getInputTbodyCell(0, 0).click(); + expect(await n8n.ndv.getInputRunSelectorValue()).toContain('2 of 2 (6 items)'); + expect(await n8n.ndv.getOutputRunSelectorValue()).toContain('2 of 2 (6 items)'); + + await n8n.ndv.changeOutputRunSelector('1 of 2 (6 items)'); + expect(await n8n.ndv.getInputRunSelectorValue()).toContain('1 of 2 (6 items)'); + await expect(n8n.ndv.getInputTbodyCell(0, 0)).toHaveText('1111'); + await expect(n8n.ndv.getOutputTbodyCell(0, 0)).toHaveText('1111'); + + await n8n.ndv.getInputTbodyCell(0, 0).click(); + await n8n.ndv.changeInputRunSelector('2 of 2 (6 items)'); + expect(await n8n.ndv.getOutputRunSelectorValue()).toContain('2 of 2 (6 items)'); + + await n8n.ndv.toggleOutputRunLinking(); + await n8n.ndv.getInputTbodyCell(0, 0).click(); + await n8n.ndv.changeOutputRunSelector('1 of 2 (6 items)'); + expect(await n8n.ndv.getInputRunSelectorValue()).toContain('2 of 2 (6 items)'); + + await n8n.ndv.toggleOutputRunLinking(); + await n8n.ndv.getInputTbodyCell(0, 0).click(); + expect(await n8n.ndv.getInputRunSelectorValue()).toContain('1 of 2 (6 items)'); + + await n8n.ndv.toggleInputRunLinking(); + await n8n.ndv.getInputTbodyCell(0, 0).click(); + await n8n.ndv.changeInputRunSelector('2 of 2 (6 items)'); + expect(await n8n.ndv.getOutputRunSelectorValue()).toContain('1 of 2 (6 items)'); + + await n8n.ndv.toggleInputRunLinking(); + await n8n.ndv.getInputTbodyCell(0, 0).click(); + expect(await n8n.ndv.getOutputRunSelectorValue()).toContain('2 of 2 (6 items)'); + }); + }); + + test.describe('Remote Options & Network', () => { + test('should not retrieve remote options when a parameter value changes', async ({ n8n }) => { + let fetchParameterOptionsCallCount = 0; + await n8n.page.route('**/rest/dynamic-node-parameters/options', async (route) => { + fetchParameterOptionsCallCount++; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: [] }), + }); + }); + + await n8n.canvas.addNode('E2E Test', { action: 'Remote Options' }); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await n8n.ndv.fillFirstAvailableTextParameterMultipleTimes(['test1', 'test2', 'test3']); + + expect(fetchParameterOptionsCallCount).toBe(1); + }); + + test('Should show a notice when remote options cannot be fetched because of missing credentials', async ({ + n8n, + }) => { + await n8n.page.route('**/rest/dynamic-node-parameters/options', async (route) => { + await route.fulfill({ status: 403 }); + }); + + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.addNode('Notion', { action: 'Update a database page', closeNDV: false }); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await n8n.ndv.addItemToFixedCollection('propertiesUi'); + await expect( + n8n.ndv.getParameterInputWithIssues('propertiesUi.propertyValues[0].key'), + ).toBeVisible(); + }); + + test('Should show error state when remote options cannot be fetched', async ({ n8n }) => { + await n8n.page.route('**/rest/dynamic-node-parameters/options', async (route) => { + await route.fulfill({ status: 500 }); + }); + + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.addNode('Notion', { action: 'Update a database page', closeNDV: false }); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await n8n.credentials.createAndSaveNewCredential('apiKey', 'sk_test_123'); + await n8n.ndv.addItemToFixedCollection('propertiesUi'); + await expect( + n8n.ndv.getParameterInputWithIssues('propertiesUi.propertyValues[0].key'), + ).toBeVisible(); + }); + }); + + test.describe('Floating Nodes Navigation', () => { + test('should traverse floating nodes with mouse', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Floating_Nodes.json'); + await n8n.canvas.getCanvasNodes().first().dblclick(); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await expect(n8n.ndv.getFloatingNodeByPosition('inputMain')).toBeHidden(); + await expect(n8n.ndv.getFloatingNodeByPosition('outputMain')).toBeVisible(); + for (let i = 0; i < 4; i++) { + await n8n.ndv.clickFloatingNodeByPosition('outputMain'); + await expect(n8n.ndv.getFloatingNodeByPosition('inputMain')).toBeVisible(); + await expect(n8n.ndv.getFloatingNodeByPosition('outputMain')).toBeVisible(); + + await n8n.ndv.close(); + await expect(n8n.canvas.getSelectedNodes()).toHaveCount(1); + + await n8n.canvas.getSelectedNodes().first().dblclick(); + await expect(n8n.ndv.getContainer()).toBeVisible(); + } + + await n8n.ndv.clickFloatingNodeByPosition('outputMain'); + await expect(n8n.ndv.getFloatingNodeByPosition('inputMain')).toBeVisible(); + + for (let i = 0; i < 4; i++) { + await n8n.ndv.clickFloatingNodeByPosition('inputMain'); + await expect(n8n.ndv.getFloatingNodeByPosition('outputMain')).toBeVisible(); + await expect(n8n.ndv.getFloatingNodeByPosition('inputMain')).toBeVisible(); + } + + await n8n.ndv.clickFloatingNodeByPosition('inputMain'); + await expect(n8n.ndv.getFloatingNodeByPosition('inputMain')).toBeHidden(); + await expect(n8n.ndv.getFloatingNodeByPosition('inputSub')).toBeHidden(); + await expect(n8n.ndv.getFloatingNodeByPosition('outputSub')).toBeHidden(); + + await n8n.ndv.close(); + await expect(n8n.canvas.getSelectedNodes()).toHaveCount(1); + }); + + test('should traverse floating nodes with keyboard', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Floating_Nodes.json'); + + await n8n.canvas.getCanvasNodes().first().dblclick(); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await expect(n8n.ndv.getFloatingNodeByPosition('inputMain')).toBeHidden(); + await expect(n8n.ndv.getFloatingNodeByPosition('outputMain')).toBeVisible(); + for (let i = 0; i < 4; i++) { + await n8n.ndv.navigateToNextFloatingNodeWithKeyboard(); + await expect(n8n.ndv.getFloatingNodeByPosition('inputMain')).toBeVisible(); + await expect(n8n.ndv.getFloatingNodeByPosition('outputMain')).toBeVisible(); + + await n8n.ndv.close(); + await expect(n8n.canvas.getSelectedNodes()).toHaveCount(1); + + await n8n.canvas.getSelectedNodes().first().dblclick(); + await expect(n8n.ndv.getContainer()).toBeVisible(); + } + + await n8n.ndv.navigateToNextFloatingNodeWithKeyboard(); + await expect(n8n.ndv.getFloatingNodeByPosition('inputMain')).toBeVisible(); + + for (let i = 0; i < 4; i++) { + await n8n.ndv.navigateToPreviousFloatingNodeWithKeyboard(); + await expect(n8n.ndv.getFloatingNodeByPosition('outputMain')).toBeVisible(); + await expect(n8n.ndv.getFloatingNodeByPosition('inputMain')).toBeVisible(); + } + + await n8n.ndv.navigateToPreviousFloatingNodeWithKeyboard(); + await expect(n8n.ndv.getFloatingNodeByPosition('inputMain')).toBeHidden(); + await expect(n8n.ndv.getFloatingNodeByPosition('inputSub')).toBeHidden(); + await expect(n8n.ndv.getFloatingNodeByPosition('outputSub')).toBeHidden(); + + await n8n.ndv.close(); + await expect(n8n.canvas.getSelectedNodes()).toHaveCount(1); + }); + + test('should connect floating sub-nodes', async ({ n8n }) => { + await n8n.canvas.addNode('AI Agent', { closeNDV: false }); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await n8n.ndv.connectAISubNode('ai_languageModel', 'Anthropic Chat Model'); + await n8n.ndv.connectAISubNode('ai_memory', 'Simple Memory'); + await n8n.ndv.connectAISubNode('ai_tool', 'HTTP Request Tool'); + + expect(await n8n.ndv.getNodesWithIssuesCount()).toBeGreaterThanOrEqual(2); + }); + + test('should have the floating nodes in correct order', async ({ n8n }) => { + await n8n.start.fromImportedWorkflow('Floating_Nodes.json'); + + await n8n.canvas.openNode('Merge'); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + expect(await n8n.ndv.getFloatingNodeCount('inputMain')).toBe(2); + await n8n.ndv.verifyFloatingNodeName('inputMain', 'Edit Fields1', 0); + await n8n.ndv.verifyFloatingNodeName('inputMain', 'Edit Fields0', 1); + + await n8n.ndv.close(); + + await n8n.canvas.openNode('Merge1'); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + expect(await n8n.ndv.getFloatingNodeCount('inputMain')).toBe(2); + await n8n.ndv.verifyFloatingNodeName('inputMain', 'Edit Fields0', 0); + await n8n.ndv.verifyFloatingNodeName('inputMain', 'Edit Fields1', 1); + }); + }); + + test.describe('Parameter Management - Advanced', () => { + test('Should clear mismatched collection parameters', async ({ n8n }) => { + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.addNode('Notion', { action: 'Create a database page', closeNDV: false }); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await n8n.ndv.addItemToFixedCollection('propertiesUi'); + await n8n.ndv.changeNodeOperation('Update'); + + await expect(n8n.ndv.getParameterItemWithText('Currently no items exist')).toBeVisible(); + }); + + test('Should keep RLC values after operation change', async ({ n8n }) => { + const TEST_DOC_ID = '1111'; + + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.addNode('Google Sheets', { closeNDV: false, action: 'Append row in sheet' }); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await n8n.ndv.setRLCValue('documentId', TEST_DOC_ID); + await n8n.ndv.changeNodeOperation('Append or Update Row'); + const input = n8n.ndv.getResourceLocatorInput('documentId').locator('input'); + await expect(input).toHaveValue(TEST_DOC_ID); + }); + + test('Should not clear resource/operation after credential change', async ({ n8n }) => { + await n8n.canvas.addNode('Manual Trigger'); + await n8n.canvas.addNode('Discord', { closeNDV: false, action: 'Delete a message' }); + await expect(n8n.ndv.getContainer()).toBeVisible(); + + await n8n.credentials.createAndSaveNewCredential('botToken', 'sk_test_123'); + const resourceInput = n8n.ndv.getParameterInputField('resource'); + const operationInput = n8n.ndv.getParameterInputField('operation'); + + await expect(resourceInput).toHaveValue('Message'); + await expect(operationInput).toHaveValue('Delete'); + }); + }); + + test.describe('Node Creator Integration', () => { + test('Should open appropriate node creator after clicking on connection hint link', async ({ + n8n, + }) => { + const hintMapper = { + Memory: 'AI Nodes', + 'Output Parser': 'AI Nodes', + 'Token Splitter': 'Document Loaders', + Tool: 'AI Nodes', + Embeddings: 'Vector Stores', + 'Vector Store': 'Retrievers', + }; + + await n8n.canvas.importWorkflow( + 'open_node_creator_for_connection.json', + 'open_node_creator_for_connection', + ); + + for (const [node, group] of Object.entries(hintMapper)) { + await n8n.canvas.openNode(node); + + await n8n.ndv.clickNodeCreatorInsertOneButton(); + await expect(n8n.canvas.getNodeCreatorHeader(group)).toBeVisible(); + await n8n.page.keyboard.press('Escape'); + } + }); + }); + + test.describe('Expression Editor Features', () => { + test('should allow selecting item for expressions', async ({ n8n }) => { + await n8n.canvas.importWorkflow('Test_workflow_3.json', 'My test workflow 2'); + + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + + await n8n.canvas.openNode('Set'); + + await n8n.ndv.getAssignmentValue('assignments').getByText('Expression').click(); + + const expressionInput = n8n.ndv.getInlineExpressionEditorInput(); + await expressionInput.click(); + await n8n.ndv.clearExpressionEditor(); + await n8n.ndv.typeInExpressionEditor('{{ $json.input[0].count'); + + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('0'); + + await n8n.ndv.expressionSelectNextItem(); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('1'); + await expect(n8n.ndv.getInlineExpressionEditorItemInput()).toHaveValue('1'); + + await expect(n8n.ndv.getInlineExpressionEditorItemNextButton()).toBeDisabled(); + + await n8n.ndv.expressionSelectPrevItem(); + await expect(n8n.ndv.getInlineExpressionEditorOutput()).toHaveText('0'); + await expect(n8n.ndv.getInlineExpressionEditorItemInput()).toHaveValue('0'); + }); + }); + + test.describe('Schema & Data Views', () => { + test('should show data from the correct output in schema view', async ({ n8n }) => { + await n8n.canvas.importWorkflow('Test_workflow_multiple_outputs.json', 'Multiple outputs'); + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + + await n8n.canvas.openNode('Only Item 1'); + await expect(n8n.ndv.getInputPanel()).toBeVisible(); + await n8n.ndv.switchInputMode('Schema'); + await expect(n8n.ndv.getInputSchemaItem('onlyOnItem1')).toBeVisible(); + await n8n.ndv.close(); + + await n8n.canvas.openNode('Only Item 2'); + await expect(n8n.ndv.getInputPanel()).toBeVisible(); + await n8n.ndv.switchInputMode('Schema'); + await expect(n8n.ndv.getInputSchemaItem('onlyOnItem2')).toBeVisible(); + await n8n.ndv.close(); + + await n8n.canvas.openNode('Only Item 3'); + await expect(n8n.ndv.getInputPanel()).toBeVisible(); + await n8n.ndv.switchInputMode('Schema'); + await expect(n8n.ndv.getInputSchemaItem('onlyOnItem3')).toBeVisible(); + await n8n.ndv.close(); + }); + }); + + test.describe('Search Functionality - Advanced', () => { + test('should not show items count when searching in schema view', async ({ n8n }) => { + await n8n.canvas.importWorkflow('Test_ndv_search.json', 'NDV Search Test'); + await n8n.canvas.openNode('Edit Fields'); + await expect(n8n.ndv.getOutputPanel()).toBeVisible(); + + await n8n.ndv.execute(); + await n8n.ndv.switchOutputMode('Schema'); + await n8n.ndv.searchOutputData('US'); + + await expect(n8n.ndv.getOutputItemsCount()).toBeHidden(); + }); + + test('should show additional tooltip when searching in schema view if no matches', async ({ + n8n, + }) => { + await n8n.canvas.importWorkflow('Test_ndv_search.json', 'NDV Search Test'); + + await n8n.canvas.openNode('Edit Fields'); + await expect(n8n.ndv.getOutputPanel()).toBeVisible(); + + await n8n.ndv.execute(); + await n8n.ndv.switchOutputMode('Schema'); + await n8n.ndv.searchOutputData('foo'); + + await expect( + n8n.ndv.getOutputPanel().getByText('To search field values, switch to table or JSON view.'), + ).toBeVisible(); + }); + }); + + test.describe('Complex Edge Cases', () => { + test('ADO-2931 - should handle multiple branches of the same input with the first branch empty correctly', async ({ + n8n, + }) => { + await n8n.canvas.importWorkflow( + 'Test_ndv_two_branches_of_same_parent_false_populated.json', + 'Multiple Branches Test', + ); + + await n8n.canvas.openNode('DebugHelper'); + await expect(n8n.ndv.getInputPanel()).toBeVisible(); + await expect(n8n.ndv.getOutputPanel()).toBeVisible(); + + await n8n.ndv.execute(); + + await expect( + n8n.ndv.getInputPanel().getByTestId('run-data-schema-item').filter({ hasText: 'a1' }), + ).toBeVisible(); + }); + }); + + test.describe('Execution Indicators - Multi-Node', () => { + test('should properly show node execution indicator for multiple nodes', async ({ n8n }) => { + await n8n.canvas.addNode(CODE_NODE_NAME, { action: 'Code in JavaScript' }); + await n8n.ndv.clickBackToCanvasButton(); + + await n8n.workflowComposer.executeWorkflowAndWaitForNotification( + 'Workflow executed successfully', + ); + + await n8n.canvas.openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME); + await expect(n8n.ndv.getNodeRunSuccessIndicator()).toBeVisible(); + await expect(n8n.ndv.getNodeRunTooltipIndicator()).toBeVisible(); + + await n8n.ndv.clickBackToCanvasButton(); + await n8n.canvas.openNode(CODE_NODE_DISPLAY_NAME); + await expect(n8n.ndv.getNodeRunSuccessIndicator()).toBeVisible(); + }); + }); +}); diff --git a/packages/testing/playwright/workflows/Floating_Nodes.json b/packages/testing/playwright/workflows/Floating_Nodes.json new file mode 100644 index 0000000000..2c95497b01 --- /dev/null +++ b/packages/testing/playwright/workflows/Floating_Nodes.json @@ -0,0 +1,221 @@ +{ + "name": "Floating Nodes", + "nodes": [ + { + "parameters": {}, + "id": "d0eda550-2526-42a1-aa19-dee411c8acf9", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [700, 560] + }, + { + "parameters": { + "options": {} + }, + "id": "30412165-1229-4b21-9890-05bfbd9952ab", + "name": "Node 1", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [920, 560] + }, + { + "parameters": { + "options": {} + }, + "id": "201cc8fc-3124-47a3-bc08-b3917c1ddcd9", + "name": "Node 2", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [1100, 560] + }, + { + "parameters": { + "options": {} + }, + "id": "a29802bb-a284-495d-9917-6c6e42fef01e", + "name": "Node 3", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [1280, 560] + }, + { + "parameters": { + "options": {} + }, + "id": "a95a72b3-8b39-44e2-a05b-d8d677741c80", + "name": "Node 4", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [1440, 560] + }, + { + "parameters": {}, + "id": "4674f10d-6144-4a17-bbbb-350c3974438e", + "name": "Chain", + "type": "@n8n/n8n-nodes-langchain.chainLlm", + "typeVersion": 1, + "position": [1580, 560] + }, + { + "parameters": { + "options": {} + }, + "id": "58e12ea5-bd3e-4abf-abec-fcfb5c0a7955", + "name": "Model", + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1, + "position": [1600, 740] + }, + { + "parameters": {}, + "type": "n8n-nodes-base.merge", + "typeVersion": 3.1, + "position": [440, -140], + "id": "a00959d3-8d4b-40af-b4f2-35ca3d73fd84", + "name": "Merge" + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [-20, -120], + "id": "a5cbc221-ccfd-4034-a648-6a192834af81", + "name": "Edit Fields0" + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [0, 100], + "id": "d3b4c17a-bee8-418b-a721-5debafd1ce11", + "name": "Edit Fields1" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.merge", + "typeVersion": 3.1, + "position": [440, 100], + "id": "b23a2a43-ffac-41a5-a265-054e21a57d70", + "name": "Merge1" + } + ], + "pinData": {}, + "connections": { + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Node 1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Node 1": { + "main": [ + [ + { + "node": "Node 2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Node 3": { + "main": [ + [ + { + "node": "Node 4", + "type": "main", + "index": 0 + } + ] + ] + }, + "Node 2": { + "main": [ + [ + { + "node": "Node 3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Chain": { + "main": [[]] + }, + "Model": { + "ai_languageModel": [ + [ + { + "node": "Chain", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Node 4": { + "main": [ + [ + { + "node": "Chain", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields0": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 1 + }, + { + "node": "Merge1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields1": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 0 + }, + { + "node": "Merge1", + "type": "main", + "index": 1 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "2730d156-a98a-4ac8-b481-5c16361fdba2", + "id": "6bzXMGxHuxeEaqsA", + "meta": { + "instanceId": "1838be0fa0389fbaf5e2e4aaedab4ddc79abc4175b433401abb22a281001b853" + }, + "tags": [] +} diff --git a/packages/testing/playwright/workflows/NDV-test-select-input.json b/packages/testing/playwright/workflows/NDV-test-select-input.json new file mode 100644 index 0000000000..f64371e7fb --- /dev/null +++ b/packages/testing/playwright/workflows/NDV-test-select-input.json @@ -0,0 +1,74 @@ +{ + "name": "ff739753-9d6e-46a7-94d3-25bd03dd4973", + "nodes": [ + { + "parameters": {}, + "id": "c30d1114-d7f7-44dc-b55a-15312ef2d76d", + "name": "On clicking 'execute'", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [600, 580] + }, + { + "parameters": { + "options": {} + }, + "id": "5bf514bd-65ae-4a1c-8b69-a01bfeaad411", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [820, 580] + }, + { + "parameters": { + "options": {} + }, + "id": "02bf49e9-44b2-4f4e-8cb2-8c02399208af", + "name": "Set1", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [1040, 580] + } + ], + "pinData": { + "On clicking 'execute'": [ + { + "json": { + "start": true + } + } + ] + }, + "connections": { + "On clicking 'execute'": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set": { + "main": [ + [ + { + "node": "Set1", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "hash": "abd7b28aa2605c96ba24474d72cbe610", + "id": 3, + "meta": { + "instanceId": "08a83d394781701f5c18052cde68e8d92c88b26f5cc8809eb10ad545f14015cb" + }, + "tags": [] +} diff --git a/packages/testing/playwright/workflows/Test_ndv_search.json b/packages/testing/playwright/workflows/Test_ndv_search.json new file mode 100644 index 0000000000..b374dce37b --- /dev/null +++ b/packages/testing/playwright/workflows/Test_ndv_search.json @@ -0,0 +1,126 @@ +{ + "name": "NDV search bugs (introduced by schema view?)", + "nodes": [ + { + "parameters": {}, + "id": "55635c7b-92ee-4d2d-a0c0-baff9ab071da", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "position": [800, 380], + "typeVersion": 1 + }, + { + "parameters": { + "operation": "getAllPeople" + }, + "id": "4737af43-e49b-4c92-b76f-32605c047114", + "name": "Customer Datastore (n8n training)", + "type": "n8n-nodes-base.n8nTrainingCustomerDatastore", + "typeVersion": 1, + "position": [1020, 380] + }, + { + "parameters": { + "assignments": { + "assignments": [] + }, + "includeOtherFields": true, + "options": {} + }, + "id": "8cc9b374-1856-4f3f-9315-08e6e27840d8", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [1240, 380] + } + ], + "pinData": { + "Customer Datastore (n8n training)": [ + { + "json": { + "id": "23423532", + "name": "Jay Gatsby", + "email": "gatsby@west-egg.com", + "notes": "Keeps asking about a green light??", + "country": "US", + "created": "1925-04-10" + } + }, + { + "json": { + "id": "23423533", + "name": "José Arcadio Buendía", + "email": "jab@macondo.co", + "notes": "Lots of people named after him. Very confusing", + "country": "CO", + "created": "1967-05-05" + } + }, + { + "json": { + "id": "23423534", + "name": "Max Sendak", + "email": "info@in-and-out-of-weeks.org", + "notes": "Keeps rolling his terrible eyes", + "country": "US", + "created": "1963-04-09" + } + }, + { + "json": { + "id": "23423535", + "name": "Zaphod Beeblebrox", + "email": "captain@heartofgold.com", + "notes": "Felt like I was talking to more than one person", + "country": null, + "created": "1979-10-12" + } + }, + { + "json": { + "id": "23423536", + "name": "Edmund Pevensie", + "email": "edmund@narnia.gov", + "notes": "Passionate sailor", + "country": "UK", + "created": "1950-10-16" + } + } + ] + }, + "connections": { + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Customer Datastore (n8n training)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Customer Datastore (n8n training)": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "20178044-fb64-4443-88dd-e941517520d0", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd" + }, + "id": "aBVnTRON9Y2cSmse", + "tags": [] +} diff --git a/packages/testing/playwright/workflows/Test_ndv_two_branches_of_same_parent_false_populated.json b/packages/testing/playwright/workflows/Test_ndv_two_branches_of_same_parent_false_populated.json new file mode 100644 index 0000000000..8fb74eded8 --- /dev/null +++ b/packages/testing/playwright/workflows/Test_ndv_two_branches_of_same_parent_false_populated.json @@ -0,0 +1,94 @@ +{ + "nodes": [ + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "6f0cf983-824b-4339-a5de-6b374a23b4b0", + "leftValue": "={{ $json.a }}", + "rightValue": 3, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [220, 0], + "id": "1755282a-ec4a-4d02-a833-0316ca413cc4", + "name": "If" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, 0], + "id": "de1e7acf-12d8-4e56-ba42-709ffb397db2", + "name": "When clicking ‘Execute workflow’" + }, + { + "parameters": { + "category": "randomData" + }, + "type": "n8n-nodes-base.debugHelper", + "typeVersion": 1, + "position": [580, 0], + "id": "86440d33-f833-453c-bcaa-fff7e0083501", + "name": "DebugHelper", + "alwaysOutputData": true + } + ], + "connections": { + "If": { + "main": [ + [ + { + "node": "DebugHelper", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "DebugHelper", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "If", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "When clicking ‘Execute workflow’": [ + { + "a": 1 + }, + { + "a": 2 + } + ] + } +} diff --git a/packages/testing/playwright/workflows/Test_workflow_5.json b/packages/testing/playwright/workflows/Test_workflow_5.json new file mode 100644 index 0000000000..cbdb780b4d --- /dev/null +++ b/packages/testing/playwright/workflows/Test_workflow_5.json @@ -0,0 +1,258 @@ +{ + "meta": { + "instanceId": "8147b3a74cd161276e0f3bfc17369a724afab0d377593fada8be82d34c0c6a95" + }, + "nodes": [ + { + "parameters": { + "jsCode": "return [\n {\n id: 6666\n },\n {\n id: 3333\n },\n {\n id: 9999\n },\n {\n id: 1111\n },\n {\n id: 4444\n },\n {\n id: 8888\n },\n]" + }, + "id": "5f023c7c-67ca-47a0-8a90-8227fcf29b9c", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [-520, 580] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "id", + "value": "={{ $json.id }}" + } + ] + }, + "options": {} + }, + "id": "bd454282-9dd7-465f-9b9a-654a0c8532ec", + "name": "Set2", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [-40, 780] + }, + { + "parameters": {}, + "id": "ef63cdc5-50bc-4525-9873-7e7f7589a60e", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-740, 580] + }, + { + "parameters": { + "sortFieldsUi": { + "sortField": [ + { + "fieldName": "id" + } + ] + }, + "options": {} + }, + "id": "555a150c-d735-4331-b628-c1f1cfed2da1", + "name": "Sort", + "type": "n8n-nodes-base.sort", + "typeVersion": 1, + "position": [-280, 580] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "id", + "value": "={{ $json.id }}" + } + ] + }, + "options": {} + }, + "id": "02372cb6-aac8-45c3-8600-f699901289ac", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [-60, 580] + }, + { + "parameters": { + "options": {} + }, + "id": "00d73944-218c-4896-af68-3f2855a922d1", + "name": "Set1", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [-280, 780] + }, + { + "parameters": { + "conditions": { + "number": [ + { + "value1": "={{ $json.id }}", + "operation": "smallerEqual", + "value2": 6666 + } + ] + } + }, + "id": "211a7bef-32d1-4928-9cef-3a45f2e61379", + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [160, 580] + }, + { + "parameters": { + "options": {} + }, + "id": "dcbd4745-832f-43d8-8a3c-dd80e8ca2777", + "name": "Set3", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [140, 780] + }, + { + "parameters": { + "jsCode": "return [\n {\n id: 1000\n },\n {\n id: 300\n },\n {\n id: 2000\n },\n {\n id: 100\n },\n {\n id: 400\n },\n {\n id: 1300\n },\n]" + }, + "id": "ec9c8f16-f3c8-4054-a6e9-4f1ebcdebb71", + "name": "Code1", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [-520, 780] + }, + { + "parameters": { + "options": {} + }, + "id": "42e89478-a53a-4d10-b20c-1dc5d5f953d5", + "name": "Set4", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [460, 460] + }, + { + "parameters": { + "options": {} + }, + "id": "5085eb1c-0345-4b9d-856a-2955279f2c5d", + "name": "Set5", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [460, 660] + } + ], + "connections": { + "Code": { + "main": [ + [ + { + "node": "Sort", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set2": { + "main": [ + [ + { + "node": "Set3", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + }, + { + "node": "Code1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sort": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + }, + { + "node": "Set2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set1": { + "main": [ + [ + { + "node": "Set2", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF": { + "main": [ + [ + { + "node": "Set4", + "type": "main", + "index": 0 + }, + { + "node": "Set5", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Set5", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code1": { + "main": [ + [ + { + "node": "Set1", + "type": "main", + "index": 0 + } + ] + ] + } + } +} diff --git a/packages/testing/playwright/workflows/Test_workflow_multiple_outputs.json b/packages/testing/playwright/workflows/Test_workflow_multiple_outputs.json new file mode 100644 index 0000000000..0ecd6e9457 --- /dev/null +++ b/packages/testing/playwright/workflows/Test_workflow_multiple_outputs.json @@ -0,0 +1,208 @@ +{ + "name": "Multiple outputs", + "nodes": [ + { + "parameters": {}, + "id": "64b27674-3da6-46ce-9008-e173182efa48", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "position": [16, -32], + "typeVersion": 1 + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "leftValue": "={{ $json.code }}", + "rightValue": 1, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Item1" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "a659050f-0867-471d-8914-d499b6ad7b31", + "leftValue": "={{ $json.code }}", + "rightValue": 2, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Item2" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "109fc001-53af-48f1-b79c-5e9afc8b94bd", + "leftValue": "={{ $json.code }}", + "rightValue": 3, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Item3" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "position": [192, -32], + "id": "3863cc7a-8f45-46fc-a60c-36aad5b12877", + "name": "Switch", + "typeVersion": 3 + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f71bac89-8852-41b2-98dd-cb689f011dcb", + "name": "", + "value": "", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "position": [480, -192], + "id": "85940094-4656-4cdf-a871-1b3b46421de3", + "name": "Only Item 1", + "typeVersion": 3.4 + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.set", + "position": [480, -32], + "id": "a7f4e2b5-8cc9-4881-aa06-38601988740e", + "name": "Only Item 2", + "typeVersion": 3.4 + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.set", + "position": [480, 128], + "id": "7e44ad56-415a-4991-a70e-fea86c430031", + "name": "Only Item 3", + "typeVersion": 3.4 + } + ], + "pinData": { + "When clicking ‘Execute workflow’": [ + { + "json": { + "name": "First item", + "onlyOnItem1": true, + "code": 1 + } + }, + { + "json": { + "name": "Second item", + "onlyOnItem2": true, + "code": 2 + } + }, + { + "json": { + "name": "Third item", + "onlyOnItem3": true, + "code": 3 + } + } + ] + }, + "connections": { + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + [ + { + "node": "Only Item 1", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Only Item 2", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Only Item 3", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "1e2a7b45-7730-42d6-989e-f3fa80de303e", + "meta": { + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "V2ld4YU11fsHgr1z", + "tags": [] +} diff --git a/packages/testing/playwright/workflows/Test_workflow_ndv_errors.json b/packages/testing/playwright/workflows/Test_workflow_ndv_errors.json new file mode 100644 index 0000000000..7f55dcee49 --- /dev/null +++ b/packages/testing/playwright/workflows/Test_workflow_ndv_errors.json @@ -0,0 +1,29 @@ +{ + "meta": { + "instanceId": "3204fc455f5cbeb4e71fdbd3b1dfaf0b088088dea3e639de49e61462b80ffc1d" + }, + "nodes": [ + { + "parameters": { + "application": { + "__rl": true, + "mode": "url", + "value": "", + "__regex": "https://airtable.com/([a-zA-Z0-9]{2,})" + }, + "table": { + "__rl": true, + "mode": "url", + "value": "", + "__regex": "https://airtable.com/[a-zA-Z0-9]{2,}/([a-zA-Z0-9]{2,})" + } + }, + "id": "e0c0cf7e-aa98-4b72-9645-6e64e2902bd1", + "name": "Airtable", + "type": "n8n-nodes-base.airtable", + "typeVersion": 1, + "position": [380, 180] + } + ], + "connections": {} +} diff --git a/packages/testing/playwright/workflows/Test_workflow_ndv_run_error.json b/packages/testing/playwright/workflows/Test_workflow_ndv_run_error.json new file mode 100644 index 0000000000..fb66d53f62 --- /dev/null +++ b/packages/testing/playwright/workflows/Test_workflow_ndv_run_error.json @@ -0,0 +1,150 @@ +{ + "name": "My workflow 52", + "nodes": [ + { + "parameters": { + "jsCode": "\nreturn [\n {\n \"field\": \"the same\"\n }\n];" + }, + "id": "38c14c4a-7af1-4b04-be76-f8e474c95569", + "name": "Break pairedItem chain", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [240, 1020] + }, + { + "parameters": { + "options": {} + }, + "id": "78c4964a-c4e8-47e5-81f3-89ba778feb8b", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [40, 1020] + }, + { + "parameters": {}, + "id": "4f4c6527-d565-448a-96bd-8f5414caf8cc", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-180, 1020] + }, + { + "parameters": { + "fields": { + "values": [ + { + "stringValue": "={{ $('Edit Fields').item.json.name }}" + } + ] + }, + "options": {} + }, + "id": "44f4e5da-bfe9-4dc3-8d1f-f38e9f364754", + "name": "Error", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [460, 1020] + } + ], + "pinData": { + "Edit Fields": [ + { + "json": { + "id": "23423532", + "name": "Jay Gatsby", + "email": "gatsby@west-egg.com", + "notes": "Keeps asking about a green light??", + "country": "US", + "created": "1925-04-10" + } + }, + { + "json": { + "id": "23423533", + "name": "José Arcadio Buendía", + "email": "jab@macondo.co", + "notes": "Lots of people named after him. Very confusing", + "country": "CO", + "created": "1967-05-05" + } + }, + { + "json": { + "id": "23423534", + "name": "Max Sendak", + "email": "info@in-and-out-of-weeks.org", + "notes": "Keeps rolling his terrible eyes", + "country": "US", + "created": "1963-04-09" + } + }, + { + "json": { + "id": "23423535", + "name": "Zaphod Beeblebrox", + "email": "captain@heartofgold.com", + "notes": "Felt like I was talking to more than one person", + "country": null, + "created": "1979-10-12" + } + }, + { + "json": { + "id": "23423536", + "name": "Edmund Pevensie", + "email": "edmund@narnia.gov", + "notes": "Passionate sailor", + "country": "UK", + "created": "1950-10-16" + } + } + ] + }, + "connections": { + "Break pairedItem chain": { + "main": [ + [ + { + "node": "Error", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "Break pairedItem chain", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "ca53267f-4eb4-481d-9e09-ecb97f6b09e2", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "6fr8GiRyMlZCiDQW", + "tags": [] +} diff --git a/packages/testing/playwright/workflows/Test_workflow_ndv_version.json b/packages/testing/playwright/workflows/Test_workflow_ndv_version.json new file mode 100644 index 0000000000..9f1a6e6950 --- /dev/null +++ b/packages/testing/playwright/workflows/Test_workflow_ndv_version.json @@ -0,0 +1,41 @@ +{ + "name": "Node versions", + "nodes": [ + { + "id": "2acca986-10a6-451e-b20a-86e95b50e627", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [460, 460] + }, + { + "id": "1ea0a87c-3395-4bd9-84fb-cf8b0f769cc4", + "name": "Function", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [960, 460] + }, + { + "id": "30bb9fab-bc89-4309-b42b-0fc586519c76", + "name": "Edit Fields (old)", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [800, 460] + }, + { + "id": "a266b96c-3539-4034-b24c-c86c6d0ca31e", + "name": "Edit Fields (no typeVersion)", + "type": "n8n-nodes-base.set", + "position": [1120, 460] + }, + { + "id": "273f60c9-08e7-457e-b01d-31e16c565171", + "name": "Edit Fields (latest)", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [640, 460] + } + ], + "connections": {}, + "pinData": {} +} diff --git a/packages/testing/playwright/workflows/Test_workflow_schema_test.json b/packages/testing/playwright/workflows/Test_workflow_schema_test.json new file mode 100644 index 0000000000..5eb5b8b4d2 --- /dev/null +++ b/packages/testing/playwright/workflows/Test_workflow_schema_test.json @@ -0,0 +1,83 @@ +{ + "name": "My workflow 8", + "nodes": [ + { + "parameters": { + "operation": "getAllPeople", + "limit": 10 + }, + "id": "39cd80ce-5a8f-4339-b3d5-c4af969dd330", + "name": "Customer Datastore (n8n training)", + "type": "n8n-nodes-base.n8nTrainingCustomerDatastore", + "typeVersion": 1, + "position": [940, 680] + }, + { + "parameters": { + "values": { + "number": [ + { + "name": "objectValue.prop1", + "value": 123 + } + ], + "string": [ + { + "name": "objectValue.prop2", + "value": "someText" + } + ] + }, + "options": { + "dotNotation": true + } + }, + "id": "6e4490f6-ba95-4400-beec-2caefdd4895a", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [1300, 680] + }, + { + "parameters": {}, + "id": "58512a93-dabf-4584-817f-27c608c1bdd5", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [720, 680] + } + ], + "pinData": {}, + "connections": { + "Customer Datastore (n8n training)": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Customer Datastore (n8n training)", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "4a4f292a-92be-427c-848a-9582527f5ed3", + "id": "8", + "meta": { + "instanceId": "032eceae7493054b723340499be69ecbf4cbe28a7ec6df676b759000750b968d" + }, + "tags": [] +} diff --git a/packages/testing/playwright/workflows/Test_workflow_schema_test_pinned_data.json b/packages/testing/playwright/workflows/Test_workflow_schema_test_pinned_data.json new file mode 100644 index 0000000000..f452c0f543 --- /dev/null +++ b/packages/testing/playwright/workflows/Test_workflow_schema_test_pinned_data.json @@ -0,0 +1,565 @@ +{ + "name": "My workflow", + "nodes": [ + { + "parameters": { + "operation": "getAllPeople", + "limit": 10 + }, + "id": "441afcbf-a678-4463-bc89-7e0b6693af5c", + "name": "Customer Datastore (n8n training)", + "type": "n8n-nodes-base.n8nTrainingCustomerDatastore", + "typeVersion": 1, + "position": [720, 440] + }, + { + "parameters": { + "values": { + "number": [ + { + "name": "objectValue.prop1", + "value": 123 + } + ], + "string": [ + { + "name": "objectValue.prop2", + "value": "someText" + } + ] + }, + "options": { + "dotNotation": true + } + }, + "id": "44094a05-b3b7-49bf-bfbf-a711e6ba45d8", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [1080, 440] + }, + { + "parameters": {}, + "id": "3dc7cf26-ff25-4437-b9fd-0e8b127ebec9", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [500, 440] + } + ], + "pinData": { + "Set": [ + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + }, + { + "json": { + "key0": 0, + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + "key6": 6, + "key7": 7, + "key8": 8, + "key9": 9, + "key10": 10, + "key11": 11, + "key12": 12, + "key13": 13, + "key14": 14, + "key15": 15, + "key16": 16, + "key17": 17, + "key18": 18, + "key19": 19 + } + } + ] + }, + "connections": { + "Customer Datastore (n8n training)": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Customer Datastore (n8n training)", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "", + "meta": { + "instanceId": "363581be2c2581d1b11e189456a090887e137f8393a4b5cb85641b1ee4fae479" + }, + "tags": [] +} diff --git a/packages/testing/playwright/workflows/Test_workflow_xml_output.json b/packages/testing/playwright/workflows/Test_workflow_xml_output.json new file mode 100644 index 0000000000..9131f8d8bc --- /dev/null +++ b/packages/testing/playwright/workflows/Test_workflow_xml_output.json @@ -0,0 +1,47 @@ +{ + "meta": { + "instanceId": "2d1cf27f75b18bb9e146336f791c37884f4fc7ddb97c2def27c0444d106778bf" + }, + "nodes": [ + { + "parameters": {}, + "id": "8108d313-8b03-4aa4-963d-cd1c0fe8f85c", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [420, 220] + }, + { + "parameters": { + "fields": { + "values": [ + { + "name": "body", + "stringValue": " Introduction to XML John Doe 2020 1234567890 Data Science Basics Jane Smith 2019 0987654321 Programming in Python Bob Johnson 2021 5432109876 " + } + ] + }, + "options": {} + }, + "id": "45888152-7c5f-4d88-9039-660c594da084", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [640, 220] + } + ], + "connections": { + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/packages/testing/playwright/workflows/open_node_creator_for_connection.json b/packages/testing/playwright/workflows/open_node_creator_for_connection.json new file mode 100644 index 0000000000..58cd4d7522 --- /dev/null +++ b/packages/testing/playwright/workflows/open_node_creator_for_connection.json @@ -0,0 +1,89 @@ +{ + "name": "open_node_creator_for_connection", + "nodes": [ + { + "parameters": {}, + "id": "25ff0c17-7064-4e14-aec6-45c71d63201b", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [740, 520] + }, + { + "parameters": {}, + "id": "49f376ca-845b-4737-aac0-073d0e4fa95c", + "name": "Token Splitter", + "type": "@n8n/n8n-nodes-langchain.textSplitterTokenSplitter", + "typeVersion": 1, + "position": [1180, 540] + }, + { + "parameters": {}, + "id": "d1db5111-4b01-4620-8ccb-a16ea576c363", + "name": "Memory", + "type": "@n8n/n8n-nodes-langchain.memoryXata", + "typeVersion": 1.2, + "position": [940, 540], + "credentials": { + "xataApi": { + "id": "q1ckaYlHTWCYDtF0", + "name": "Xata Api account" + } + } + }, + { + "parameters": {}, + "id": "b08b6d3a-bef8-42ac-9cef-ec9d4e5402b1", + "name": "Output Parser", + "type": "@n8n/n8n-nodes-langchain.outputParserStructured", + "typeVersion": 1.1, + "position": [1060, 540] + }, + { + "parameters": {}, + "id": "ee557938-9cf1-4b78-afef-c783c52fd307", + "name": "Tool", + "type": "@n8n/n8n-nodes-langchain.toolWikipedia", + "typeVersion": 1, + "position": [1300, 540] + }, + { + "parameters": { + "options": {} + }, + "id": "814f2e9c-cc7b-4f3c-89b4-d6eb82bc24df", + "name": "Embeddings", + "type": "@n8n/n8n-nodes-langchain.embeddingsHuggingFaceInference", + "typeVersion": 1, + "position": [1420, 540] + }, + { + "parameters": { + "tableName": { + "__rl": true, + "mode": "list", + "value": "" + }, + "options": {} + }, + "id": "e8569b0b-a580-4249-9c5e-f1feed5c644e", + "name": "Vector Store", + "type": "@n8n/n8n-nodes-langchain.vectorStoreSupabase", + "typeVersion": 1, + "position": [1540, 540] + } + ], + "pinData": {}, + "connections": {}, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "8e90604c-f7e9-489d-8e43-1cc699b7db04", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "L3tgfoW660UOSuX6", + "tags": [] +}