Files
n8n-enterprise-unlocked/cypress/e2e/5-ndv.cy.ts
Milorad FIlipović 711fa2b925 fix(editor): Fix operation change failing in certain conditions (#8114)
## Summary
This PR handles the case when there are multiple parameters with the
same name but different `options` and `displayOptions`. In this case, if
one of such fields is set, changing the dependent parameter value so the
other should be shown causes an error in case their options are not
compatible (this
[check](7806a65229/packages/workflow/src/NodeHelpers.ts (L786))).

#### Example:
LDAP node has two `options` properties with the same name:
1. `attributes` with predefined options (`add`, `replace`, `delete`).
Shown when **Update** operation is selected
2. `attributes` with a collection of `attribute` objects. Shows for the
**Create** operation

Setting one of these parameter values and switching operation so the
other is shown breaks the app.
This PR checks if there is a value saved for such parameter and removes
it before calling `getNodeParameters` in `valueChanged` handler.

## Related tickets and issues
Fixes ADO-1589

## Review / Merge checklist
- [x] PR title and summary are descriptive. **Remember, the title
automatically goes into the changelog. Use `(no-changelog)` otherwise.**
([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md))
- [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up
ticket created.
- [x] Tests included.
> A bug is not considered fixed, unless a test is added to prevent it
from happening again.
   > A feature is not complete without tests.
2023-12-22 08:41:20 +01:00

509 lines
20 KiB
TypeScript

import { v4 as uuid } from 'uuid';
import { getVisibleSelect } from '../utils';
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../constants';
import { NDV, WorkflowPage } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('NDV', () => {
beforeEach(() => {
workflowPage.actions.visit();
workflowPage.actions.renameWorkflow(uuid());
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 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 ${uuid()}`);
workflowPage.actions.zoomToFit();
workflowPage.getters.canvasNodes().last().dblclick();
ndv.getters.inputSelect().click();
ndv.getters.inputOption().last().click();
ndv.getters.inputDataContainer().find('[class*=schema_]').should('exist');
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', 0);
ndv.getters.backToCanvas().click();
workflowPage.actions.openNode('Airtable');
cy.get('.has-issues').should('have.length', 2);
cy.get('[class*=hasIssues]').should('have.length', 1);
});
it('should show all validation errors when opening pasted node', () => {
cy.fixture('Test_workflow_ndv_errors.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
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 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', `NDV test schema view ${uuid()}`);
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('label').click();
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.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 ${uuid()}`,
);
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] [data-test-id=run-data-schema-item]')
.should('have.length', 20);
});
});
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.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.switchInputMode('Table');
ndv.actions.switchOutputMode('Table');
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`);
workflowPage.actions.openNode('Set1');
ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions
[
{
input: 'hello',
},
{
input: '',
output: '[empty]',
},
{
input: ' test',
},
{
input: ' ',
},
{
input: '<div></div>',
},
].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('should not retrieve remote options when required params throw errors', () => {
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
ndv.actions.setInvalidExpression({ fieldName: 'fieldId', delay: 200 });
ndv.getters.container().click(); // remove focus from input, hide expression preview
ndv.getters.parameterInput('remoteOptions').click();
ndv.getters.parameterInputIssues('remoteOptions').realHover();
// Remote options dropdown should not be visible
ndv.getters.parameterInput('remoteOptions').find('.el-select').should('not.exist');
});
it('should retrieve remote options when non-required params throw errors', () => {
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
ndv.getters.parameterInput('remoteOptions').click();
ndv.actions.setInvalidExpression({ fieldName: 'otherField', delay: 50 });
ndv.getters.container().click(); // remove focus from input, hide expression preview
ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
});
it('should flag issues as soon as params are set', () => {
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();
ndv.getters.nodeExecuteButton().should('be.disabled');
ndv.getters.triggerPanelExecuteButton().should('not.exist');
ndv.actions.close();
workflowPage.getters.nodeIssuesByName('Webhook').should('exist');
workflowPage.getters.canvasNodes().first().dblclick();
ndv.getters.parameterInput('path').type('t');
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.addInitialNodeToCanvas('Code', { 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 not retrieve remote options when a parameter value changes', () => {
cy.intercept('/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}]`);
}
beforeEach(() => {
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`);
workflowPage.getters.canvasNodes().first().dblclick()
getFloatingNodeByPosition("inputMain").should('not.exist');
getFloatingNodeByPosition("outputMain").should('exist');
});
it('should traverse floating nodes with mouse', () => {
// 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();
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');
getFloatingNodeByPosition("inputSub").should('exist');
getFloatingNodeByPosition("inputSub").click({ force: true});
ndv.getters.nodeNameContainer().should('contain', 'Model');
getFloatingNodeByPosition("inputSub").should('not.exist');
getFloatingNodeByPosition("inputMain").should('not.exist');
getFloatingNodeByPosition("outputMain").should('not.exist');
getFloatingNodeByPosition("outputSub").should('exist');
ndv.actions.close();
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters.selectedNodes().first().should('contain', 'Model');
workflowPage.getters.selectedNodes().first().dblclick();
getFloatingNodeByPosition("outputSub").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 mouse', () => {
// 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');
getFloatingNodeByPosition("inputSub").should('exist');
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowDown'])
ndv.getters.nodeNameContainer().should('contain', 'Model');
getFloatingNodeByPosition("inputSub").should('not.exist');
getFloatingNodeByPosition("inputMain").should('not.exist');
getFloatingNodeByPosition("outputMain").should('not.exist');
getFloatingNodeByPosition("outputSub").should('exist');
ndv.actions.close();
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters.selectedNodes().first().should('contain', 'Model');
workflowPage.getters.selectedNodes().first().dblclick();
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowUp'])
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 show node name and version in settings', () => {
cy.createFixtureWorkflow('Test_workflow_ndv_version.json', `NDV test version ${uuid()}`);
workflowPage.actions.openNode('Edit Fields (old)');
ndv.actions.openSettings();
ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.2)');
ndv.actions.close();
workflowPage.actions.openNode('Edit Fields (latest)');
ndv.actions.openSettings();
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.2 (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 handle mismatched option attributes', () => {
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);
});
});