From 14d1743595d3d12f7971ca78612031423ac2e1ac Mon Sep 17 00:00:00 2001 From: OlegIvaniv Date: Wed, 8 Feb 2023 14:48:28 +0100 Subject: [PATCH] test(editor): Add canvas dragging tests (no-changelog) (#5406) * WIP * Fix undo/redo tests and add canvas tests * Cleanup * Cleanup * Use drag and draganddrop commands * Fix hanging CI run * Undo redo spec --- cypress/constants.ts | 4 + cypress/e2e/12-canvas.cy.ts | 119 +++++++++++++++++++++- cypress/pages/workflow.ts | 14 +++ cypress/support/commands.ts | 22 ++-- packages/editor-ui/src/mixins/nodeBase.ts | 16 ++- 5 files changed, 162 insertions(+), 13 deletions(-) diff --git a/cypress/constants.ts b/cypress/constants.ts index 26740bfb8b..5118696bc8 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -4,9 +4,13 @@ export const DEFAULT_USER_EMAIL = 'nathan@n8n.io'; export const DEFAULT_USER_PASSWORD = 'CypressTest123'; export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; +export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Execute Workflow"'; export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; export const CODE_NODE_NAME = 'Code'; export const SET_NODE_NAME = 'Set'; +export const IF_NODE_NAME = 'IF'; +export const MERGE_NODE_NAME = 'Merge'; +export const SWITCH_NODE_NAME = 'Switch'; export const GMAIL_NODE_NAME = 'Gmail'; export const TRELLO_NODE_NAME = 'Trello'; export const NOTION_NODE_NAME = 'Notion'; diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index c88e62035d..ac0bbd7bad 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -1,8 +1,12 @@ import { MANUAL_TRIGGER_NODE_NAME, + MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME, SET_NODE_NAME, + SWITCH_NODE_NAME, + IF_NODE_NAME, + MERGE_NODE_NAME, HTTP_REQUEST_NODE_NAME, } from './../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; @@ -33,12 +37,125 @@ describe('Canvas Actions', () => { WorkflowPage.getters.executeWorkflowButton().should('be.visible'); }); + it('should connect and disconnect a simple node', () => { + WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME); + WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true }); + cy.get('.jtk-connector').should('have.length', 1); + WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME); + + // Change connection from Set to Set1 + cy.draganddrop( + WorkflowPage.getters.getEndpointSelector('input', SET_NODE_NAME), + WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`) + ) + + WorkflowPage.getters.canvasNodeInputEndpointByName(`${SET_NODE_NAME}1`).should('have.class', 'jtk-endpoint-connected'); + + cy.get('.jtk-connector').should('have.length', 1); + // Disconnect Set1 + cy.drag(WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`), [-200, 100]) + cy.get('.jtk-connector').should('have.length', 0); + }); + it('should add first step', () => { WorkflowPage.getters.canvasPlusButton().should('be.visible'); WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.getters.canvasNodes().should('have.length', 1); }); + it('should add a node via plus endpoint drag', () => { + WorkflowPage.getters.canvasPlusButton().should('be.visible'); + WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, true); + + cy.drag(WorkflowPage.getters.getEndpointSelector('plus', SCHEDULE_TRIGGER_NODE_NAME), [100, 100]) + + WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); + WorkflowPage.actions.addNodeToCanvas(IF_NODE_NAME, false); + WorkflowPage.getters.nodeViewBackground().click({ force: true }); + }); + + it('should add switch node and test connections', () => { + WorkflowPage.actions.addNodeToCanvas(SWITCH_NODE_NAME, true); + + // Switch has 4 output endpoints + for (let i = 0; i < 4; i++) { + WorkflowPage.getters.canvasNodePlusEndpointByName(SWITCH_NODE_NAME, i).click({ force: true }) + WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); + WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, false); + WorkflowPage.actions.zoomToFit(); + } + WorkflowPage.actions.saveWorkflowOnButtonClick(); + cy.reload() + cy.waitForLoad(); + // Make sure all connections are there after reload + for (let i = 0; i < 4; i++) { + const setName = `${SET_NODE_NAME}${i > 0 ? i : ''}`; + WorkflowPage.getters.canvasNodeInputEndpointByName(setName).should('have.class', 'jtk-endpoint-connected'); + } + }); + + it('should add merge node and test connections', () => { + WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + for (let i = 0; i < 2; i++) { + WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, true); + WorkflowPage.getters.nodeViewBackground().click(600 + i * 100, 200, { force: true }); + } + WorkflowPage.actions.zoomToFit(); + + WorkflowPage.actions.addNodeToCanvas(MERGE_NODE_NAME); + WorkflowPage.actions.zoomToFit(); + + // Connect manual to Set1 + cy.draganddrop( + WorkflowPage.getters.getEndpointSelector('output', MANUAL_TRIGGER_NODE_DISPLAY_NAME), + WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`) + ) + + cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 2); + + // Connect Set1 and Set2 to merge + cy.draganddrop( + WorkflowPage.getters.getEndpointSelector('plus', SET_NODE_NAME), + WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 0) + ) + cy.draganddrop( + WorkflowPage.getters.getEndpointSelector('plus', `${SET_NODE_NAME}1`), + WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1) + ) + + cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4); + + // Make sure all connections are there after save & reload + WorkflowPage.actions.saveWorkflowOnButtonClick(); + cy.reload() + cy.waitForLoad(); + + cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4); + WorkflowPage.actions.executeWorkflow(); + // If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node + cy.get('[data-label="2 items"]').should('be.visible'); + }); + + it('should add nodes and check execution success', () => { + WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + for (let i = 0; i < 3; i++) { + WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, true); + } + WorkflowPage.actions.zoomToFit(); + WorkflowPage.actions.executeWorkflow(); + + cy.get('.jtk-connector.success').should('have.length', 3); + cy.get('.data-count').should('have.length', 4); + cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'); + + WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME); + WorkflowPage.actions.zoomToFit(); + + cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success'); + cy.get('.jtk-connector.success').should('have.length', 3); + cy.get('.jtk-connector').should('have.length', 4); + }) + it('should add a connected node using plus endpoint', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); cy.get('.plus-endpoint').should('be.visible').click(); @@ -226,7 +343,7 @@ describe('Canvas Actions', () => { it('should delete a connection by moving it away from endpoint', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - cy.drag('.rect-input-endpoint.jtk-endpoint-connected', [0, -100]); + cy.drag(WorkflowPage.getters.getEndpointSelector('input', CODE_NODE_NAME), [0, -100]); WorkflowPage.getters.nodeConnections().should('have.length', 0); }); diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index e8e8a99489..80ef05b3cc 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -24,6 +24,18 @@ export class WorkflowPage extends BasePage { canvasNodes: () => cy.getByTestId('canvas-node'), canvasNodeByName: (nodeName: string) => this.getters.canvasNodes().filter(`:contains("${nodeName}")`), + getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => { + return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']` + }, + canvasNodeInputEndpointByName: (nodeName: string, index = 0) => { + return cy.get(this.getters.getEndpointSelector('input', nodeName, index)); + }, + canvasNodeOutputEndpointByName: (nodeName: string, index = 0) => { + return cy.get(this.getters.getEndpointSelector('output', nodeName, index)); + }, + canvasNodePlusEndpointByName: (nodeName: string, index = 0) => { + return cy.get(this.getters.getEndpointSelector('plus', nodeName, index)); + }, successToast: () => cy.get('.el-notification .el-icon-success').parent(), errorToast: () => cy.get('.el-notification .el-icon-error'), activatorSwitch: () => cy.getByTestId('workflow-activate-switch'), @@ -122,7 +134,9 @@ export class WorkflowPage extends BasePage { this.getters.workflowMenu().click(); }, saveWorkflowOnButtonClick: () => { + this.getters.saveButton().should('contain', 'Save'); this.getters.saveButton().click(); + this.getters.saveButton().should('contain', 'Saved') }, saveWorkflowUsingKeyboardShortcut: () => { cy.get('body').type('{meta}', { release: false }).type('s'); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index eade824115..c5e9f898ce 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -187,23 +187,27 @@ Cypress.Commands.add('drag', (selector, pos) => { pageY: originalLocation.top + yDiff, force: true, }); - element.trigger('mouseup'); + element.trigger('mouseup', { force: true }); }); Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => { cy.get(draggableSelector).should('exist'); cy.get(droppableSelector).should('exist'); - const droppableEl = Cypress.$(droppableSelector)[0]; - const coords = droppableEl.getBoundingClientRect(); + cy.get(droppableSelector).first().then(([$el]) => { + const coords = $el.getBoundingClientRect(); - const pageX = coords.left + coords.width / 2; - const pageY = coords.top + coords.height / 2; + const pageX = coords.left + coords.width / 2; + const pageY = coords.top + coords.height / 2; - cy.get(draggableSelector).realMouseDown(); - cy.get(droppableSelector).realMouseMove(pageX, pageY) - .realHover() - .realMouseUp(); + // We can't use realMouseDown here because it hangs headless run + cy.get(draggableSelector).trigger('mousedown'); + // We don't chain these commands to make sure cy.get is re-trying correctly + cy.get(droppableSelector).realMouseMove(pageX, pageY) + cy.get(droppableSelector).realHover() + cy.get(droppableSelector).realMouseUp(); + cy.get(draggableSelector).realMouseUp(); + }) }); diff --git a/packages/editor-ui/src/mixins/nodeBase.ts b/packages/editor-ui/src/mixins/nodeBase.ts index b90ac49ff1..10e9f3b0d8 100644 --- a/packages/editor-ui/src/mixins/nodeBase.ts +++ b/packages/editor-ui/src/mixins/nodeBase.ts @@ -10,7 +10,7 @@ import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; import { useNodeTypesStore } from '@/stores/nodeTypes'; import { BrowserJsPlumbInstance } from '@jsplumb/browser-ui'; -import { EndpointOptions } from '@jsplumb/core'; +import { Endpoint, EndpointOptions } from '@jsplumb/core'; import * as NodeViewUtils from '@/utils/nodeViewUtils'; import { useHistoryStore } from '@/stores/history'; import { useCanvasStore } from '@/stores/canvas'; @@ -60,6 +60,14 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({ }, }, methods: { + __addEndpointTestingData(endpoint: Endpoint, type: string, inputIndex: number) { + if (window?.Cypress && 'canvas' in endpoint.endpoint) { + const canvas = endpoint.endpoint.canvas; + this.instance.setAttribute(canvas, 'data-endpoint-name', this.data.name); + this.instance.setAttribute(canvas, 'data-input-index', inputIndex.toString()); + this.instance.setAttribute(canvas, 'data-endpoint-type', type); + } + }, __addInputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) { // Add Inputs let index; @@ -104,6 +112,7 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({ this.$refs[this.data.name] as Element, newEndpointData, ); + this.__addEndpointTestingData(endpoint, 'input', index); if (nodeTypeData.inputNames) { // Apply input names if they got set endpoint.addOverlay(NodeViewUtils.getInputNameOverlay(nodeTypeData.inputNames[index])); @@ -155,7 +164,6 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({ uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index), anchor: anchorPosition, maxConnections: -1, - endpoint: { type: 'Dot', options: { @@ -185,10 +193,11 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({ this.$refs[this.data.name] as Element, newEndpointData, ); + this.__addEndpointTestingData(endpoint, 'output', index); if (nodeTypeData.outputNames) { // Apply output names if they got set const overlaySpec = NodeViewUtils.getOutputNameOverlay(nodeTypeData.outputNames[index]); - const overlay = endpoint.addOverlay(overlaySpec); + endpoint.addOverlay(overlaySpec); } if (!Array.isArray(endpoint)) { @@ -236,6 +245,7 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({ this.$refs[this.data.name] as Element, plusEndpointData, ); + this.__addEndpointTestingData(plusEndpoint, 'plus', index); if (!Array.isArray(plusEndpoint)) { plusEndpoint.__meta = {