diff --git a/cypress/e2e/5-workflow-actions.cy.ts b/cypress/e2e/5-workflow-actions.cy.ts deleted file mode 100644 index 20d6924801..0000000000 --- a/cypress/e2e/5-workflow-actions.cy.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { randFirstName, randLastName } from "@ngneat/falso"; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from "../constants"; -import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; - -const NEW_WORKFLOW_NAME = 'Something else'; -const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; -const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; - -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); -const WorkflowPage = new WorkflowPageClass(); - -describe('Workflow Actions', () => { - before(() => { - cy.resetAll(); - cy.setup({ email, firstName, lastName, password }); - }); - - beforeEach(() => { - cy.on('uncaught:exception', (err, runnable) => { - expect(err.message).to.include('Not logged in'); - - return false; - }) - - cy.signin({ email, password }); - - WorkflowPage.actions.visit(); - }); - - it('should be able to save on button click', () => { - WorkflowPage.actions.saveWorkflowOnButtonClick(); - WorkflowPage.getters.isWorkflowSaved(); - }); - - it('should save workflow on keyboard shortcut', () => { - WorkflowPage.actions.saveWorkflowUsingKeyboardShortcut(); - WorkflowPage.getters.isWorkflowSaved(); - }); - - it('should not be able to activate unsaved workflow', () => { - WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled'); - }); - - it('should not be able to activate workflow without trigger node', () => { - // Manual trigger is not enough to activate the workflow - WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); - WorkflowPage.actions.saveWorkflowOnButtonClick(); - WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled'); - }); - - it('should be able to activate workflow', () => { - WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); - WorkflowPage.actions.saveWorkflowOnButtonClick(); - WorkflowPage.actions.activateWorkflow(); - WorkflowPage.getters.isWorkflowActivated(); - }); - - it('should save new workflow after renaming', () => { - WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME); - WorkflowPage.getters.isWorkflowSaved(); - }); - - it('should rename workflow', () => { - WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); - WorkflowPage.actions.saveWorkflowOnButtonClick(); - WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME); - WorkflowPage.getters.isWorkflowSaved(); - WorkflowPage.getters.workflowNameInputContainer().invoke('attr', 'title').should('eq', NEW_WORKFLOW_NAME); - }); - -}); diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts new file mode 100644 index 0000000000..9891d6a387 --- /dev/null +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -0,0 +1,107 @@ +import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; + +const NEW_WORKFLOW_NAME = 'Something else'; +const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; +const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; +const CODE_NODE = 'Code' +const TEST_WF_TAGS = ['Tag 1', 'Tag 2', 'Tag 3']; + +const WorkflowPage = new WorkflowPageClass(); + +describe('Workflow Actions', () => { + beforeEach(() => { + cy.resetAll(); + cy.skipSetup(); + WorkflowPage.actions.visit(); + }); + + it('should be able to save on button click', () => { + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.getters.isWorkflowSaved(); + }); + + it('should save workflow on keyboard shortcut', () => { + WorkflowPage.actions.saveWorkflowUsingKeyboardShortcut(); + WorkflowPage.getters.isWorkflowSaved(); + }); + + it('should not be able to activate unsaved workflow', () => { + WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled'); + }); + + it('should not be able to activate workflow without trigger node', () => { + // Manual trigger is not enough to activate the workflow + WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled'); + }); + + it('should be able to activate workflow', () => { + WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.actions.activateWorkflow(); + WorkflowPage.getters.isWorkflowActivated(); + }); + + it('should save new workflow after renaming', () => { + WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME); + WorkflowPage.getters.isWorkflowSaved(); + }); + + it('should rename workflow', () => { + WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.actions.renameWorkflow(NEW_WORKFLOW_NAME); + WorkflowPage.getters.isWorkflowSaved(); + WorkflowPage.getters.workflowNameInputContainer().invoke('attr', 'title').should('eq', NEW_WORKFLOW_NAME); + }); + + it('should add tags', () => { + WorkflowPage.getters.newTagLink().click(); + WorkflowPage.actions.addTags(TEST_WF_TAGS); + WorkflowPage.getters.isWorkflowSaved(); + WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length); + }); + + it('should add more tags', () => { + WorkflowPage.getters.newTagLink().click(); + WorkflowPage.actions.addTags(TEST_WF_TAGS); + WorkflowPage.getters.workflowTagElements().first().click(); + WorkflowPage.actions.addTags(['Another one']); + WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length + 1); + }); + + it('should remove tags by clicking X in tag', () => { + WorkflowPage.getters.newTagLink().click(); + WorkflowPage.actions.addTags(TEST_WF_TAGS); + WorkflowPage.getters.workflowTagElements().first().click(); + WorkflowPage.getters.workflowTagsContainer().find('.el-tag__close').first().click(); + cy.get('body').type('{enter}'); + WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length - 1); + }); + + it('should remove tags from dropdown', () => { + WorkflowPage.getters.newTagLink().click(); + WorkflowPage.actions.addTags(TEST_WF_TAGS); + WorkflowPage.getters.workflowTagElements().first().click(); + WorkflowPage.getters.workflowTagsDropdown().find('li').first().click(); + cy.get('body').type('{enter}'); + WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length - 1); + }); + + it('should copy nodes', () => { + WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + WorkflowPage.actions.addNodeToCanvas(CODE_NODE); + cy.get('body').type('{meta}', { release: false }).type('a'); + cy.get('body').type('{meta}', { release: false }).type('c'); + WorkflowPage.getters.successToast().should('exist'); + }); + + it('should paste nodes', () => { + cy.fixture('Test_workflow-actions_paste-data.json').then(data => { + cy.get('body').paste(JSON.stringify(data)); + WorkflowPage.getters.canvasNodes().should('have.have.length', 2); + }); + }); + +}); diff --git a/cypress/fixtures/Test_workflow-actions_paste-data.json b/cypress/fixtures/Test_workflow-actions_paste-data.json new file mode 100644 index 0000000000..1846939545 --- /dev/null +++ b/cypress/fixtures/Test_workflow-actions_paste-data.json @@ -0,0 +1,37 @@ +{ + "meta": { + "instanceId": "1a30c82b98a30444ad25bce513655a5e02be772d361403542c23172be6062f04" + }, + "nodes": [{ + "parameters": { + "rule": { + "interval": [{}] + } + }, + "id": "a898563b-d2a4-4b15-a979-366872e801b0", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [420, 260] + }, { + "parameters": { + "options": {} + }, + "id": "b9a13e3d-bfa5-4873-959f-fd3d67e380d9", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [640, 260] + }], + "connections": { + "Schedule Trigger": { + "main": [ + [{ + "node": "Set", + "type": "main", + "index": 0 + }] + ] + } + } +} diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index d42998c7dc..ed68f67685 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -3,12 +3,14 @@ import { BasePage } from './base'; export class WorkflowPage extends BasePage { url = '/workflow/new'; getters = { - workflowNameInputContainer: () => cy - .getByTestId('workflow-name-input', { timeout: 5000 }), + workflowNameInputContainer: () => cy.getByTestId('workflow-name-input', { timeout: 5000 }), workflowNameInput: () => this.getters.workflowNameInputContainer().then(($el) => cy.wrap($el.find('input'))), workflowImportInput: () => cy.getByTestId('workflow-import-input'), workflowTags: () => cy.getByTestId('workflow-tags'), workflowTagsContainer: () => cy.getByTestId('workflow-tags-container'), + workflowTagsInput: () => this.getters.workflowTagsContainer().then(($el) => cy.wrap($el.find('input').first())), + workflowTagElements: () => this.getters.workflowTagsContainer().find('span.tags').children(), + workflowTagsDropdown: () => cy.getByTestId('workflow-tags-dropdown'), newTagLink: () => cy.getByTestId('new-tag-link'), saveButton: () => cy.getByTestId('workflow-save-button'), nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'), @@ -29,6 +31,10 @@ export class WorkflowPage extends BasePage { isWorkflowActivated: () => this.getters.activatorSwitch().should('have.class', 'is-checked'), expressionModalInput: () => cy.getByTestId('expression-modal-input'), expressionModalOutput: () => cy.getByTestId('expression-modal-output'), + + nodeViewRoot: () => cy.getByTestId('node-view-root'), + copyPasteInput: () => cy.getByTestId('hidden-copy-paste'), + canvasNodes: () => cy.getByTestId('canvas-node'), }; actions = { visit: () => { @@ -86,10 +92,9 @@ export class WorkflowPage extends BasePage { cy.get('body').type('{enter}'); }, addTags: (tags: string[]) => { - this.getters.newTagLink().click(); tags.forEach(tag => { - cy.get('body').type(tag); - cy.get('body').type('{enter}'); + this.getters.workflowTagsInput().type(tag); + this.getters.workflowTagsInput().type('{enter}'); }); cy.get('body').type('{enter}'); }, diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 7637577e0d..8a3d135901 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -128,3 +128,15 @@ Cypress.Commands.add('resetAll', () => { Cypress.Commands.add('setupOwner', (payload) => { cy.task('setup-owner', payload); }); + +Cypress.Commands.add('paste', { prevSubject: true }, (selector, pastePayload) => { + // https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event + cy.wrap(selector).then($destination => { + const pasteEvent = Object.assign(new Event('paste', { bubbles: true, cancelable: true }), { + clipboardData: { + getData: () => pastePayload + } + }); + $destination[0].dispatchEvent(pasteEvent); + }); +}); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index afb2dee0c1..40d7c0840a 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -24,6 +24,7 @@ declare global { setupOwner(payload: SetupPayload): void; skipSetup(): void; resetAll(): void; + paste(pastePayload: string): void, } } } diff --git a/packages/editor-ui/src/mixins/copyPaste.ts b/packages/editor-ui/src/mixins/copyPaste.ts index 0f73d533db..fff3c75368 100644 --- a/packages/editor-ui/src/mixins/copyPaste.ts +++ b/packages/editor-ui/src/mixins/copyPaste.ts @@ -56,6 +56,7 @@ export const copyPaste = Vue.extend({ hiddenInput.setAttribute('type', 'text'); hiddenInput.setAttribute('id', 'hidden-input-copy-paste'); hiddenInput.setAttribute('class', 'hidden-copy-paste'); + hiddenInput.setAttribute('data-test-id', 'hidden-copy-paste'); this.hiddenInput = hiddenInput; document.body.append(hiddenInput); diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index c242158030..adf9ba1163 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -3,6 +3,7 @@