Skip to content

Commit

Permalink
Merge branch 'master' into N8N-5498-workflow-action-tests
Browse files Browse the repository at this point in the history
* master:
  refactor: Trim down the response on the Workflows listing endpoint (#4726)
  fix: Fix HTTP Request node e2e test in full run (no-changelog) (#4728)
  test: Add e2e tests for node creator (#4714)
  test: Set up e2e tests for HTTP Request node (#4718)
  fix: Lint typescript stories (no-changelog) (#4723)
  fix(editor): Fix slots rendering of NodeCreator's NoResults component (#4721)

# Conflicts:
#	cypress/pages/workflow.ts
#	packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue
  • Loading branch information
MiloradFilipovic committed Nov 25, 2022
2 parents fb18fb6 + 1579d05 commit 84e52ab
Show file tree
Hide file tree
Showing 50 changed files with 479 additions and 111 deletions.
24 changes: 24 additions & 0 deletions cypress/e2e/4-http-request-node.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';

const WorkflowsPage = new WorkflowsPageClass();
const WorkflowPage = new WorkflowPageClass();

describe('HTTP Request node', () => {
beforeEach(() => {
cy.task('db:reset');
cy.skipSetup();
});

it('should make a request with a URL and receive a response', () => {
WorkflowsPage.actions.createWorkflowFromCard();
WorkflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
WorkflowPage.actions.addNodeToCanvas('HTTP Request');
WorkflowPage.actions.openNodeNdv('HTTP Request');
WorkflowPage.actions.typeIntoParameterInput('url', 'https://google.com');

WorkflowPage.actions.executeNodeFromNdv();

WorkflowPage.getters.ndvOutputPanel().contains('<!doctype html>');
});
});
168 changes: 168 additions & 0 deletions cypress/e2e/4-node-creator.cy.ts
Original file line number Diff line number Diff line change
@@ -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');
})
})
});
55 changes: 55 additions & 0 deletions cypress/fixtures/Custom_node.json
Original file line number Diff line number Diff line change
@@ -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"
}
55 changes: 55 additions & 0 deletions cypress/pages/features/node-creator.ts
Original file line number Diff line number Diff line change
@@ -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;
}
};
}
41 changes: 37 additions & 4 deletions cypress/pages/workflow.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,52 @@
import { BasePage } from "./base";
import { BasePage } from './base';

export class WorkflowPage extends BasePage {
url = '/workflow/new';
getters = {
// workflowNameInput: () => cy.getByTestId('workflow-name-input', { timeout: 5000 }).then($el => cy.wrap($el.find('input'))),
workflowNameInput: () => cy.getByTestId('workflow-name-input'),
workflowImportInput: () => cy.getByTestId('workflow-import-input'),
workflowTags: () => cy.getByTestId('workflow-tags'),
saveButton: () => cy.getByTestId('wf-save-button').children().first(),
saveButton: () => cy.getByTestId('save-button'),

nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'),
nodeCreatorPlusButton: () => cy.getByTestId('node-creator-plus-button'),
canvasPlusButton: () => cy.getByTestId('canvas-plus-button'),
canvasNodeBox: (nodeDisplayName: string) => {
return cy
.getByTestId('canvas-node-box-title')
.contains(nodeDisplayName)
.parents('[data-test-id="canvas-node-box"]');
},

ndvParameterInput: (parameterName: string) =>
cy.getByTestId(`parameter-input-${parameterName}`),
ndvOutputPanel: () => cy.getByTestId('output-panel'),
activatorSwitch: () => cy.getByTestId('wf-activate-switch'),
workflowMenu: () => cy.getByTestId('workflow-menu'),
firstStepButton: () => cy.getByTestId('canvas-add-button'),
triggerNodeItem: (name: string) => cy.getByTestId(name),
workflowNameInput: () => cy.getByTestId('workflow-name-input'),
};

actions = {
addInitialNodeToCanvas: (nodeDisplayName: string) => {
this.getters.canvasPlusButton().click();
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
this.getters.nodeCreatorSearchBar().type('{enter}{esc}');
},
addNodeToCanvas: (nodeDisplayName: string) => {
this.getters.nodeCreatorPlusButton().click();
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
this.getters.nodeCreatorSearchBar().type('{enter}{esc}');
},
openNodeNdv: (nodeTypeName: string) => {
this.getters.canvasNodeBox(nodeTypeName).dblclick();
},
typeIntoParameterInput: (parameterName: string, content: string) => {
this.getters.ndvParameterInput(parameterName).type(content);
},
executeNodeFromNdv: () => {
cy.contains('Execute node').click();
},
visit: () => {
cy.visit(this.url);
cy.get('[data-test-id=node-view-loader]', { timeout: 5000 }).should('not.exist');
Expand Down
6 changes: 6 additions & 0 deletions cypress/pages/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,10 @@ export class WorkflowsPage extends BasePage {
// myWorkflows: () => cy.getByTestId('my-workflows'),
// allWorkflows: () => cy.getByTestId('all-workflows'),
};

actions = {
createWorkflowFromCard: () => {
this.getters.newWorkflowButtonCard().click();
},
}
}
3 changes: 3 additions & 0 deletions packages/cli/src/databases/entities/WorkflowEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ export class WorkflowEntity extends AbstractEntity implements IWorkflowDb {
setHash(): void {
const { name, active, nodes, connections, settings, staticData, pinData } = this;

// Workflow listing page does not request the `connections` column, so we can use this for `undefined` to avoid generating hashes for all the workflows.
if (!connections) return;

const state = JSON.stringify({
name,
active,
Expand Down
Loading

0 comments on commit 84e52ab

Please sign in to comment.