diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts
new file mode 100644
index 0000000000..2ca59e7915
--- /dev/null
+++ b/packages/core/test/Helpers.ts
@@ -0,0 +1,253 @@
+import { set } from 'lodash';
+
+import {
+ INodeExecutionData,
+ INodeParameters,
+ INodeType,
+ INodeTypes,
+ INodeTypesObject,
+ IRun,
+ ITaskData,
+ IWorkflowExecuteAdditionalData,
+} from 'n8n-workflow';
+
+import {
+ IDeferredPromise,
+ IExecuteFunctions,
+} from '../src';
+
+
+class NodeTypesClass implements INodeTypes {
+
+ nodeTypes: INodeTypesObject = {
+ 'n8n-nodes-base.merge': {
+ description: {
+ displayName: 'Merge',
+ name: 'merge',
+ icon: 'fa:clone',
+ group: ['transform'],
+ version: 1,
+ description: 'Merges data of multiple streams once data of both is available',
+ defaults: {
+ name: 'Merge',
+ color: '#00cc22',
+ },
+ inputs: ['main', 'main'],
+ outputs: ['main'],
+ properties: [
+ {
+ displayName: 'Mode',
+ name: 'mode',
+ type: 'options',
+ options: [
+ {
+ name: 'Append',
+ value: 'append',
+ description: 'Combines data of both inputs. The output will contain items of input 1 and input 2.',
+ },
+ {
+ name: 'Pass-through',
+ value: 'passThrough',
+ description: 'Passes through data of one input. The output will conain only items of the defined input.',
+ },
+ {
+ name: 'Wait',
+ value: 'wait',
+ description: 'Waits till data of both inputs is available and will then output a single empty item.',
+ },
+ ],
+ default: 'append',
+ description: 'How data should be merged. If it should simply
be appended or merged depending on a property.',
+ },
+ {
+ displayName: 'Output Data',
+ name: 'output',
+ type: 'options',
+ displayOptions: {
+ show: {
+ mode: [
+ 'passThrough'
+ ],
+ },
+ },
+ options: [
+ {
+ name: 'Input 1',
+ value: 'input1',
+ },
+ {
+ name: 'Input 2',
+ value: 'input2',
+ },
+ ],
+ default: 'input1',
+ description: 'Defines of which input the data should be used as output of node.',
+ },
+ ]
+ },
+ async execute(this: IExecuteFunctions): Promise {
+ // const itemsInput2 = this.getInputData(1);
+
+ const returnData: INodeExecutionData[] = [];
+
+ const mode = this.getNodeParameter('mode', 0) as string;
+
+ if (mode === 'append') {
+ // Simply appends the data
+ for (let i = 0; i < 2; i++) {
+ returnData.push.apply(returnData, this.getInputData(i));
+ }
+ } else if (mode === 'passThrough') {
+ const output = this.getNodeParameter('output', 0) as string;
+
+ if (output === 'input1') {
+ returnData.push.apply(returnData, this.getInputData(0));
+ } else {
+ returnData.push.apply(returnData, this.getInputData(1));
+ }
+ } else if (mode === 'wait') {
+ returnData.push({ json: {} });
+ }
+
+ return [returnData];
+ }
+ },
+ 'n8n-nodes-base.set': {
+ description: {
+ displayName: 'Set',
+ name: 'set',
+ group: ['input'],
+ version: 1,
+ description: 'Sets a value',
+ defaults: {
+ name: 'Set',
+ color: '#0000FF',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ properties: [
+ {
+ displayName: 'Keep Only Set',
+ name: 'keepOnlySet',
+ type: 'boolean',
+ default: false,
+ description: 'If only the values set on this node should be
kept and all others removed.',
+ },
+ {
+ displayName: 'Values to Set',
+ name: 'values',
+ placeholder: 'Add Value',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ },
+ description: 'The value to set.',
+ default: {},
+ options: [
+ {
+ name: 'number',
+ displayName: 'Number',
+ values: [
+ {
+ displayName: 'Name',
+ name: 'name',
+ type: 'string',
+ default: 'propertyName',
+ description: 'Name of the property to write data to.
Supports dot-notation.
Example: "data.person[0].name"',
+ },
+ {
+ displayName: 'Value',
+ name: 'value',
+ type: 'number',
+ default: 0,
+ description: 'The number value to write in the property.',
+ },
+ ]
+ },
+ ],
+ },
+ ]
+ },
+ execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+
+ let item: INodeExecutionData;
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
+ item = items[itemIndex];
+ // Add number values
+ (this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
+ set(item.json, setItem.name as string, setItem.value);
+ });
+ }
+
+ return this.prepareOutputData(items);
+ }
+ },
+ 'n8n-nodes-base.start': {
+ description: {
+ displayName: 'Start',
+ name: 'start',
+ group: ['input'],
+ version: 1,
+ description: 'Starts the workflow execution from this node',
+ defaults: {
+ name: 'Start',
+ color: '#553399',
+ },
+ inputs: [],
+ outputs: ['main'],
+ properties: []
+ },
+ execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+
+ return this.prepareOutputData(items);
+ },
+ },
+ };
+
+ async init(nodeTypes: INodeTypesObject): Promise { }
+
+ getAll(): INodeType[] {
+ return Object.values(this.nodeTypes);
+ }
+
+ getByName(nodeType: string): INodeType {
+ return this.nodeTypes[nodeType];
+ }
+}
+
+let nodeTypesInstance: NodeTypesClass | undefined;
+
+
+export function NodeTypes(): NodeTypesClass {
+ if (nodeTypesInstance === undefined) {
+ nodeTypesInstance = new NodeTypesClass();
+ nodeTypesInstance.init({});
+ }
+
+ return nodeTypesInstance;
+}
+
+
+export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise, nodeExecutionOrder: string[]): IWorkflowExecuteAdditionalData {
+ return {
+ credentials: {},
+ hooks: {
+ nodeExecuteAfter: [
+ async (executionId: string, nodeName: string, data: ITaskData): Promise => {
+ nodeExecutionOrder.push(nodeName);
+ },
+ ],
+ workflowExecuteAfter: [
+ async (fullRunData: IRun, executionId: string): Promise => {
+ waitPromise.resolve(fullRunData);
+ },
+ ],
+ },
+ encryptionKey: 'test',
+ timezone: 'America/New_York',
+ webhookBaseUrl: 'webhook',
+ webhookTestBaseUrl: 'webhook-test',
+ };
+}
diff --git a/packages/core/test/WorkflowExecute.test.ts b/packages/core/test/WorkflowExecute.test.ts
new file mode 100644
index 0000000000..387d1b36f5
--- /dev/null
+++ b/packages/core/test/WorkflowExecute.test.ts
@@ -0,0 +1,625 @@
+
+import {
+ IConnections,
+ INode,
+ IRun,
+ Workflow,
+} from 'n8n-workflow';
+
+import {
+ createDeferredPromise,
+ WorkflowExecute,
+} from '../src';
+
+import * as Helpers from './Helpers';
+
+
+describe('WorkflowExecute', () => {
+
+ describe('run', () => {
+
+ const tests: Array<{
+ description: string;
+ input: {
+ workflowData: {
+ nodes: INode[],
+ connections: IConnections,
+ }
+ },
+ output: {
+ nodeExecutionOrder: string[];
+ nodeData: {
+ [key: string]: any[][]; // tslint:disable-line:no-any
+ };
+ },
+ }> = [
+ {
+ description: 'should run basic two node workflow',
+ input: {
+ // Leave the workflowData in regular JSON to be able to easily
+ // copy it from/in the UI
+ workflowData: {
+ "nodes": [
+ {
+ "parameters": {},
+ "name": "Start",
+ "type": "n8n-nodes-base.start",
+ "typeVersion": 1,
+ "position": [
+ 100,
+ 300
+ ]
+ },
+ {
+ "parameters": {
+ "values": {
+ "number": [
+ {
+ "name": "value1",
+ "value": 1
+ }
+ ]
+ }
+ },
+ "name": "Set",
+ "type": "n8n-nodes-base.set",
+ "typeVersion": 1,
+ "position": [
+ 280,
+ 300
+ ]
+ }
+ ],
+ "connections": {
+ "Start": {
+ "main": [
+ [
+ {
+ "node": "Set",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ }
+ },
+ },
+ output: {
+ nodeExecutionOrder: [
+ 'Start',
+ 'Set',
+ ],
+ nodeData: {
+ Set: [
+ [
+ {
+ value1: 1,
+ },
+ ],
+ ],
+ },
+ },
+ },
+ {
+ description: 'should run node twice when it has two input connections',
+ input: {
+ // Leave the workflowData in regular JSON to be able to easily
+ // copy it from/in the UI
+ workflowData: {
+ "nodes": [
+ {
+ "parameters": {},
+ "name": "Start",
+ "type": "n8n-nodes-base.start",
+ "typeVersion": 1,
+ "position": [
+ 100,
+ 300
+ ]
+ },
+ {
+ "parameters": {
+ "values": {
+ "number": [
+ {
+ "name": "value1",
+ "value": 1
+ }
+ ]
+ }
+ },
+ "name": "Set1",
+ "type": "n8n-nodes-base.set",
+ "typeVersion": 1,
+ "position": [
+ 300,
+ 250
+ ]
+ },
+ {
+ "parameters": {
+ "values": {
+ "number": [
+ {
+ "name": "value2",
+ "value": 2
+ }
+ ]
+ }
+ },
+ "name": "Set2",
+ "type": "n8n-nodes-base.set",
+ "typeVersion": 1,
+ "position": [
+ 500,
+ 400
+ ]
+ }
+ ],
+ "connections": {
+ "Start": {
+ "main": [
+ [
+ {
+ "node": "Set1",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Set2",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Set1": {
+ "main": [
+ [
+ {
+ "node": "Set2",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ }
+ },
+ },
+ output: {
+ nodeExecutionOrder: [
+ 'Start',
+ 'Set1',
+ 'Set2',
+ 'Set2',
+ ],
+ nodeData: {
+ Set1: [
+ [
+ {
+ value1: 1,
+ },
+ ]
+ ],
+ Set2: [
+ [
+ {
+ value2: 2,
+ },
+ ],
+ [
+ {
+ value1: 1,
+ value2: 2,
+ },
+ ],
+ ],
+ },
+ },
+ },
+ {
+ description: 'should run complicated multi node workflow',
+ input: {
+ // Leave the workflowData in regular JSON to be able to easily
+ // copy it from/in the UI
+ workflowData: {
+ "nodes": [
+ {
+ "parameters": {
+ "mode": "passThrough"
+ },
+ "name": "Merge4",
+ "type": "n8n-nodes-base.merge",
+ "typeVersion": 1,
+ "position": [
+ 1150,
+ 500
+ ]
+ },
+ {
+ "parameters": {
+ "values": {
+ "number": [
+ {
+ "name": "value2",
+ "value": 2
+ }
+ ]
+ }
+ },
+ "name": "Set2",
+ "type": "n8n-nodes-base.set",
+ "typeVersion": 1,
+ "position": [
+ 290,
+ 400
+ ]
+ },
+ {
+ "parameters": {
+ "values": {
+ "number": [
+ {
+ "name": "value4",
+ "value": 4
+ }
+ ]
+ }
+ },
+ "name": "Set4",
+ "type": "n8n-nodes-base.set",
+ "typeVersion": 1,
+ "position": [
+ 850,
+ 200
+ ]
+ },
+ {
+ "parameters": {
+ "values": {
+ "number": [
+ {
+ "name": "value3",
+ "value": 3
+ }
+ ]
+ }
+ },
+ "name": "Set3",
+ "type": "n8n-nodes-base.set",
+ "typeVersion": 1,
+ "position": [
+ 650,
+ 200
+ ]
+ },
+ {
+ "parameters": {
+ "mode": "passThrough"
+ },
+ "name": "Merge4",
+ "type": "n8n-nodes-base.merge",
+ "typeVersion": 1,
+ "position": [
+ 1150,
+ 500
+ ]
+ },
+ {
+ "parameters": {},
+ "name": "Merge3",
+ "type": "n8n-nodes-base.merge",
+ "typeVersion": 1,
+ "position": [
+ 1000,
+ 400
+ ]
+ },
+ {
+ "parameters": {
+ "mode": "passThrough",
+ "output": "input2"
+ },
+ "name": "Merge2",
+ "type": "n8n-nodes-base.merge",
+ "typeVersion": 1,
+ "position": [
+ 700,
+ 400
+ ]
+ },
+ {
+ "parameters": {},
+ "name": "Merge1",
+ "type": "n8n-nodes-base.merge",
+ "typeVersion": 1,
+ "position": [
+ 500,
+ 300
+ ]
+ },
+ {
+ "parameters": {
+ "values": {
+ "number": [
+ {
+ "name": "value1",
+ "value": 1
+ }
+ ]
+ }
+ },
+ "name": "Set1",
+ "type": "n8n-nodes-base.set",
+ "typeVersion": 1,
+ "position": [
+ 300,
+ 200
+ ]
+ },
+ {
+ "parameters": {},
+ "name": "Start",
+ "type": "n8n-nodes-base.start",
+ "typeVersion": 1,
+ "position": [
+ 100,
+ 300
+ ]
+ }
+ ],
+ "connections": {
+ "Set2": {
+ "main": [
+ [
+ {
+ "node": "Merge1",
+ "type": "main",
+ "index": 1
+ },
+ {
+ "node": "Merge2",
+ "type": "main",
+ "index": 1
+ }
+ ]
+ ]
+ },
+ "Set4": {
+ "main": [
+ [
+ {
+ "node": "Merge3",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Set3": {
+ "main": [
+ [
+ {
+ "node": "Set4",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Merge3": {
+ "main": [
+ [
+ {
+ "node": "Merge4",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Merge2": {
+ "main": [
+ [
+ {
+ "node": "Merge3",
+ "type": "main",
+ "index": 1
+ }
+ ]
+ ]
+ },
+ "Merge1": {
+ "main": [
+ [
+ {
+ "node": "Merge2",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Set1": {
+ "main": [
+ [
+ {
+ "node": "Merge1",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Set3",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Start": {
+ "main": [
+ [
+ {
+ "node": "Set1",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Set2",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Merge4",
+ "type": "main",
+ "index": 1
+ }
+ ]
+ ]
+ }
+ }
+ },
+ },
+ output: {
+ nodeExecutionOrder: [
+ 'Start',
+ 'Set1',
+ 'Set2',
+ 'Set3',
+ 'Merge1',
+ 'Set4',
+ 'Merge2',
+ 'Merge3',
+ 'Merge4',
+ ],
+ nodeData: {
+ Set1: [
+ [
+ {
+ value1: 1,
+ },
+ ],
+ ],
+ Set2: [
+ [
+ {
+ value2: 2,
+ },
+ ],
+ ],
+ Set3: [
+ [
+ {
+ value1: 1,
+ value3: 3,
+ },
+ ],
+ ],
+ Set4: [
+ [
+ {
+ value1: 1,
+ value3: 3,
+ value4: 4,
+ },
+ ],
+ ],
+ Merge1: [
+ [
+ {
+ value1: 1,
+ },
+ {
+ value2: 2,
+ },
+ ]
+ ],
+ Merge2: [
+ [
+ {
+ value2: 2,
+ },
+ ]
+ ],
+ Merge3: [
+ [
+ {
+ value1: 1,
+ value3: 3,
+ value4: 4,
+ },
+ {
+ value2: 2,
+ },
+ ]
+ ],
+ Merge4: [
+ [
+ {
+ value1: 1,
+ value3: 3,
+ value4: 4,
+ },
+ {
+ value2: 2,
+ },
+ ]
+ ],
+ },
+ },
+ },
+ ];
+
+
+ const executionMode = 'manual';
+ const nodeTypes = Helpers.NodeTypes();
+
+ for (const testData of tests) {
+ test(testData.description, async () => {
+
+ const workflowInstance = new Workflow('test', testData.input.workflowData.nodes, testData.input.workflowData.connections, false, nodeTypes);
+
+ const waitPromise = await createDeferredPromise();
+ const nodeExecutionOrder: string[] = [];
+ const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
+
+ const workflowExecute = new WorkflowExecute(additionalData, executionMode);
+
+ const executionId = await workflowExecute.run(workflowInstance, undefined);
+ expect(executionId).toBeDefined();
+
+ const result = await waitPromise.promise();
+
+ // Check if the output data of the nodes is correct
+ for (const nodeName of Object.keys(testData.output.nodeData)) {
+ if (result.data.resultData.runData[nodeName] === undefined) {
+ throw new Error(`Data for node "${nodeName}" is missing!`);
+ }
+
+ const resultData = result.data.resultData.runData[nodeName].map((nodeData) => {
+ if (nodeData.data === undefined) {
+ return null;
+ }
+ return nodeData.data.main[0]!.map((entry) => entry.json );
+ });
+
+ // expect(resultData).toEqual(testData.output.nodeData[nodeName]);
+ expect(resultData).toEqual(testData.output.nodeData[nodeName]);
+ }
+
+ // Check if the nodes did execute in the correct order
+ expect(nodeExecutionOrder).toEqual(testData.output.nodeExecutionOrder);
+
+ // Check if other data has correct value
+ expect(result.finished).toEqual(true);
+ expect(result.data.executionData!.contextData).toEqual({});
+ expect(result.data.executionData!.nodeExecutionStack).toEqual([]);
+ expect(result.data.executionData!.waitingExecution).toEqual({});
+ });
+ }
+
+ });
+
+});