test: Enable Canvas V2 E2E Testing (#11321)

Co-authored-by: Alex Grozav <alex@grozav.com>
This commit is contained in:
oleg 2024-10-21 15:13:09 +02:00 committed by GitHub
parent c0b5b92f62
commit de04c93f2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 356 additions and 161 deletions

View file

@ -41,6 +41,11 @@ on:
description: 'PR number to run tests for.'
required: false
type: number
node_view_version:
description: 'Node View version to run tests with.'
required: false
default: '1'
type: string
secrets:
CYPRESS_RECORD_KEY:
description: 'Cypress record key.'
@ -160,6 +165,7 @@ jobs:
spec: '${{ inputs.spec }}'
env:
NODE_OPTIONS: --dns-result-order=ipv4first
CYPRESS_NODE_VIEW_VERSION: ${{ inputs.node_view_version }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
E2E_TESTS: true

View file

@ -27,6 +27,11 @@ on:
description: 'URL to call after workflow is done.'
required: false
default: ''
node_view_version:
description: 'Node View version to run tests with.'
required: false
default: '1'
type: string
jobs:
calls-start-url:
@ -46,6 +51,7 @@ jobs:
branch: ${{ github.event.inputs.branch || 'master' }}
user: ${{ github.event.inputs.user || 'PR User' }}
spec: ${{ github.event.inputs.spec || 'e2e/*' }}
node_view_version: ${{ github.event.inputs.node_view_version || '1' }}
secrets:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

View file

@ -20,6 +20,7 @@ describe('Undo/Redo', () => {
WorkflowPage.actions.visit();
});
// FIXME: Canvas V2: Fix redo connections
it('should undo/redo adding node in the middle', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -114,6 +115,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});
// FIXME: Canvas V2: Fix moving of nodes via e2e tests
it('should undo/redo moving nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -146,18 +148,14 @@ describe('Undo/Redo', () => {
it('should undo/redo deleting a connection using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().realHover();
cy.get('.connection-actions .delete')
.filter(':visible')
.should('be.visible')
.click({ force: true });
WorkflowPage.actions.deleteNodeBetweenNodes(SCHEDULE_TRIGGER_NODE_NAME, CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});
// FIXME: Canvas V2: Fix disconnecting by moving
it('should undo/redo deleting a connection by moving it away', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -206,6 +204,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.disabledNodes().should('have.length', 2);
});
// FIXME: Canvas V2: Fix undo renaming node
it('should undo/redo renaming node using keyboard shortcut', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -244,6 +243,7 @@ describe('Undo/Redo', () => {
});
});
// FIXME: Canvas V2: Figure out why moving doesn't work from e2e
it('should undo/redo multiple steps', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);

View file

