mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
chore: More tests for Workflow.ts (#13349)
This commit is contained in:
parent
81282bd5c0
commit
1d03c2c8ca
|
@ -2001,11 +2001,7 @@ export class WorkflowExecute {
|
||||||
ensureInputData(workflow: Workflow, executionNode: INode, executionData: IExecuteData): boolean {
|
ensureInputData(workflow: Workflow, executionNode: INode, executionData: IExecuteData): boolean {
|
||||||
const inputConnections = workflow.connectionsByDestinationNode[executionNode.name]?.main ?? [];
|
const inputConnections = workflow.connectionsByDestinationNode[executionNode.name]?.main ?? [];
|
||||||
for (let connectionIndex = 0; connectionIndex < inputConnections.length; connectionIndex++) {
|
for (let connectionIndex = 0; connectionIndex < inputConnections.length; connectionIndex++) {
|
||||||
const highestNodes = workflow.getHighestNode(
|
const highestNodes = workflow.getHighestNode(executionNode.name, connectionIndex);
|
||||||
executionNode.name,
|
|
||||||
NodeConnectionType.Main,
|
|
||||||
connectionIndex,
|
|
||||||
);
|
|
||||||
if (highestNodes.length === 0) {
|
if (highestNodes.length === 0) {
|
||||||
// If there is no valid incoming node (if all are disabled)
|
// If there is no valid incoming node (if all are disabled)
|
||||||
// then ignore that it has inputs and simply execute it as it is without
|
// then ignore that it has inputs and simply execute it as it is without
|
||||||
|
|
|
@ -458,7 +458,6 @@ export class Workflow {
|
||||||
*/
|
*/
|
||||||
getHighestNode(
|
getHighestNode(
|
||||||
nodeName: string,
|
nodeName: string,
|
||||||
type: NodeConnectionType = NodeConnectionType.Main,
|
|
||||||
nodeConnectionIndex?: number,
|
nodeConnectionIndex?: number,
|
||||||
checkedNodes?: string[],
|
checkedNodes?: string[],
|
||||||
): string[] {
|
): string[] {
|
||||||
|
@ -473,7 +472,7 @@ export class Workflow {
|
||||||
return currentHighest;
|
return currentHighest;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.connectionsByDestinationNode[nodeName].hasOwnProperty(type)) {
|
if (!this.connectionsByDestinationNode[nodeName].hasOwnProperty(NodeConnectionType.Main)) {
|
||||||
// Node does not have incoming connections of given type
|
// Node does not have incoming connections of given type
|
||||||
return currentHighest;
|
return currentHighest;
|
||||||
}
|
}
|
||||||
|
@ -493,14 +492,15 @@ export class Workflow {
|
||||||
let connectionsByIndex: IConnection[] | null;
|
let connectionsByIndex: IConnection[] | null;
|
||||||
for (
|
for (
|
||||||
let connectionIndex = 0;
|
let connectionIndex = 0;
|
||||||
connectionIndex < this.connectionsByDestinationNode[nodeName][type].length;
|
connectionIndex < this.connectionsByDestinationNode[nodeName][NodeConnectionType.Main].length;
|
||||||
connectionIndex++
|
connectionIndex++
|
||||||
) {
|
) {
|
||||||
if (nodeConnectionIndex !== undefined && nodeConnectionIndex !== connectionIndex) {
|
if (nodeConnectionIndex !== undefined && nodeConnectionIndex !== connectionIndex) {
|
||||||
// If a connection-index is given ignore all other ones
|
// If a connection-index is given ignore all other ones
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
connectionsByIndex = this.connectionsByDestinationNode[nodeName][type][connectionIndex];
|
connectionsByIndex =
|
||||||
|
this.connectionsByDestinationNode[nodeName][NodeConnectionType.Main][connectionIndex];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||||
connectionsByIndex?.forEach((connection) => {
|
connectionsByIndex?.forEach((connection) => {
|
||||||
if (checkedNodes.includes(connection.node)) {
|
if (checkedNodes.includes(connection.node)) {
|
||||||
|
@ -508,7 +508,7 @@ export class Workflow {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addNodes = this.getHighestNode(connection.node, type, undefined, checkedNodes);
|
addNodes = this.getHighestNode(connection.node, undefined, checkedNodes);
|
||||||
|
|
||||||
if (addNodes.length === 0) {
|
if (addNodes.length === 0) {
|
||||||
// The checked node does not have any further parents so add it
|
// The checked node does not have any further parents so add it
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,5 @@
|
||||||
|
import { DateTime, Duration, Interval } from 'luxon';
|
||||||
|
|
||||||
import { ensureError } from '@/errors/ensure-error';
|
import { ensureError } from '@/errors/ensure-error';
|
||||||
import { ExpressionError } from '@/errors/expression.error';
|
import { ExpressionError } from '@/errors/expression.error';
|
||||||
import {
|
import {
|
||||||
|
@ -590,4 +592,198 @@ describe('WorkflowDataProxy', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('DateTime and Time-related functions', () => {
|
||||||
|
const fixture = loadFixture('base');
|
||||||
|
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'End');
|
||||||
|
|
||||||
|
test('$now should return current datetime', () => {
|
||||||
|
expect(proxy.$now).toBeInstanceOf(DateTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$today should return datetime at start of day', () => {
|
||||||
|
const today = proxy.$today;
|
||||||
|
expect(today).toBeInstanceOf(DateTime);
|
||||||
|
expect(today.hour).toBe(0);
|
||||||
|
expect(today.minute).toBe(0);
|
||||||
|
expect(today.second).toBe(0);
|
||||||
|
expect(today.millisecond).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should expose DateTime, Interval, and Duration', () => {
|
||||||
|
expect(proxy.DateTime).toBe(DateTime);
|
||||||
|
expect(proxy.Interval).toBe(Interval);
|
||||||
|
expect(proxy.Duration).toBe(Duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$now should be configurable with timezone', () => {
|
||||||
|
const timezoneProxy = getProxyFromFixture(
|
||||||
|
{ ...fixture.workflow, settings: { timezone: 'America/New_York' } },
|
||||||
|
fixture.run,
|
||||||
|
'End',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(timezoneProxy.$now.zoneName).toBe('America/New_York');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Node version and ID', () => {
|
||||||
|
const fixture = loadFixture('base');
|
||||||
|
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'End');
|
||||||
|
|
||||||
|
test('$nodeVersion should return node type version', () => {
|
||||||
|
expect(proxy.$nodeVersion).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$nodeId should return node ID', () => {
|
||||||
|
expect(proxy.$nodeId).toBe('uuid-5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$webhookId should be optional', () => {
|
||||||
|
expect(proxy.$webhookId).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('$jmesPath', () => {
|
||||||
|
const fixture = loadFixture('base');
|
||||||
|
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'End');
|
||||||
|
|
||||||
|
test('should query simple object', () => {
|
||||||
|
const data = { name: 'John', age: 30 };
|
||||||
|
expect(proxy.$jmesPath(data, 'name')).toBe('John');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should query nested object', () => {
|
||||||
|
const data = {
|
||||||
|
user: {
|
||||||
|
name: 'John',
|
||||||
|
details: { age: 30 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(proxy.$jmesPath(data, 'user.details.age')).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should query array', () => {
|
||||||
|
const data = [
|
||||||
|
{ name: 'John', age: 30 },
|
||||||
|
{ name: 'Jane', age: 25 },
|
||||||
|
];
|
||||||
|
expect(proxy.$jmesPath(data, '[*].name')).toEqual(['John', 'Jane']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error for invalid arguments', () => {
|
||||||
|
expect(() => proxy.$jmesPath('not an object', 'test')).toThrow(ExpressionError);
|
||||||
|
expect(() => proxy.$jmesPath({}, 123 as unknown as string)).toThrow(ExpressionError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$jmespath should alias $jmesPath', () => {
|
||||||
|
const data = { name: 'John' };
|
||||||
|
expect(proxy.$jmespath(data, 'name')).toBe(proxy.$jmesPath(data, 'name'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('$mode', () => {
|
||||||
|
const fixture = loadFixture('base');
|
||||||
|
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'End', 'manual');
|
||||||
|
|
||||||
|
test('should return execution mode', () => {
|
||||||
|
expect(proxy.$mode).toBe('manual');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('$item', () => {
|
||||||
|
const fixture = loadFixture('base');
|
||||||
|
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'End');
|
||||||
|
|
||||||
|
test('should return data proxy for specific item', () => {
|
||||||
|
const itemProxy = proxy.$item(1);
|
||||||
|
expect(itemProxy.$json.data).toBe(160);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow specifying run index', () => {
|
||||||
|
const itemProxy = proxy.$item(1, 0);
|
||||||
|
expect(itemProxy.$json.data).toBe(160);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('$items', () => {
|
||||||
|
const fixture = loadFixture('base');
|
||||||
|
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'End');
|
||||||
|
|
||||||
|
describe('Default behavior (no arguments)', () => {
|
||||||
|
test('should return input items from previous node', () => {
|
||||||
|
const items = proxy.$items();
|
||||||
|
expect(items.length).toBe(5);
|
||||||
|
expect(items[0].json.data).toBe(105);
|
||||||
|
expect(items[1].json.data).toBe(160);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should limit items for nodes with executeOnce=true', () => {
|
||||||
|
// Mock a node with executeOnce=true
|
||||||
|
const mockWorkflow = {
|
||||||
|
...fixture.workflow,
|
||||||
|
nodes: fixture.workflow.nodes.map((node) =>
|
||||||
|
node.name === 'Rename' ? { ...node, executeOnce: true } : node,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProxy = getProxyFromFixture(mockWorkflow, fixture.run, 'End');
|
||||||
|
const items = mockProxy.$items();
|
||||||
|
|
||||||
|
expect(items.length).toBe(1);
|
||||||
|
expect(items[0].json.data).toBe(105);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('With node name argument', () => {
|
||||||
|
test('should return items for specified node', () => {
|
||||||
|
const items = proxy.$items('Rename');
|
||||||
|
expect(items.length).toBe(5);
|
||||||
|
expect(items[0].json.data).toBe(105);
|
||||||
|
expect(items[1].json.data).toBe(160);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error for non-existent node', () => {
|
||||||
|
expect(() => proxy.$items('NonExistentNode')).toThrowError(ExpressionError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('With node name and output index', () => {
|
||||||
|
const switchWorkflow = loadFixture('multiple_outputs');
|
||||||
|
const switchProxy = getProxyFromFixture(
|
||||||
|
switchWorkflow.workflow,
|
||||||
|
switchWorkflow.run,
|
||||||
|
'Edit Fields',
|
||||||
|
);
|
||||||
|
|
||||||
|
test('should return items from specific output', () => {
|
||||||
|
const items = switchProxy.$items('If', 1);
|
||||||
|
expect(items[0].json.code).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('With node name, output index, and run index', () => {
|
||||||
|
test('should handle negative run index', () => {
|
||||||
|
const items = proxy.$items('Rename', 0, -1);
|
||||||
|
expect(items.length).toBe(5);
|
||||||
|
expect(items[0].json.data).toBe(105);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error handling', () => {
|
||||||
|
test('should throw error for invalid run index', () => {
|
||||||
|
expect(() => proxy.$items('Rename', 0, 999)).toThrowError(ExpressionError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle nodes with no execution data', () => {
|
||||||
|
const noDataWorkflow = {
|
||||||
|
...fixture.workflow,
|
||||||
|
nodes: fixture.workflow.nodes.filter((node) => node.name !== 'Rename'),
|
||||||
|
};
|
||||||
|
const noDataProxy = getProxyFromFixture(noDataWorkflow, null, 'End');
|
||||||
|
|
||||||
|
expect(() => noDataProxy.$items('Rename')).toThrowError(ExpressionError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue