diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts new file mode 100644 index 0000000000..516bfafa6b --- /dev/null +++ b/cypress/e2e/4-node-creator.cy.ts @@ -0,0 +1,168 @@ +import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from "../constants"; +import { randFirstName, randLastName } from "@ngneat/falso"; +import { NodeCreator } from '../pages/features/node-creator'; +import { INodeTypeDescription } from '../../packages/workflow'; +import CustomNodeFixture from '../fixtures/Custom_node.json'; + +const username = DEFAULT_USER_EMAIL; +const password = DEFAULT_USER_PASSWORD; +const firstName = randFirstName(); +const lastName = randLastName(); +const nodeCreatorFeature = new NodeCreator(); + +describe('Node Creator', () => { + beforeEach(() => { + cy.intercept('GET', '/types/nodes.json', (req) => { + // Delete caching headers so that we can intercept the request + ['etag', 'if-none-match', 'if-modified-since'].forEach(header => {delete req.headers[header]}); + + + req.continue((res) => { + const nodes = res.body as INodeTypeDescription[]; + + nodes.push(CustomNodeFixture as INodeTypeDescription); + res.send(nodes) + }) + }).as('nodesIntercept') + + cy.signup(username, firstName, lastName, password); + + cy.on('uncaught:exception', (err, runnable) => { + expect(err.message).to.include('Not logged in'); + + return false; + }) + + cy.signin(username, password); + cy.visit(nodeCreatorFeature.url); + }); + + it('should open node creator on trigger tab if no trigger is on canvas', () => { + nodeCreatorFeature.getters.canvasAddButton().click(); + + nodeCreatorFeature.getters.nodeCreator().contains('When should this workflow run?').should('be.visible'); + + nodeCreatorFeature.getters.nodeCreatorTabs().should('not.exist'); + }) + + it('should see all tabs when opening via plus button', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.nodeCreatorTabs().should('exist'); + nodeCreatorFeature.getters.selectedTab().should('have.text', 'Trigger'); + }); + + it('should navigate subcategory', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.getCreatorItem('On App Event').click(); + nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'On App Event'); + // Go back + nodeCreatorFeature.getters.activeSubcategory().find('button').click() + nodeCreatorFeature.getters.activeSubcategory().should('not.exist'); + }) + + it('should search for nodes', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.selectedTab().should('have.text', 'Trigger'); + + nodeCreatorFeature.getters.searchBar().find('input').type('manual'); + nodeCreatorFeature.getters.creatorItem().should('have.length', 1); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('manual123'); + nodeCreatorFeature.getters.creatorItem().should('have.length', 0); + nodeCreatorFeature.getters.noResults() + .should('exist') + .should('contain.text', 'We didn\'t make that... yet'); + + nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image'); + nodeCreatorFeature.getters.creatorItem().should('have.length', 0); + nodeCreatorFeature.getters.noResults() + .should('exist') + .should('contain.text', 'To see results, click here'); + + nodeCreatorFeature.getters.noResults().contains('click here').click(); + nodeCreatorFeature.getters.nodeCreatorTabs().should('exist'); + nodeCreatorFeature.getters.getCreatorItem('Edit Image').should('exist'); + nodeCreatorFeature.getters.selectedTab().should('have.text', 'All'); + nodeCreatorFeature.getters.searchBar().find('button').click(); + nodeCreatorFeature.getters.searchBar().find('input').should('be.empty') + }) + + it('should add manual trigger node', () => { + nodeCreatorFeature.getters.canvasAddButton().click(); + nodeCreatorFeature.getters.getCreatorItem('Manually').click(); + + // TODO: Replace once we have canvas feature utils + cy.get('span').contains('Back to canvas').click(); + + nodeCreatorFeature.getters.canvasAddButton().should('not.be.visible'); + nodeCreatorFeature.getters.nodeCreator().should('not.exist'); + + // TODO: Replace once we have canvas feature utils + cy.get('div').contains("On clicking 'execute'").should('exist'); + }) + + it('check if non-core nodes are rendered all nodes', () => { + cy.wait('@nodesIntercept').then((interception) => { + const nodes = interception.response?.body as INodeTypeDescription[]; + + const categorizedNodes = nodeCreatorFeature.actions.categorizeNodes(nodes); + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.actions.selectTab('All'); + + const categories = Object.keys(categorizedNodes); + categories.forEach((category: string) => { + // Core Nodes contains subcategories which we'll test separately + if(category === 'Core Nodes') return; + + nodeCreatorFeature.actions.toggleCategory(category) + + // Check if all nodes are present + categorizedNodes[category].forEach((node: INodeTypeDescription) => { + if(node.hidden) return; + nodeCreatorFeature.getters.categorizedItems().contains(node.displayName).should('exist'); + }) + + nodeCreatorFeature.actions.toggleCategory(category) + }) + }) + }) + + it.only('should render and select community node', () => { + cy.wait('@nodesIntercept').then(() => { + const customCategory = 'customCategory'; + const customNode = 'E2E Node'; + const customNodeDescription = 'Demonstrate rendering of node'; + + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.actions.selectTab('All'); + + nodeCreatorFeature.getters.getCreatorItem(customCategory).should('exist'); + + nodeCreatorFeature.actions.toggleCategory(customCategory); + nodeCreatorFeature.getters.getCreatorItem(customNode).findChildByTestId('node-item-community-tooltip').should('exist'); + nodeCreatorFeature.getters.getCreatorItem(customNode).contains(customNodeDescription).should('exist'); + nodeCreatorFeature.actions.selectNode(customNode); + + // TODO: Replace once we have canvas feature utils + cy.get('.data-display .node-name').contains(customNode).should('exist'); + + const nodeParameters = () => cy.getByTestId('node-parameters') + const firstParameter = () => nodeParameters().find('.parameter-item').eq(0); + const secondParameter = () => nodeParameters().find('.parameter-item').eq(1); + + // Check correct fields are rendered + nodeParameters().should('exist') + // Test property text input + firstParameter().contains('Test property').should('exist'); + firstParameter().find('input.el-input__inner').should('have.value', 'Some default'); + // Resource select input + secondParameter().find('label').contains('Resource').should('exist'); + secondParameter().find('input.el-input__inner').should('have.value', 'option2'); + secondParameter().find('.el-select').click(); + secondParameter().find('.el-select-dropdown__list').should('exist') + // Check if all options are rendered and select the fourth one + secondParameter().find('.el-select-dropdown__list').children().should('have.length', 4); + secondParameter().find('.el-select-dropdown__list').children().eq(3).contains('option4').should('exist').click(); + secondParameter().find('input.el-input__inner').should('have.value', 'option4'); + }) + }) +}); diff --git a/cypress/fixtures/Custom_node.json b/cypress/fixtures/Custom_node.json new file mode 100644 index 0000000000..bbd661d261 --- /dev/null +++ b/cypress/fixtures/Custom_node.json @@ -0,0 +1,55 @@ +{ + "properties": [{ + "displayName": "Test property", + "name": "testProp", + "type": "string", + "required": true, + "noDataExpression": false, + "default": "Some default" + }, + { + "displayName": "Resource", + "name": "resource", + "type": "options", + "noDataExpression": true, + "options": [{ + "name": "option1", + "value": "option1" + }, + { + "name": "option2", + "value": "option2" + }, + { + "name": "option3", + "value": "option3" + }, + { + "name": "option4", + "value": "option4" + } + ], + "default": "option2" + } + ], + "displayName": "E2E Node", + "name": "@e2e/n8n-nodes-e2e", + "group": [ + "transform" + ], + "codex": { + "categories": ["CustomCategory"] + }, + "version": 1, + "description": "Demonstrate rendering of node", + "defaults": { + "name": "E2E Node " + }, + "inputs": [ + "main" + ], + "outputs": [ + "main" + ], + "icon": "fa:network-wired" +} diff --git a/cypress/pages/features/node-creator.ts b/cypress/pages/features/node-creator.ts new file mode 100644 index 0000000000..129277a753 --- /dev/null +++ b/cypress/pages/features/node-creator.ts @@ -0,0 +1,55 @@ +import { BasePage } from "../base"; +import { INodeTypeDescription } from '../../packages/workflow'; + +export class NodeCreator extends BasePage { + url = '/workflow/new'; + getters = { + plusButton: () => cy.getByTestId('node-creator-plus-button'), + canvasAddButton: () => cy.getByTestId('canvas-add-button'), + searchBar: () => cy.getByTestId('search-bar'), + getCreatorItem: (label: string) => this.getters.creatorItem().contains(label).parents('[data-test-id="item-iterator-item"]'), + getNthCreatorItem: (n: number) => this.getters.creatorItem().eq(n), + nodeCreator: () => cy.getByTestId('node-creator'), + nodeCreatorTabs: () => cy.getByTestId('node-creator-type-selector'), + selectedTab: () => this.getters.nodeCreatorTabs().find('.is-active'), + categorizedItems: () => cy.getByTestId('categorized-items'), + creatorItem: () => cy.getByTestId('item-iterator-item'), + communityNodeTooltip: () => cy.getByTestId('node-item-community-tooltip'), + noResults: () => cy.getByTestId('categorized-no-results'), + activeSubcategory: () => cy.getByTestId('categorized-items-subcategory'), + expandedCategories: () => this.getters.creatorItem().find('>div').filter('.active').invoke('text'), + }; + actions = { + openNodeCreator: () => { + this.getters.plusButton().click(); + this.getters.nodeCreator().should('be.visible') + }, + selectNode: (displayName: string) => { + this.getters.getCreatorItem(displayName).click(); + }, + selectTab: (tab: string) => { + this.getters.nodeCreatorTabs().contains(tab).click(); + }, + toggleCategory: (category: string) => { + this.getters.getCreatorItem(category).click() + }, + categorizeNodes: (nodes: INodeTypeDescription[]) => { + const categorizedNodes = nodes.reduce((acc, node) => { + const categories = (node?.codex?.categories || []).map((category: string) => category.trim()); + + categories.forEach((category: {[key: string]: INodeTypeDescription[]}) => { + // Node creator should show only the latest version of a node + const newerVersion = nodes.find((n: INodeTypeDescription) => n.name === node.nameĀ && (n.version > node.version || Array.isArray(n.version))); + + if (acc[category] === undefined) { + acc[category] = []; + } + acc[category].push(newerVersion ?? node); + }); + return acc; + }, {}) + + return categorizedNodes; + } + }; +} diff --git a/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue b/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue index 1e00350a03..9d6b50b62b 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue @@ -7,12 +7,13 @@ tabindex="0" @keydown.capture="nodeFilterKeyDown" :key="`${activeSubcategoryTitle}_transition`" + data-test-id="categorized-items" >
-
+
@@ -45,6 +46,7 @@
diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue index 24ffb1aa28..bf0396cd5a 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue @@ -13,6 +13,7 @@ :key="item.key" :class="item.type" :data-key="item.key" + data-test-id="item-iterator-item" > - +