@ -16,6 +16,7 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.visit();
});
// FIXME: Canvas V2: Missing execute button if no nodes
it('should render canvas', () => {
WorkflowPage.getters.nodeViewRoot().should('be.visible');
WorkflowPage.getters.canvasPlusButton().should('be.visible');
@ -25,10 +26,11 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.executeWorkflowButton().should('be.visible');
});
// FIXME: Canvas V2: Fix changing of connection
it('should connect and disconnect a simple node', () => {
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
cy.get('.jtk-connector').should('have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.getters.nodeViewBackground().click(600, 400, { force: true });
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
@ -40,16 +42,16 @@ describe('Canvas Actions', () => {
);
WorkflowPage.getters
.canvasNodeInputEndpointByName(`${EDIT_FIELDS_SET_NODE_NAME}1`)
.should('have.class', 'jtk-endpoint-connected');
.getConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, `${EDIT_FIELDS_SET_NODE_NAME}1`)
.should('be.visible');
cy.get('.jtk-connector').should('have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
// Disconnect Set1
cy.drag(
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
[-200, 100],
);
cy.get('.jtk-connector').should('have.length', 0);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});
it('should add first step', () => {
@ -74,7 +76,7 @@ describe('Canvas Actions', () => {
it('should add a connected node using plus endpoint', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
cy.get('.plus-endpoint').should('be.visible').click();
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}');
@ -85,7 +87,7 @@ describe('Canvas Actions', () => {
it('should add a connected node dragging from node creator', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
cy.get('.plus-endpoint').should('be.visible').click();
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
cy.drag(WorkflowPage.getters.nodeCreatorNodeItems().first(), [100, 100], {
@ -99,7 +101,7 @@ describe('Canvas Actions', () => {
it('should open a category when trying to drag and drop it on the canvas', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
cy.get('.plus-endpoint').should('be.visible').click();
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
cy.drag(WorkflowPage.getters.nodeCreatorActionItems().first(), [100, 100], {
@ -114,7 +116,7 @@ describe('Canvas Actions', () => {
it('should add disconnected node if nothing is selected', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
// Deselect nodes
WorkflowPage.getters.nodeViewBackground().click({ force: true });
WorkflowPage.getters.nodeView().click({ force: true });
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
@ -136,10 +138,10 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 3);
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left'));
const editFieldsNodeLeft = WorkflowPage.getters.getNodeLeftPosition($editFieldsNode);
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
const httpNodeLeft = parseFloat($httpNode.css('left'));
const httpNodeLeft = WorkflowPage.getters.getNodeLeftPosition($httpNode);
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
});
});
@ -159,10 +161,12 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().first().realHover();
cy.get('.connection-actions .delete').first().click({ force: true });
WorkflowPage.actions.deleteNodeBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});
// FIXME: Canvas V2: Fix disconnecting of connection by dragging it
it('should delete a connection by moving it away from endpoint', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
@ -216,10 +220,10 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitCopy();
successToast().should('contain', 'Copied!');
successToast().should('contain', 'Copied to clipboard');
WorkflowPage.actions.copyNode(CODE_NODE_NAME);
successToast().should('contain', 'Copied!');
successToast().should('contain', 'Copied to clipboard');
});
it('should select/deselect all nodes', () => {
@ -231,17 +235,31 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.selectedNodes().should('have.length', 0);
});
// FIXME: Canvas V2: Selection via arrow keys is broken
it('should select nodes using arrow keys', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500);
cy.get('body').type('{leftArrow}');
WorkflowPage.getters.canvasNodes().first().should('have.class', 'jtk-drag-selected');
const selectedCanvasNodes = () =>
cy.ifCanvasVersion(
() => WorkflowPage.getters.canvasNodes(),
() => WorkflowPage.getters.canvasNodes().parent(),
);
cy.ifCanvasVersion(
() => selectedCanvasNodes().first().should('have.class', 'jtk-drag-selected'),
() => selectedCanvasNodes().first().should('have.class', 'selected'),
);
cy.get('body').type('{rightArrow}');
WorkflowPage.getters.canvasNodes().last().should('have.class', 'jtk-drag-selected');
cy.ifCanvasVersion(
() => selectedCanvasNodes().last().should('have.class', 'jtk-drag-selected'),
() => selectedCanvasNodes().last().should('have.class', 'selected'),
);
});
// FIXME: Canvas V2: Selection via shift and arrow keys is broken
it('should select nodes using shift and arrow keys', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
@ -251,6 +269,7 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.selectedNodes().should('have.length', 2);
});
// FIXME: Canvas V2: Fix select & deselect
it('should not break lasso selection when dragging node action buttons', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters
@ -262,6 +281,7 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
});
// FIXME: Canvas V2: Fix select & deselect
it('should not break lasso selection with multiple clicks on node action buttons', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);

View file

@ -9,6 +9,7 @@ import {
} from './../constants';
import { NDV, WorkflowExecutionsTab } from '../pages';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { isCanvasV2 } from '../utils/workflowUtils';
const WorkflowPage = new WorkflowPageClass();
const ExecutionsTab = new WorkflowExecutionsTab();
@ -52,15 +53,15 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.reload();
cy.waitForLoad();
// Make sure outputless switch was connected correctly
cy.get(
`[data-target-node="${SWITCH_NODE_NAME}1"][data-source-node="${EDIT_FIELDS_SET_NODE_NAME}3"]`,
).should('be.visible');
WorkflowPage.getters
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}3`, `${SWITCH_NODE_NAME}1`)
.should('exist');
// Make sure all connections are there after reload
for (let i = 0; i < desiredOutputs; i++) {
const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`;
WorkflowPage.getters
.canvasNodeInputEndpointByName(setName)
.should('have.class', 'jtk-endpoint-connected');
.getConnectionBetweenNodes(`${SWITCH_NODE_NAME}`, setName)
.should('exist');
}
});
@ -69,9 +70,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
for (let i = 0; i < 2; i++) {
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
WorkflowPage.getters
.nodeViewBackground()
.click((i + 1) * 200, (i + 1) * 200, { force: true });
WorkflowPage.getters.nodeView().click((i + 1) * 200, (i + 1) * 200, { force: true });
}
WorkflowPage.actions.zoomToFit();
@ -84,8 +83,6 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_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', EDIT_FIELDS_SET_NODE_NAME),
@ -95,20 +92,36 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`),
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
);
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
const checkConnections = () => {
WorkflowPage.getters
.getConnectionBetweenNodes(
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
`${EDIT_FIELDS_SET_NODE_NAME}1`,
)
.should('exist');
WorkflowPage.getters
.getConnectionBetweenNodes(EDIT_FIELDS_SET_NODE_NAME, MERGE_NODE_NAME)
.should('exist');
WorkflowPage.getters
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}1`, MERGE_NODE_NAME)
.should('exist');
};
checkConnections();
// 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);
checkConnections();
// cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
WorkflowPage.actions.executeWorkflow();
WorkflowPage.getters.stopExecutionButton().should('not.exist');
// 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');
cy.ifCanvasVersion(
() => cy.get('[data-label="2 items"]').should('be.visible'),
() => cy.getByTestId('canvas-node-output-handle').contains('2 items').should('be.visible'),
);
});
it('should add nodes and check execution success', () => {
@ -120,16 +133,42 @@ describe('Canvas Node Manipulation and Navigation', () => {
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');
cy.ifCanvasVersion(
() => cy.get('.jtk-connector.success').should('have.length', 3),
() => cy.get('[data-edge-status=success]').should('have.length', 3),
);
cy.ifCanvasVersion(
() => cy.get('.data-count').should('have.length', 4),
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
);
cy.ifCanvasVersion(
() => cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'),
() => cy.getByTestId('canvas-handle-plus').should('have.attr', 'data-plus-type', 'success'),
);
WorkflowPage.actions.addNodeToCanvas(CODE_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);
cy.ifCanvasVersion(
() =>
cy
.get('.plus-draggable-endpoint')
.filter(':visible')
.should('not.have.class', 'ep-success'),
() =>
cy.getByTestId('canvas-handle-plus').should('not.have.attr', 'data-plus-type', 'success'),
);
cy.ifCanvasVersion(
() => cy.get('.jtk-connector.success').should('have.length', 3),
// The new version of the canvas correctly shows executed data being passed to the input of the next node
() => cy.get('[data-edge-status=success]').should('have.length', 4),
);
cy.ifCanvasVersion(
() => cy.get('.data-count').should('have.length', 4),
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
);
});
it('should delete node using context menu', () => {
@ -194,19 +233,29 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodes().should('have.length', 0);
});
// FIXME: Canvas V2: Figure out how to test moving of the node
it('should move node', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.zoomToFit();
WorkflowPage.getters
.canvasNodes()
.last()
.then(($node) => {
const { left, top } = $node.position();
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
clickToFinish: true,
});
if (isCanvasV2()) {
cy.drag('.vue-flow__node', [300, 300], {
realMouse: true,
});
} else {
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
clickToFinish: true,
});
}
WorkflowPage.getters
.canvasNodes()
.last()
@ -218,91 +267,80 @@ describe('Canvas Node Manipulation and Navigation', () => {
});
});
it('should zoom in', () => {
WorkflowPage.getters.zoomInButton().should('be.visible').click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${ZOOM_IN_X1_FACTOR}, 0, 0, ${ZOOM_IN_X1_FACTOR}, 0, 0)`,
describe('Canvas Zoom Functionality', () => {
const getContainer = () =>
cy.ifCanvasVersion(
() => WorkflowPage.getters.nodeView(),
() => WorkflowPage.getters.canvasViewport(),
);
WorkflowPage.getters.zoomInButton().click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${ZOOM_IN_X2_FACTOR}, 0, 0, ${ZOOM_IN_X2_FACTOR}, 0, 0)`,
);
});
const checkZoomLevel = (expectedFactor: number) => {
return getContainer().should(($nodeView) => {
const newTransform = $nodeView.css('transform');
const newScale = parseFloat(newTransform.split(',')[0].slice(7));
it('should zoom out', () => {
WorkflowPage.getters.zoomOutButton().should('be.visible').click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${ZOOM_OUT_X1_FACTOR}, 0, 0, ${ZOOM_OUT_X1_FACTOR}, 0, 0)`,
);
WorkflowPage.getters.zoomOutButton().click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${ZOOM_OUT_X2_FACTOR}, 0, 0, ${ZOOM_OUT_X2_FACTOR}, 0, 0)`,
);
});
expect(newScale).to.be.closeTo(expectedFactor, 0.2);
});
};
it('should zoom using scroll or pinch gesture', () => {
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${PINCH_ZOOM_IN_FACTOR}, 0, 0, ${PINCH_ZOOM_IN_FACTOR}, 0, 0)`,
const zoomAndCheck = (action: 'zoomIn' | 'zoomOut', expectedFactor: number) => {
WorkflowPage.getters[`${action}Button`]().click();
checkZoomLevel(expectedFactor);
};
it('should zoom in', () => {
WorkflowPage.getters.zoomInButton().should('be.visible');
getContainer().then(($nodeView) => {
const initialTransform = $nodeView.css('transform');
const initialScale =
initialTransform === 'none' ? 1 : parseFloat(initialTransform.split(',')[0].slice(7));
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X1_FACTOR);
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X2_FACTOR);
});
});
it('should zoom out', () => {
zoomAndCheck('zoomOut', ZOOM_OUT_X1_FACTOR);
zoomAndCheck('zoomOut', ZOOM_OUT_X2_FACTOR);
});
it('should zoom using scroll or pinch gesture', () => {
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
// V2 Canvas is using the same zoom factor for both pinch and scroll
cy.ifCanvasVersion(
() => checkZoomLevel(PINCH_ZOOM_IN_FACTOR),
() => checkZoomLevel(ZOOM_IN_X1_FACTOR),
);
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
// Zoom in 1x + Zoom out 1x should reset to default (=1)
WorkflowPage.getters.nodeView().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)');
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
checkZoomLevel(1); // Zoom in 1x + Zoom out 1x should reset to default (=1)
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${PINCH_ZOOM_OUT_FACTOR}, 0, 0, ${PINCH_ZOOM_OUT_FACTOR}, 0, 0)`,
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
cy.ifCanvasVersion(
() => checkZoomLevel(PINCH_ZOOM_OUT_FACTOR),
() => checkZoomLevel(ZOOM_OUT_X1_FACTOR),
);
});
});
it('should reset zoom', () => {
// Reset zoom should not appear until zoom level changed
WorkflowPage.getters.resetZoomButton().should('not.exist');
WorkflowPage.getters.zoomInButton().click();
WorkflowPage.getters.resetZoomButton().should('be.visible').click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${DEFAULT_ZOOM_FACTOR}, 0, 0, ${DEFAULT_ZOOM_FACTOR}, 0, 0)`,
);
});
it('should reset zoom', () => {
WorkflowPage.getters.resetZoomButton().should('not.exist');
WorkflowPage.getters.zoomInButton().click();
WorkflowPage.getters.resetZoomButton().should('be.visible').click();
checkZoomLevel(DEFAULT_ZOOM_FACTOR);
});
it('should zoom to fit', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
// At this point last added node should be off-screen
WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
WorkflowPage.getters.zoomToFitButton().click();
WorkflowPage.getters.canvasNodes().last().should('be.visible');
it('should zoom to fit', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
// At this point last added node should be off-screen
WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
WorkflowPage.getters.zoomToFitButton().click();
WorkflowPage.getters.canvasNodes().last().should('be.visible');
});
});
it('should disable node (context menu or shortcut)', () => {
@ -426,9 +464,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.reload();
cy.waitForLoad();
WorkflowPage.getters.canvasNodes().should('have.length', 2);
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
});
// FIXME: Canvas V2: Credentials should show issue on the first open
it('should remove unknown credentials on pasting workflow', () => {
cy.fixture('workflow-with-unknown-credentials.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
@ -441,6 +479,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
});
});
// FIXME: Canvas V2: Unknown nodes should still render connection endpoints
it('should render connections correctly if unkown nodes are present', () => {
const unknownNodeName = 'Unknown node';
cy.createFixtureWorkflow('workflow-with-unknown-nodes.json', 'Unknown nodes');

View file

@ -1,6 +1,7 @@
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { clearNotifications, errorToast, successToast } from '../pages/notifications';
import { isCanvasV2 } from '../utils/workflowUtils';
const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();
@ -117,15 +118,22 @@ describe('Execution', () => {
.canvasNodeByName('Manual')
.within(() => cy.get('.fa-check'))
.should('exist');
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
if (isCanvasV2()) {
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
} else {
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
}
workflowPage.getters
.canvasNodeByName('Set')
.within(() => cy.get('.fa-check').should('not.exist'));
successToast().should('be.visible');
clearNotifications();
// Clear execution data
workflowPage.getters.clearExecutionDataButton().should('be.visible');
@ -206,6 +214,7 @@ describe('Execution', () => {
workflowPage.getters.clearExecutionDataButton().should('not.exist');
});
// FIXME: Canvas V2: Webhook should show waiting state but it doesn't
it('should test webhook workflow stop', () => {
cy.createFixtureWorkflow('Webhook_wait_set.json');
@ -267,9 +276,17 @@ describe('Execution', () => {
.canvasNodeByName('Webhook')
.within(() => cy.get('.fa-check'))
.should('exist');
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
if (isCanvasV2()) {
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
} else {
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
}
workflowPage.getters
.canvasNodeByName('Set')
.within(() => cy.get('.fa-check').should('not.exist'));
@ -295,6 +312,7 @@ describe('Execution', () => {
});
});
// FIXME: Canvas V2: Missing pinned states for `edge-label-wrapper`
describe('connections should be colored differently for pinned data', () => {
beforeEach(() => {
cy.createFixtureWorkflow('Schedule_pinned.json');

View file

@ -2,7 +2,7 @@ import { BasePage } from './base';
import { NodeCreator } from './features/node-creator';
import { META_KEY } from '../constants';
import { getVisibleSelect } from '../utils';
import { getUniqueWorkflowName } from '../utils/workflowUtils';
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';
const nodeCreator = new NodeCreator();
export class WorkflowPage extends BasePage {
@ -27,7 +27,11 @@ export class WorkflowPage extends BasePage {
nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'),
nodeCreatorPlusButton: () => cy.getByTestId('node-creator-plus-button'),
canvasPlusButton: () => cy.getByTestId('canvas-plus-button'),
canvasNodes: () => cy.getByTestId('canvas-node'),
canvasNodes: () =>
cy.ifCanvasVersion(
() => cy.getByTestId('canvas-node'),
() => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'),
),
canvasNodeByName: (nodeName: string) =>
this.getters.canvasNodes().filter(`:contains(${nodeName})`),
nodeIssuesByName: (nodeName: string) =>
@ -37,6 +41,17 @@ export class WorkflowPage extends BasePage {
.should('have.length.greaterThan', 0)
.findChildByTestId('node-issues'),
getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => {
if (isCanvasV2()) {
if (type === 'input') {
return `[data-test-id="canvas-node-input-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`;
}
if (type === 'output') {
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`;
}
if (type === 'plus') {
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"] [data-test-id="canvas-handle-plus"] .clickable`;
}
}
return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`;
},
canvasNodeInputEndpointByName: (nodeName: string, index = 0) => {
@ -46,7 +61,15 @@ export class WorkflowPage extends BasePage {
return cy.get(this.getters.getEndpointSelector('output', nodeName, index));
},
canvasNodePlusEndpointByName: (nodeName: string, index = 0) => {
return cy.get(this.getters.getEndpointSelector('plus', nodeName, index));
return cy.ifCanvasVersion(
() => cy.get(this.getters.getEndpointSelector('plus', nodeName, index)),
() =>
cy
.get(
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"] .clickable`,
)
.eq(index),
);
},
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
workflowMenu: () => cy.getByTestId('workflow-menu'),
@ -56,13 +79,29 @@ export class WorkflowPage extends BasePage {
expressionModalInput: () => cy.getByTestId('expression-modal-input').find('[role=textbox]'),
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
nodeViewRoot: () => cy.getByTestId('node-view-root'),
nodeViewRoot: () =>
cy.ifCanvasVersion(
() => cy.getByTestId('node-view-root'),
() => this.getters.nodeView(),
),
copyPasteInput: () => cy.getByTestId('hidden-copy-paste'),
nodeConnections: () => cy.get('.jtk-connector'),
nodeConnections: () =>
cy.ifCanvasVersion(
() => cy.get('.jtk-connector'),
() => cy.getByTestId('edge-label-wrapper'),
),
zoomToFitButton: () => cy.getByTestId('zoom-to-fit'),
nodeEndpoints: () => cy.get('.jtk-endpoint-connected'),
disabledNodes: () => cy.get('.node-box.disabled'),
selectedNodes: () => this.getters.canvasNodes().filter('.jtk-drag-selected'),
disabledNodes: () =>
cy.ifCanvasVersion(
() => cy.get('.node-box.disabled'),
() => cy.get('[data-test-id="canvas-trigger-node"][class*="disabled"]'),
),
selectedNodes: () =>
cy.ifCanvasVersion(
() => this.getters.canvasNodes().filter('.jtk-drag-selected'),
() => this.getters.canvasNodes().parent().filter('.selected'),
),
// Workflow menu items
workflowMenuItemDuplicate: () => cy.getByTestId('workflow-menu-item-duplicate'),
workflowMenuItemDownload: () => cy.getByTestId('workflow-menu-item-download'),
@ -92,8 +131,21 @@ export class WorkflowPage extends BasePage {
shareButton: () => cy.getByTestId('workflow-share-button'),
duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'),
nodeViewBackground: () => cy.getByTestId('node-view-background'),
nodeView: () => cy.getByTestId('node-view'),
nodeViewBackground: () =>
cy.ifCanvasVersion(
() => cy.getByTestId('node-view-background'),
() => cy.getByTestId('canvas'),
),
nodeView: () =>
cy.ifCanvasVersion(
() => cy.getByTestId('node-view'),
() => cy.get('[data-test-id="canvas-wrapper"]'),
),
canvasViewport: () =>
cy.ifCanvasVersion(
() => cy.getByTestId('node-view'),
() => cy.get('.vue-flow__transformationpane.vue-flow__container'),
),
inlineExpressionEditorInput: () =>
cy.getByTestId('inline-expression-editor-input').find('[role=textbox]'),
inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'),
@ -115,12 +167,26 @@ export class WorkflowPage extends BasePage {
ndvParameters: () => cy.getByTestId('parameter-item'),
nodeCredentialsLabel: () => cy.getByTestId('credentials-label'),
getConnectionBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
cy.get(
`.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
cy.ifCanvasVersion(
() =>
cy.get(
`.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
),
() =>
cy.get(
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`,
),
),
getConnectionActionsBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
cy.get(
`.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
cy.ifCanvasVersion(
() =>
cy.get(
`.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
),
() =>
cy.get(
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`,
),
),
addStickyButton: () => cy.getByTestId('add-sticky-button'),
stickies: () => cy.getByTestId('sticky'),
@ -128,6 +194,18 @@ export class WorkflowPage extends BasePage {
workflowHistoryButton: () => cy.getByTestId('workflow-history-button'),
colors: () => cy.getByTestId('color'),
contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`),
getNodeLeftPosition: (element: JQuery<HTMLElement>) => {
if (isCanvasV2()) {
return parseFloat(element.parent().css('transform').split(',')[4]);
}
return parseFloat(element.css('left'));
},
getNodeTopPosition: (element: JQuery<HTMLElement>) => {
if (isCanvasV2()) {
return parseFloat(element.parent().css('transform').split(',')[5]);
}
return parseFloat(element.css('top'));
},
};
actions = {
@ -332,7 +410,7 @@ export class WorkflowPage extends BasePage {
pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => {
cy.window().then((win) => {
// Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling)
this.getters.nodeViewBackground().trigger('wheel', {
this.getters.nodeView().trigger('wheel', {
force: true,
bubbles: true,
ctrlKey: true,
@ -391,9 +469,12 @@ export class WorkflowPage extends BasePage {
action?: string,
) => {
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
this.getters
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
.find('.add')
const connectionsBetweenNodes = () =>
this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName);
cy.ifCanvasVersion(
() => connectionsBetweenNodes().find('.add'),
() => connectionsBetweenNodes().get('[data-test-id="add-connection-button"]'),
)
.first()
.click({ force: true });
@ -401,9 +482,12 @@ export class WorkflowPage extends BasePage {
},
deleteNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string) => {
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
this.getters
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
.find('.delete')
const connectionsBetweenNodes = () =>
this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName);
cy.ifCanvasVersion(
() => connectionsBetweenNodes().find('.delete'),
() => connectionsBetweenNodes().get('[data-test-id="delete-connection-button"]'),
)
.first()
.click({ force: true });
},

View file

@ -10,7 +10,7 @@ import {
N8N_AUTH_COOKIE,
} from '../constants';
import { WorkflowPage } from '../pages';
import { getUniqueWorkflowName } from '../utils/workflowUtils';
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';
Cypress.Commands.add('setAppDate', (targetDate: number | Date) => {
cy.window().then((win) => {
@ -26,6 +26,10 @@ Cypress.Commands.add('getByTestId', (selector, ...args) => {
return cy.get(`[data-test-id="${selector}"]`, ...args);
});
Cypress.Commands.add('ifCanvasVersion', (getterV1, getterV2) => {
return isCanvasV2() ? getterV2() : getterV1();
});
Cypress.Commands.add(
'createFixtureWorkflow',
(fixtureKey: string, workflowName = getUniqueWorkflowName()) => {

View file

@ -20,6 +20,11 @@ beforeEach(() => {
win.localStorage.setItem('N8N_THEME', 'light');
win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true');
win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true');
const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION');
if (nodeViewVersion) {
win.localStorage.setItem('NodeView.version', nodeViewVersion);
}
});
cy.intercept('GET', '/rest/settings', (req) => {

View file

@ -28,6 +28,7 @@ declare global {
selector: string,
...args: Array<Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined>
): Chainable<JQuery<HTMLElement>>;
ifCanvasVersion<T1, T2>(getterV1: () => T1, getterV2: () => T2): T1 | T2;
findChildByTestId(childTestId: string): Chainable<JQuery<HTMLElement>>;
/**
* Creates a workflow from the given fixture and optionally renames it.

View file

@ -3,3 +3,7 @@ import { nanoid } from 'nanoid';
export function getUniqueWorkflowName(workflowNamePrefix?: string) {
return workflowNamePrefix ? `${workflowNamePrefix} ${nanoid(12)}` : nanoid(12);
}
export function isCanvasV2() {
return Cypress.env('NODE_VIEW_VERSION') === 2;
}

View file

@ -78,14 +78,14 @@ watch(
void externalHooks.run('expressionEdit.dialogVisibleChanged', {
dialogVisible: newValue,
parameter: props.parameter,
value: props.modelValue,
value: props.modelValue.toString(),
resolvedExpressionValue,
});
if (!newValue) {
const telemetryPayload = createExpressionTelemetryPayload(
segments.value,
props.modelValue,
props.modelValue.toString(),
workflowsStore.workflowId,
ndvStore.pushRef,
ndvStore.activeNode?.type ?? '',

View file

@ -668,7 +668,7 @@ onBeforeUnmount(() => {
width="auto"
:append-to="`#${APP_MODALS_ELEMENT_ID}`"
data-test-id="ndv"
z-index="1800"
:z-index="1800"
:data-has-output-connection="hasOutputConnection"
>
<n8n-tooltip

View file

@ -713,7 +713,8 @@ const populateSettings = () => {
},
],
default: 'stopWorkflow',
noDataExpression: i18n.baseText('nodeSettings.onError.description'),
description: i18n.baseText('nodeSettings.onError.description'),
noDataExpression: true,
},
] as INodeProperties[]),
);

