mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
531 lines
12 KiB
TypeScript
531 lines
12 KiB
TypeScript
|
import {
|
||
|
mapLegacyConnectionsToCanvasConnections,
|
||
|
mapLegacyEndpointsToCanvasConnectionPort,
|
||
|
getUniqueNodeName,
|
||
|
} from '@/utils/canvasUtilsV2';
|
||
|
import type { IConnections, INodeTypeDescription } from 'n8n-workflow';
|
||
|
import type { CanvasConnection } from '@/types';
|
||
|
import type { INodeUi } from '@/Interface';
|
||
|
|
||
|
vi.mock('uuid', () => ({
|
||
|
v4: vi.fn(() => 'mock-uuid'),
|
||
|
}));
|
||
|
|
||
|
describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||
|
it('should map legacy connections to canvas connections', () => {
|
||
|
const legacyConnections: IConnections = {
|
||
|
'Node A': {
|
||
|
main: [[{ node: 'Node B', type: 'main', index: 0 }]],
|
||
|
},
|
||
|
};
|
||
|
const nodes: INodeUi[] = [
|
||
|
{
|
||
|
id: '1',
|
||
|
name: 'Node A',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [100, 100],
|
||
|
parameters: {},
|
||
|
},
|
||
|
{
|
||
|
id: '2',
|
||
|
name: 'Node B',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [200, 200],
|
||
|
parameters: {},
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const result: CanvasConnection[] = mapLegacyConnectionsToCanvasConnections(
|
||
|
legacyConnections,
|
||
|
nodes,
|
||
|
);
|
||
|
|
||
|
expect(result).toEqual([
|
||
|
{
|
||
|
id: '[1/main/0][2/main/0]',
|
||
|
source: '1',
|
||
|
target: '2',
|
||
|
sourceHandle: 'outputs/main/0',
|
||
|
targetHandle: 'inputs/main/0',
|
||
|
data: {
|
||
|
fromNodeName: 'Node A',
|
||
|
source: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
target: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('should return empty array when no matching nodes found', () => {
|
||
|
const legacyConnections: IConnections = {
|
||
|
'Node A': {
|
||
|
main: [[{ node: 'Node B', type: 'main', index: 0 }]],
|
||
|
},
|
||
|
};
|
||
|
const nodes: INodeUi[] = [];
|
||
|
|
||
|
const result: CanvasConnection[] = mapLegacyConnectionsToCanvasConnections(
|
||
|
legacyConnections,
|
||
|
nodes,
|
||
|
);
|
||
|
|
||
|
expect(result).toEqual([]);
|
||
|
});
|
||
|
|
||
|
it('should return empty array when no legacy connections provided', () => {
|
||
|
const legacyConnections: IConnections = {};
|
||
|
const nodes: INodeUi[] = [
|
||
|
{
|
||
|
id: '1',
|
||
|
name: 'Node A',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [100, 100],
|
||
|
parameters: {},
|
||
|
},
|
||
|
{
|
||
|
id: '2',
|
||
|
name: 'Node B',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [200, 200],
|
||
|
parameters: {},
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const result: CanvasConnection[] = mapLegacyConnectionsToCanvasConnections(
|
||
|
legacyConnections,
|
||
|
nodes,
|
||
|
);
|
||
|
|
||
|
expect(result).toEqual([]);
|
||
|
});
|
||
|
|
||
|
it('should map multiple connections between the same nodes', () => {
|
||
|
const legacyConnections: IConnections = {
|
||
|
'Node A': {
|
||
|
main: [
|
||
|
[{ node: 'Node B', type: 'main', index: 0 }],
|
||
|
[{ node: 'Node B', type: 'main', index: 1 }],
|
||
|
],
|
||
|
},
|
||
|
};
|
||
|
const nodes: INodeUi[] = [
|
||
|
{
|
||
|
id: '1',
|
||
|
name: 'Node A',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [100, 100],
|
||
|
parameters: {},
|
||
|
},
|
||
|
{
|
||
|
id: '2',
|
||
|
name: 'Node B',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [200, 200],
|
||
|
parameters: {},
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const result: CanvasConnection[] = mapLegacyConnectionsToCanvasConnections(
|
||
|
legacyConnections,
|
||
|
nodes,
|
||
|
);
|
||
|
|
||
|
expect(result).toEqual([
|
||
|
{
|
||
|
id: '[1/main/0][2/main/0]',
|
||
|
source: '1',
|
||
|
target: '2',
|
||
|
sourceHandle: 'outputs/main/0',
|
||
|
targetHandle: 'inputs/main/0',
|
||
|
data: {
|
||
|
fromNodeName: 'Node A',
|
||
|
source: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
target: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
id: '[1/main/1][2/main/1]',
|
||
|
source: '1',
|
||
|
target: '2',
|
||
|
sourceHandle: 'outputs/main/1',
|
||
|
targetHandle: 'inputs/main/1',
|
||
|
data: {
|
||
|
fromNodeName: 'Node A',
|
||
|
source: {
|
||
|
index: 1,
|
||
|
type: 'main',
|
||
|
},
|
||
|
target: {
|
||
|
index: 1,
|
||
|
type: 'main',
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('should map multiple connections from one node to different nodes', () => {
|
||
|
const legacyConnections: IConnections = {
|
||
|
'Node A': {
|
||
|
main: [
|
||
|
[{ node: 'Node B', type: 'main', index: 0 }],
|
||
|
[{ node: 'Node C', type: 'main', index: 0 }],
|
||
|
],
|
||
|
},
|
||
|
};
|
||
|
const nodes: INodeUi[] = [
|
||
|
{
|
||
|
id: '1',
|
||
|
name: 'Node A',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [100, 100],
|
||
|
parameters: {},
|
||
|
},
|
||
|
{
|
||
|
id: '2',
|
||
|
name: 'Node B',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [200, 200],
|
||
|
parameters: {},
|
||
|
},
|
||
|
{
|
||
|
id: '3',
|
||
|
name: 'Node C',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [300, 300],
|
||
|
parameters: {},
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const result: CanvasConnection[] = mapLegacyConnectionsToCanvasConnections(
|
||
|
legacyConnections,
|
||
|
nodes,
|
||
|
);
|
||
|
|
||
|
expect(result).toEqual([
|
||
|
{
|
||
|
id: '[1/main/0][2/main/0]',
|
||
|
source: '1',
|
||
|
target: '2',
|
||
|
sourceHandle: 'outputs/main/0',
|
||
|
targetHandle: 'inputs/main/0',
|
||
|
data: {
|
||
|
fromNodeName: 'Node A',
|
||
|
source: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
target: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
id: '[1/main/1][3/main/0]',
|
||
|
source: '1',
|
||
|
target: '3',
|
||
|
sourceHandle: 'outputs/main/1',
|
||
|
targetHandle: 'inputs/main/0',
|
||
|
data: {
|
||
|
fromNodeName: 'Node A',
|
||
|
source: {
|
||
|
index: 1,
|
||
|
type: 'main',
|
||
|
},
|
||
|
target: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('should map complex node setup with mixed inputs and outputs', () => {
|
||
|
const legacyConnections: IConnections = {
|
||
|
'Node A': {
|
||
|
main: [[{ node: 'Node B', type: 'main', index: 0 }]],
|
||
|
other: [[{ node: 'Node C', type: 'other', index: 1 }]],
|
||
|
},
|
||
|
'Node B': {
|
||
|
main: [[{ node: 'Node C', type: 'main', index: 0 }]],
|
||
|
},
|
||
|
};
|
||
|
const nodes: INodeUi[] = [
|
||
|
{
|
||
|
id: '1',
|
||
|
name: 'Node A',
|
||
|
typeVersion: 1,
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
position: [100, 100],
|
||
|
parameters: {},
|
||
|
},
|
||
|
{
|
||
|
id: '2',
|
||
|
name: 'Node B',
|
||
|
typeVersion: 1,
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
position: [200, 200],
|
||
|
parameters: {},
|
||
|
},
|
||
|
{
|
||
|
id: '3',
|
||
|
name: 'Node C',
|
||
|
typeVersion: 1,
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
position: [300, 300],
|
||
|
parameters: {},
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const result: CanvasConnection[] = mapLegacyConnectionsToCanvasConnections(
|
||
|
legacyConnections,
|
||
|
nodes,
|
||
|
);
|
||
|
|
||
|
expect(result).toEqual([
|
||
|
{
|
||
|
id: '[1/main/0][2/main/0]',
|
||
|
source: '1',
|
||
|
target: '2',
|
||
|
sourceHandle: 'outputs/main/0',
|
||
|
targetHandle: 'inputs/main/0',
|
||
|
data: {
|
||
|
fromNodeName: 'Node A',
|
||
|
source: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
target: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
id: '[1/other/0][3/other/1]',
|
||
|
source: '1',
|
||
|
target: '3',
|
||
|
sourceHandle: 'outputs/other/0',
|
||
|
targetHandle: 'inputs/other/1',
|
||
|
data: {
|
||
|
fromNodeName: 'Node A',
|
||
|
source: {
|
||
|
index: 0,
|
||
|
type: 'other',
|
||
|
},
|
||
|
target: {
|
||
|
index: 1,
|
||
|
type: 'other',
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
id: '[2/main/0][3/main/0]',
|
||
|
source: '2',
|
||
|
target: '3',
|
||
|
sourceHandle: 'outputs/main/0',
|
||
|
targetHandle: 'inputs/main/0',
|
||
|
data: {
|
||
|
fromNodeName: 'Node B',
|
||
|
source: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
target: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('should handle edge cases with invalid data gracefully', () => {
|
||
|
const legacyConnections: IConnections = {
|
||
|
'Node A': {
|
||
|
main: [
|
||
|
[{ node: 'Nonexistent Node', type: 'main', index: 0 }],
|
||
|
[{ node: 'Node B', type: 'main', index: 0 }],
|
||
|
],
|
||
|
},
|
||
|
};
|
||
|
const nodes: INodeUi[] = [
|
||
|
{
|
||
|
id: '1',
|
||
|
name: 'Node A',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [100, 100],
|
||
|
parameters: {},
|
||
|
},
|
||
|
{
|
||
|
id: '2',
|
||
|
name: 'Node B',
|
||
|
type: 'n8n-nodes-base.node',
|
||
|
typeVersion: 1,
|
||
|
position: [200, 200],
|
||
|
parameters: {},
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const result: CanvasConnection[] = mapLegacyConnectionsToCanvasConnections(
|
||
|
legacyConnections,
|
||
|
nodes,
|
||
|
);
|
||
|
|
||
|
expect(result).toEqual([
|
||
|
{
|
||
|
id: '[1/main/1][2/main/0]',
|
||
|
source: '1',
|
||
|
target: '2',
|
||
|
sourceHandle: 'outputs/main/1',
|
||
|
targetHandle: 'inputs/main/0',
|
||
|
data: {
|
||
|
fromNodeName: 'Node A',
|
||
|
source: {
|
||
|
index: 1,
|
||
|
type: 'main',
|
||
|
},
|
||
|
target: {
|
||
|
index: 0,
|
||
|
type: 'main',
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
]);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('mapLegacyEndpointsToCanvasConnectionPort', () => {
|
||
|
it('should return an empty array and log a warning when inputs is a string', () => {
|
||
|
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||
|
const endpoints: INodeTypeDescription['inputs'] = 'some code';
|
||
|
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
|
||
|
|
||
|
expect(result).toEqual([]);
|
||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||
|
'Node endpoints have not been evaluated',
|
||
|
'some code',
|
||
|
);
|
||
|
|
||
|
consoleWarnSpy.mockRestore();
|
||
|
});
|
||
|
|
||
|
it('should map string endpoints correctly', () => {
|
||
|
const endpoints: INodeTypeDescription['inputs'] = ['main', 'ai_tool'];
|
||
|
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
|
||
|
|
||
|
expect(result).toEqual([
|
||
|
{ type: 'main', index: 0, label: undefined },
|
||
|
{ type: 'ai_tool', index: 0, label: undefined },
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('should map object endpoints correctly', () => {
|
||
|
const endpoints: INodeTypeDescription['inputs'] = [
|
||
|
{ type: 'main', displayName: 'Main Input' },
|
||
|
{ type: 'ai_tool', displayName: 'AI Tool', required: true },
|
||
|
];
|
||
|
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
|
||
|
|
||
|
expect(result).toEqual([
|
||
|
{ type: 'main', index: 0, label: 'Main Input' },
|
||
|
{ type: 'ai_tool', index: 0, label: 'AI Tool', required: true },
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('should map mixed string and object endpoints correctly', () => {
|
||
|
const endpoints: INodeTypeDescription['inputs'] = [
|
||
|
'main',
|
||
|
{ type: 'ai_tool', displayName: 'AI Tool' },
|
||
|
'main',
|
||
|
];
|
||
|
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
|
||
|
|
||
|
expect(result).toEqual([
|
||
|
{ type: 'main', index: 0, label: undefined },
|
||
|
{ type: 'ai_tool', index: 0, label: 'AI Tool' },
|
||
|
{ type: 'main', index: 1, label: undefined },
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('should handle multiple same type object endpoints', () => {
|
||
|
const endpoints: INodeTypeDescription['inputs'] = [
|
||
|
{ type: 'main', displayName: 'Main Input' },
|
||
|
{ type: 'main', displayName: 'Secondary Main Input' },
|
||
|
];
|
||
|
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
|
||
|
|
||
|
expect(result).toEqual([
|
||
|
{ type: 'main', index: 0, label: 'Main Input' },
|
||
|
{ type: 'main', index: 1, label: 'Secondary Main Input' },
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('should map required and non-required endpoints correctly', () => {
|
||
|
const endpoints: INodeTypeDescription['inputs'] = [
|
||
|
{ type: 'main', displayName: 'Main Input', required: true },
|
||
|
{ type: 'ai_tool', displayName: 'Optional Tool', required: false },
|
||
|
];
|
||
|
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
|
||
|
|
||
|
expect(result).toEqual([
|
||
|
{ type: 'main', index: 0, label: 'Main Input', required: true },
|
||
|
{ type: 'ai_tool', index: 0, label: 'Optional Tool' },
|
||
|
]);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('getUniqueNodeName', () => {
|
||
|
it('should return the original name if it is unique', () => {
|
||
|
const name = 'Node A';
|
||
|
const existingNames = new Set(['Node B', 'Node C']);
|
||
|
const result = getUniqueNodeName(name, existingNames);
|
||
|
expect(result).toBe(name);
|
||
|
});
|
||
|
|
||
|
it('should append a number to the name if it already exists', () => {
|
||
|
const name = 'Node A';
|
||
|
const existingNames = new Set(['Node A', 'Node B']);
|
||
|
const result = getUniqueNodeName(name, existingNames);
|
||
|
expect(result).toBe('Node A 1');
|
||
|
});
|
||
|
|
||
|
it('should find the next available number for the name', () => {
|
||
|
const name = 'Node A';
|
||
|
const existingNames = new Set(['Node A', 'Node A 1', 'Node A 2']);
|
||
|
const result = getUniqueNodeName(name, existingNames);
|
||
|
expect(result).toBe('Node A 3');
|
||
|
});
|
||
|
|
||
|
it('should use UUID if more than 99 variations exist', () => {
|
||
|
const name = 'Node A';
|
||
|
const existingNames = new Set([...Array(100).keys()].map((i) => `Node A ${i}`).concat([name]));
|
||
|
const result = getUniqueNodeName(name, existingNames);
|
||
|
expect(result).toBe('Node A mock-uuid');
|
||
|
});
|
||
|
});
|