diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index 47a5421b27..c3ee945c0d 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -1,6 +1,6 @@ import { getManualChatModal } from './modals/chat-modal'; import { clickGetBackToCanvas, getParameterInputByName } from './ndv'; -import { ROUTES } from '../constants'; +import { META_KEY, ROUTES } from '../constants'; import type { OpenContextMenuOptions } from '../types'; /** @@ -364,3 +364,48 @@ export function openExecutions() { export function clickClearExecutionDataButton() { cy.getByTestId('clear-execution-data-button').click(); } + +/** + * Undo/Redo + */ + +export function hitComboShortcut(modifier: string, key: string) { + cy.get('body').wait(100).type(modifier, { delay: 100, release: false }).type(key); +} +export function hitUndo() { + hitComboShortcut(`{${META_KEY}}`, 'z'); +} +export function hitRedo() { + cy.get('body').type(`{${META_KEY}+shift+z}`); +} +export function hitSelectAll() { + hitComboShortcut(`{${META_KEY}}`, 'a'); +} +export function hitDeleteAllNodes() { + hitSelectAll(); + cy.get('body').type('{backspace}'); +} +export function hitDisableNodeShortcut() { + cy.get('body').type('d'); +} +export function hitCopy() { + hitComboShortcut(`{${META_KEY}}`, 'c'); +} +export function hitPinNodeShortcut() { + cy.get('body').type('p'); +} +export function hitSaveWorkflow() { + cy.get('body').type(`{${META_KEY}+s}`); +} +export function hitExecuteWorkflow() { + cy.get('body').type(`{${META_KEY}+enter}`); +} +export function hitDuplicateNode() { + cy.get('body').type(`{${META_KEY}+d}`); +} +export function hitAddSticky() { + cy.get('body').type('{shift+S}'); +} +export function selectRight() { + cy.get('body').type('{shift+rightArrow}'); +} diff --git a/cypress/e2e/51-subworkflow-extraction.ts b/cypress/e2e/51-subworkflow-extraction.ts new file mode 100644 index 0000000000..f65d2e0883 --- /dev/null +++ b/cypress/e2e/51-subworkflow-extraction.ts @@ -0,0 +1,80 @@ +import { + clickContextMenuAction, + clickZoomToFit, + executeWorkflow, + getCanvasNodeByName, + getCanvasNodes, + hitUndo, + navigateToNewWorkflowPage, + openContextMenu, + pasteWorkflow, + saveWorkflowOnButtonClick, + selectRight, +} from '../composables/workflow'; +import SubworkflowExtractionFixture from '../fixtures/Subworkflow-extraction-workflow.json'; +import { clearAnyNotifications, successToast } from '../pages/notifications'; + +const EDIT_FIELDS_NAMES = [ + 'Edit Fields0', + 'Edit Fields1', + 'Edit Fields2', + 'Edit Fields3', + 'Edit Fields4', + 'Edit Fields5', +] as const; + +function selectNode(nodeName: string) { + getCanvasNodeByName(nodeName).click(); +} + +function executeAndConfirmSuccess() { + executeWorkflow(); + successToast().should('contain.text', 'Workflow executed successfully'); + clearAnyNotifications(); +} + +describe('Subworkflow Extraction', () => { + beforeEach(() => { + navigateToNewWorkflowPage(); + // this selects all nodes + pasteWorkflow(SubworkflowExtractionFixture); + saveWorkflowOnButtonClick(); + getCanvasNodes().should('have.length', 7); + clickZoomToFit(); + + executeAndConfirmSuccess(); + + openContextMenu(); + clickContextMenuAction('deselect_all'); + }); + + describe('can extract a valid selection and still execute the workflow', () => { + it('should extract a node and succeed execution, and then undo and succeed executions', () => { + for (const name of EDIT_FIELDS_NAMES) { + selectNode(name); + openContextMenu(name); + clickContextMenuAction('extract_sub_workflow'); + cy.getByTestId('submit-button').click(); + + executeAndConfirmSuccess(); + } + + for (const _ of EDIT_FIELDS_NAMES) { + hitUndo(); + + executeAndConfirmSuccess(); + } + }); + + it('should extract all nodes besides trigger and succeed execution', () => { + selectNode(EDIT_FIELDS_NAMES[0]); + selectRight(); + openContextMenu(); + clickContextMenuAction('extract_sub_workflow'); + + cy.getByTestId('submit-button').click(); + + executeAndConfirmSuccess(); + }); + }); +}); diff --git a/cypress/fixtures/Subworkflow-extraction-workflow.json b/cypress/fixtures/Subworkflow-extraction-workflow.json new file mode 100644 index 0000000000..515926cf4d --- /dev/null +++ b/cypress/fixtures/Subworkflow-extraction-workflow.json @@ -0,0 +1,231 @@ +{ + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, -5], + "id": "41b93a1d-ca68-49c1-b252-323e57f58ba9", + "name": "When clicking ‘Execute workflow’" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "624c7310-e17a-4075-922f-3b57952e3dfa", + "name": "x", + "value": "={{ $json.x + 'a' }}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [220, -5], + "id": "afd95339-62f5-4efd-bcaa-637cec90ab0f", + "name": "Edit Fields0" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "624c7310-e17a-4075-922f-3b57952e3dfa", + "name": "x", + "value": "={{ $json.x + 'a' + $('When clicking ‘Execute workflow’').item.json.x}}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [440, 120], + "id": "c279107f-08c2-4be6-9016-d19215cfeb9e", + "name": "Edit Fields1" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "624c7310-e17a-4075-922f-3b57952e3dfa", + "name": "x", + "value": "={{ $json.x + 'a' + $('When clicking ‘Execute workflow’').first().json.x }}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [440, -120], + "id": "1d4eb1a0-ad87-4d8b-87e6-c853f27aa8ad", + "name": "Edit Fields2" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "624c7310-e17a-4075-922f-3b57952e3dfa", + "name": "x", + "value": "={{ $json.x + 'a' + $('When clicking ‘Execute workflow’').last().json.x}}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [440, -320], + "id": "b99d763e-cb4c-487f-8718-8b9a5f02c167", + "name": "Edit Fields3" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "624c7310-e17a-4075-922f-3b57952e3dfa", + "name": "x", + "value": "={{ $json.x + 'a' + $('When clicking ‘Execute workflow’').item.json.x}}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [720, 0], + "id": "4244c039-51c3-415b-8b3d-06be5a90dc7a", + "name": "Edit Fields4" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "624c7310-e17a-4075-922f-3b57952e3dfa", + "name": "x", + "value": "={{ $json.x + 'a' + $('When clicking ‘Execute workflow’').item.json.x + $('Edit Fields0').item.json.x }}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [960, 0], + "id": "2653bbfe-8f06-4029-9071-8faacfb73cd0", + "name": "Edit Fields5" + } + ], + "connections": { + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Edit Fields0", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields0": { + "main": [ + [ + { + "node": "Edit Fields1", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields2", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields3", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields4", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields1": { + "main": [ + [ + { + "node": "Edit Fields4", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields2": { + "main": [ + [ + { + "node": "Edit Fields4", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields3": { + "main": [ + [ + { + "node": "Edit Fields4", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields4": { + "main": [ + [ + { + "node": "Edit Fields5", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "When clicking ‘Execute workflow’": [ + { + "x": "l" + }, + { + "x": "m" + }, + { + "x": "o" + } + ] + }, + "meta": { + "instanceId": "d30ee1956588565f63beb4b8b589790a4701843b47fcd9e8d6d5527fe47872c3" + } +} diff --git a/cypress/pages/notifications.ts b/cypress/pages/notifications.ts index 2c3648355a..3f5c681877 100644 --- a/cypress/pages/notifications.ts +++ b/cypress/pages/notifications.ts @@ -24,3 +24,19 @@ export const clearNotifications = () => { } }); }; + +// Clears notifications without asserting their existence +export const clearAnyNotifications = () => { + const notificationSelector = '.el-notification:has(.el-notification--success)'; + cy.get('body') + .should('have.length.gte', 0) + .then(($body) => { + if ($body.find(notificationSelector).length) { + cy.get(notificationSelector).each(($el) => { + if ($el.find('.el-notification__closeBtn').length) { + cy.wrap($el).find('.el-notification__closeBtn').click({ force: true }); + } + }); + } + }); +};