View file

@ -52,7 +52,7 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping(
</script>
<template>
<div :class="$style.wrapper">
<div :class="$style.wrapper" data-test-id="canvas-wrapper">
<div :class="$style.canvas">
<Canvas
v-if="workflow"

View file

@ -117,6 +117,9 @@ function onDelete() {
<EdgeLabelRenderer>
<div
data-test-id="edge-label-wrapper"
:data-source-node-name="sourceNode?.label"
:data-target-node-name="targetNode?.label"
:data-edge-status="status"
:style="edgeToolbarStyle"
:class="$style.edgeLabelWrapper"
@mouseenter="isHovered = true"

View file

@ -73,6 +73,7 @@ function onClickAdd() {
v-if="!isConnected && !isReadOnly"
v-show="isHandlePlusVisible"
data-test-id="canvas-handle-plus"
:data-plus-type="plusType"
:line-size="plusLineSize"
:handle-classes="handleClasses"
:type="plusType"

View file

@ -259,6 +259,7 @@ onBeforeUnmount(() => {
<div
:class="[$style.canvasNode, { [$style.showToolbar]: showToolbar }]"
data-test-id="canvas-node"
:data-node-type="data.type"
>
<template
v-for="source in mappedOutputs"
@ -269,7 +270,9 @@ onBeforeUnmount(() => {
:mode="CanvasConnectionMode.Output"
:is-read-only="readOnly"
:is-valid-connection="isValidConnection"
:data-node-name="label"
data-test-id="canvas-node-output-handle"
:data-handle-index="source.index"
@add="onAdd"
/>
</template>
@ -284,6 +287,8 @@ onBeforeUnmount(() => {
:is-read-only="readOnly"
:is-valid-connection="isValidConnection"
data-test-id="canvas-node-input-handle"
:data-handle-index="target.index"
:data-node-name="label"
@add="onAdd"
/>
</template>

View file

@ -1784,9 +1784,6 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
}, []);
workflowsStore.addWorkflowTagIds(tagIds);
setTimeout(() => {
nodeHelpers.addPinDataConnections(workflowsStore.pinnedWorkflowData);
});
}
async function fetchWorkflowDataFromUrl(url: string): Promise<IWorkflowDataUpdate | undefined> {

View file

@ -1443,6 +1443,7 @@ function selectNodes(ids: string[]) {
function onClickPane(position: CanvasNode['position']) {
lastClickPosition.value = [position.x, position.y];
uiStore.isCreateNodeActive = false;
setNodeSelected();
}
/**

View file

@ -1704,7 +1704,7 @@ export default defineComponent({
if (data.nodes.length > 0) {
if (!isCut) {
this.showMessage({
title: 'Copied!',
title: this.$locale.baseText('generic.copiedToClipboard'),
message: '',
type: 'success',
});