mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-13 05:47:31 -08:00
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:
parent
389e4af4d9
commit
14d1743595
|
@ -4,9 +4,13 @@ export const DEFAULT_USER_EMAIL = 'nathan@n8n.io';
|
||||||
export const DEFAULT_USER_PASSWORD = 'CypressTest123';
|
export const DEFAULT_USER_PASSWORD = 'CypressTest123';
|
||||||
|
|
||||||
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
|
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 SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
||||||
export const CODE_NODE_NAME = 'Code';
|
export const CODE_NODE_NAME = 'Code';
|
||||||
export const SET_NODE_NAME = 'Set';
|
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 GMAIL_NODE_NAME = 'Gmail';
|
||||||
export const TRELLO_NODE_NAME = 'Trello';
|
export const TRELLO_NODE_NAME = 'Trello';
|
||||||
export const NOTION_NODE_NAME = 'Notion';
|
export const NOTION_NODE_NAME = 'Notion';
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import {
|
import {
|
||||||
MANUAL_TRIGGER_NODE_NAME,
|
MANUAL_TRIGGER_NODE_NAME,
|
||||||
|
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||||
CODE_NODE_NAME,
|
CODE_NODE_NAME,
|
||||||
SCHEDULE_TRIGGER_NODE_NAME,
|
SCHEDULE_TRIGGER_NODE_NAME,
|
||||||
SET_NODE_NAME,
|
SET_NODE_NAME,
|
||||||
|
SWITCH_NODE_NAME,
|
||||||
|
IF_NODE_NAME,
|
||||||
|
MERGE_NODE_NAME,
|
||||||
HTTP_REQUEST_NODE_NAME,
|
HTTP_REQUEST_NODE_NAME,
|
||||||
} from './../constants';
|
} from './../constants';
|
||||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
|
@ -33,12 +37,125 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.getters.executeWorkflowButton().should('be.visible');
|
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', () => {
|
it('should add first step', () => {
|
||||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||||
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 1);
|
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', () => {
|
it('should add a connected node using plus endpoint', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
cy.get('.plus-endpoint').should('be.visible').click();
|
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', () => {
|
it('should delete a connection by moving it away from endpoint', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_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);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,18 @@ export class WorkflowPage extends BasePage {
|
||||||
canvasNodes: () => cy.getByTestId('canvas-node'),
|
canvasNodes: () => cy.getByTestId('canvas-node'),
|
||||||
canvasNodeByName: (nodeName: string) =>
|
canvasNodeByName: (nodeName: string) =>
|
||||||
this.getters.canvasNodes().filter(`:contains("${nodeName}")`),
|
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(),
|
successToast: () => cy.get('.el-notification .el-icon-success').parent(),
|
||||||
errorToast: () => cy.get('.el-notification .el-icon-error'),
|
errorToast: () => cy.get('.el-notification .el-icon-error'),
|
||||||
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
|
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
|
||||||
|
@ -122,7 +134,9 @@ export class WorkflowPage extends BasePage {
|
||||||
this.getters.workflowMenu().click();
|
this.getters.workflowMenu().click();
|
||||||
},
|
},
|
||||||
saveWorkflowOnButtonClick: () => {
|
saveWorkflowOnButtonClick: () => {
|
||||||
|
this.getters.saveButton().should('contain', 'Save');
|
||||||
this.getters.saveButton().click();
|
this.getters.saveButton().click();
|
||||||
|
this.getters.saveButton().should('contain', 'Saved')
|
||||||
},
|
},
|
||||||
saveWorkflowUsingKeyboardShortcut: () => {
|
saveWorkflowUsingKeyboardShortcut: () => {
|
||||||
cy.get('body').type('{meta}', { release: false }).type('s');
|
cy.get('body').type('{meta}', { release: false }).type('s');
|
||||||
|
|
|
@ -187,23 +187,27 @@ Cypress.Commands.add('drag', (selector, pos) => {
|
||||||
pageY: originalLocation.top + yDiff,
|
pageY: originalLocation.top + yDiff,
|
||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
element.trigger('mouseup');
|
element.trigger('mouseup', { force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => {
|
Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => {
|
||||||
cy.get(draggableSelector).should('exist');
|
cy.get(draggableSelector).should('exist');
|
||||||
cy.get(droppableSelector).should('exist');
|
cy.get(droppableSelector).should('exist');
|
||||||
|
|
||||||
const droppableEl = Cypress.$(droppableSelector)[0];
|
cy.get(droppableSelector).first().then(([$el]) => {
|
||||||
const coords = droppableEl.getBoundingClientRect();
|
const coords = $el.getBoundingClientRect();
|
||||||
|
|
||||||
const pageX = coords.left + coords.width / 2;
|
const pageX = coords.left + coords.width / 2;
|
||||||
const pageY = coords.top + coords.height / 2;
|
const pageY = coords.top + coords.height / 2;
|
||||||
|
|
||||||
cy.get(draggableSelector).realMouseDown();
|
// 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).realMouseMove(pageX, pageY)
|
||||||
.realHover()
|
cy.get(droppableSelector).realHover()
|
||||||
.realMouseUp();
|
cy.get(droppableSelector).realMouseUp();
|
||||||
|
cy.get(draggableSelector).realMouseUp();
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useUIStore } from '@/stores/ui';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows';
|
import { useWorkflowsStore } from '@/stores/workflows';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
||||||
import { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
import { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
||||||
import { EndpointOptions } from '@jsplumb/core';
|
import { Endpoint, EndpointOptions } from '@jsplumb/core';
|
||||||
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||||
import { useHistoryStore } from '@/stores/history';
|
import { useHistoryStore } from '@/stores/history';
|
||||||
import { useCanvasStore } from '@/stores/canvas';
|
import { useCanvasStore } from '@/stores/canvas';
|
||||||
|
@ -60,6 +60,14 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
__addInputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) {
|
||||||
// Add Inputs
|
// Add Inputs
|
||||||
let index;
|
let index;
|
||||||
|
@ -104,6 +112,7 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
this.$refs[this.data.name] as Element,
|
this.$refs[this.data.name] as Element,
|
||||||
newEndpointData,
|
newEndpointData,
|
||||||
);
|
);
|
||||||
|
this.__addEndpointTestingData(endpoint, 'input', index);
|
||||||
if (nodeTypeData.inputNames) {
|
if (nodeTypeData.inputNames) {
|
||||||
// Apply input names if they got set
|
// Apply input names if they got set
|
||||||
endpoint.addOverlay(NodeViewUtils.getInputNameOverlay(nodeTypeData.inputNames[index]));
|
endpoint.addOverlay(NodeViewUtils.getInputNameOverlay(nodeTypeData.inputNames[index]));
|
||||||
|
@ -155,7 +164,6 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
|
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
|
||||||
anchor: anchorPosition,
|
anchor: anchorPosition,
|
||||||
maxConnections: -1,
|
maxConnections: -1,
|
||||||
|
|
||||||
endpoint: {
|
endpoint: {
|
||||||
type: 'Dot',
|
type: 'Dot',
|
||||||
options: {
|
options: {
|
||||||
|
@ -185,10 +193,11 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
this.$refs[this.data.name] as Element,
|
this.$refs[this.data.name] as Element,
|
||||||
newEndpointData,
|
newEndpointData,
|
||||||
);
|
);
|
||||||
|
this.__addEndpointTestingData(endpoint, 'output', index);
|
||||||
if (nodeTypeData.outputNames) {
|
if (nodeTypeData.outputNames) {
|
||||||
// Apply output names if they got set
|
// Apply output names if they got set
|
||||||
const overlaySpec = NodeViewUtils.getOutputNameOverlay(nodeTypeData.outputNames[index]);
|
const overlaySpec = NodeViewUtils.getOutputNameOverlay(nodeTypeData.outputNames[index]);
|
||||||
const overlay = endpoint.addOverlay(overlaySpec);
|
endpoint.addOverlay(overlaySpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(endpoint)) {
|
if (!Array.isArray(endpoint)) {
|
||||||
|
@ -236,6 +245,7 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
this.$refs[this.data.name] as Element,
|
this.$refs[this.data.name] as Element,
|
||||||
plusEndpointData,
|
plusEndpointData,
|
||||||
);
|
);
|
||||||
|
this.__addEndpointTestingData(plusEndpoint, 'plus', index);
|
||||||
|
|
||||||
if (!Array.isArray(plusEndpoint)) {
|
if (!Array.isArray(plusEndpoint)) {
|
||||||
plusEndpoint.__meta = {
|
plusEndpoint.__meta = {
|
||||||
|
|
Loading…
Reference in a new issue