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
This commit is contained in:
OlegIvaniv 2023-02-08 14:48:28 +01:00 committed by GitHub
parent 389e4af4d9
commit 14d1743595
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 162 additions and 13 deletions

View file

@ -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';

View file

@ -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);
});

View file

@ -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');

View file

@ -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();
})
});

View file

@ -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 = {