Skip to content

Commit

Permalink
fix(editor): Avoid sanitizing output to search node data (#8126)
Browse files Browse the repository at this point in the history
In search feature, output sanitization was added to support `<mark` tag
in output panel to highlight searched text. This removes any html like
data in the input/output panel..
This PR removes sanitization while keeping text highlights..

https://community.n8n.io/t/n8n-output/33997
https://community.n8n.io/t/html-tags-in-editor-rendered/34240
#8081
https://linear.app/n8n/issue/ADO-1594/node-output-view-not-consistent
https://linear.app/n8n/issue/ADO-1597/bug-xml-display-issue

- [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.
- [ ] 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.
  • Loading branch information
mutdmour authored and ivov committed Dec 27, 2023
1 parent 98ad3d4 commit 9a3c133
Show file tree
Hide file tree
Showing 9 changed files with 397 additions and 133 deletions.
210 changes: 151 additions & 59 deletions cypress/e2e/5-ndv.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,108 +370,129 @@ describe('NDV', () => {
});

describe('floating nodes', () => {
function getFloatingNodeByPosition(position: 'inputMain' | 'outputMain' | 'outputSub'| 'inputSub') {
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');
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});
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');
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()
.should('contain', `Node ${i + 1}`);
workflowPage.getters.selectedNodes().first().dblclick();
})
});

getFloatingNodeByPosition("outputMain").click({ force: true});
getFloatingNodeByPosition('outputMain').click({ force: true });
ndv.getters.nodeNameContainer().should('contain', 'Chain');
getFloatingNodeByPosition("inputSub").should('exist');
getFloatingNodeByPosition("inputSub").click({ force: true});
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');
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});
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');
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);
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'])
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');
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()
.should('contain', `Node ${i + 1}`);
workflowPage.getters.selectedNodes().first().dblclick();
})
});

cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight'])
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']);
ndv.getters.nodeNameContainer().should('contain', 'Chain');
getFloatingNodeByPosition("inputSub").should('exist');
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowDown'])
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');
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'])
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');
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);
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()}`);

Expand All @@ -490,17 +511,88 @@ describe('NDV', () => {
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', '<?xml version="1.0" encoding="UTF-8"?> <library>');

cy.document().trigger('keyup', { key: '/' });
ndv.getters.searchInput().filter(':focus').type('<lib');

ndv.getters.outputTableRow(1).find('mark').should('have.text', '<lib');

ndv.getters.outputDisplayMode().find('label').eq(1).should('include.text', 'JSON');
ndv.getters.outputDisplayMode().find('label').eq(1).click();

ndv.getters
.outputDataContainer()
.should(
'have.text',
'[{"body": "<?xml version="1.0" encoding="UTF-8"?> <library> <book> <title>Introduction to XML</title> <author>John Doe</author> <publication_year>2020</publication_year> <isbn>1234567890</isbn> </book> <book> <title>Data Science Basics</title> <author>Jane Smith</author> <publication_year>2019</publication_year> <isbn>0987654321</isbn> </book> <book> <title>Programming in Python</title> <author>Bob Johnson</author> <publication_year>2021</publication_year> <isbn>5432109876</isbn> </book> </library>"}]',
);
ndv.getters.outputDataContainer().find('mark').should('have.text', '<lib');

ndv.getters.outputDisplayMode().find('label').eq(2).should('include.text', 'Schema');
ndv.getters.outputDisplayMode().find('label').eq(2).click({ force: true });
ndv.getters
.outputDataContainer()
.findChildByTestId('run-data-schema-item')
.find('> span')
.should('include.text', '<?xml version="1.0" encoding="UTF-8"?>');
});

it('should properly show node execution indicator', () => {
workflowPage.actions.addInitialNodeToCanvas('Code');
workflowPage.actions.openNode('Code');
// Should not show run info before execution
ndv.getters.nodeRunSuccessIndicator().should('not.exist');
ndv.getters.nodeRunErrorIndicator().should('not.exist');
ndv.getters.nodeExecuteButton().click();
ndv.getters.nodeRunSuccessIndicator().should('exist');
});

it('should properly show node execution indicator for multiple nodes', () => {
workflowPage.actions.addInitialNodeToCanvas('Code');
workflowPage.actions.openNode('Code');
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');
// Code node should show error
ndv.getters.backToCanvas().click();
workflowPage.actions.openNode('Code');
ndv.getters.nodeRunErrorIndicator().should('exist');
});

it('Should handle mismatched option attributes', () => {
workflowPage.actions.addInitialNodeToCanvas('LDAP', { keepNdvOpen: true, action: 'Create a new entry' });
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' });
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);
Expand Down
53 changes: 53 additions & 0 deletions cypress/fixtures/Test_workflow_xml_output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"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": "<?xml version=\"1.0\" encoding=\"UTF-8\"?> <library> <book> <title>Introduction to XML</title> <author>John Doe</author> <publication_year>2020</publication_year> <isbn>1234567890</isbn> </book> <book> <title>Data Science Basics</title> <author>Jane Smith</author> <publication_year>2019</publication_year> <isbn>0987654321</isbn> </book> <book> <title>Programming in Python</title> <author>Bob Johnson</author> <publication_year>2021</publication_year> <isbn>5432109876</isbn> </book> </library>"
}
]
},
"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": {}
}
Loading

0 comments on commit 9a3c133

Please sign in to comment.