mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 02:51:14 +00:00
3074 lines
96 KiB
TypeScript
3074 lines
96 KiB
TypeScript
import { setActivePinia } from 'pinia';
|
||
import type {
|
||
IConnection,
|
||
INodeTypeDescription,
|
||
IWebhookDescription,
|
||
Workflow,
|
||
INodeConnections,
|
||
WorkflowExecuteMode,
|
||
} from 'n8n-workflow';
|
||
import { NodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
|
||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||
import type { CanvasConnection, CanvasNode } from '@/types';
|
||
import { CanvasConnectionMode } from '@/types';
|
||
import type {
|
||
ICredentialsResponse,
|
||
IExecutionResponse,
|
||
INodeUi,
|
||
IWorkflowDb,
|
||
IWorkflowTemplate,
|
||
IWorkflowTemplateNode,
|
||
} from '@/Interface';
|
||
import { RemoveNodeCommand } from '@/models/history';
|
||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||
import { useUIStore } from '@/stores/ui.store';
|
||
import { useHistoryStore } from '@/stores/history.store';
|
||
import { useNDVStore } from '@/stores/ndv.store';
|
||
import {
|
||
createTestNode,
|
||
createTestWorkflow,
|
||
createTestWorkflowObject,
|
||
mockNode,
|
||
mockNodeTypeDescription,
|
||
} from '@/__tests__/mocks';
|
||
import { useRouter } from 'vue-router';
|
||
import { mock } from 'vitest-mock-extended';
|
||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||
import { waitFor } from '@testing-library/vue';
|
||
import { createTestingPinia } from '@pinia/testing';
|
||
import { mockedStore } from '@/__tests__/utils';
|
||
import {
|
||
AGENT_NODE_TYPE,
|
||
FORM_TRIGGER_NODE_TYPE,
|
||
SET_NODE_TYPE,
|
||
STICKY_NODE_TYPE,
|
||
STORES,
|
||
WEBHOOK_NODE_TYPE,
|
||
} from '@/constants';
|
||
import type { Connection } from '@vue-flow/core';
|
||
import { useClipboard } from '@/composables/useClipboard';
|
||
import { createCanvasConnectionHandleString } from '@/utils/canvasUtils';
|
||
import { nextTick } from 'vue';
|
||
import { useProjectsStore } from '@/stores/projects.store';
|
||
import type { CanvasLayoutEvent } from './useCanvasLayout';
|
||
import { useTelemetry } from './useTelemetry';
|
||
import { useToast } from '@/composables/useToast';
|
||
|
||
vi.mock('vue-router', async (importOriginal) => {
|
||
const actual = await importOriginal<{}>();
|
||
return {
|
||
...actual,
|
||
useRouter: () => ({}),
|
||
};
|
||
});
|
||
|
||
vi.mock('n8n-workflow', async (importOriginal) => {
|
||
const actual = await importOriginal<{}>();
|
||
return {
|
||
...actual,
|
||
TelemetryHelpers: {
|
||
generateNodesGraph: vi.fn().mockReturnValue({
|
||
nodeGraph: {
|
||
nodes: [],
|
||
},
|
||
}),
|
||
},
|
||
};
|
||
});
|
||
|
||
vi.mock('@/composables/useClipboard', async () => {
|
||
const copySpy = vi.fn();
|
||
return { useClipboard: vi.fn(() => ({ copy: copySpy })) };
|
||
});
|
||
|
||
vi.mock('@/composables/useTelemetry', () => {
|
||
const track = vi.fn();
|
||
return {
|
||
useTelemetry: () => ({ track }),
|
||
};
|
||
});
|
||
|
||
vi.mock('@/composables/useToast', () => {
|
||
const showMessage = vi.fn();
|
||
const showError = vi.fn();
|
||
const showToast = vi.fn();
|
||
return {
|
||
useToast: () => {
|
||
return {
|
||
showMessage,
|
||
showError,
|
||
showToast,
|
||
};
|
||
},
|
||
};
|
||
});
|
||
|
||
describe('useCanvasOperations', () => {
|
||
const router = useRouter();
|
||
|
||
const workflowId = 'test';
|
||
const initialState = {
|
||
[STORES.NODE_TYPES]: {},
|
||
[STORES.NDV]: {},
|
||
[STORES.WORKFLOWS]: {
|
||
workflowId,
|
||
workflow: mock<IWorkflowDb>({
|
||
id: workflowId,
|
||
nodes: [],
|
||
connections: {},
|
||
tags: [],
|
||
usedCredentials: [],
|
||
}),
|
||
},
|
||
[STORES.SETTINGS]: {
|
||
settings: {
|
||
enterprise: {},
|
||
},
|
||
},
|
||
};
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
|
||
const pinia = createTestingPinia({ initialState });
|
||
setActivePinia(pinia);
|
||
});
|
||
|
||
describe('requireNodeTypeDescription', () => {
|
||
it('should return node type description when type and version match', () => {
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const type = 'testType';
|
||
const version = 1;
|
||
const expectedDescription = mockNodeTypeDescription({ name: type, version });
|
||
|
||
nodeTypesStore.nodeTypes = { [type]: { [version]: expectedDescription } };
|
||
|
||
const { requireNodeTypeDescription } = useCanvasOperations({ router });
|
||
const result = requireNodeTypeDescription(type, version);
|
||
|
||
expect(result).toBe(expectedDescription);
|
||
});
|
||
|
||
it('should return node type description when only type is provided and it exists', () => {
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const type = 'testTypeWithoutVersion';
|
||
const expectedDescription = mockNodeTypeDescription({ name: type });
|
||
|
||
nodeTypesStore.nodeTypes = { [type]: { 2: expectedDescription } };
|
||
|
||
const { requireNodeTypeDescription } = useCanvasOperations({ router });
|
||
const result = requireNodeTypeDescription(type);
|
||
|
||
expect(result).toBe(expectedDescription);
|
||
});
|
||
|
||
it("should return placeholder node type description if node type doesn't exist", () => {
|
||
const type = 'nonexistentType';
|
||
|
||
const { requireNodeTypeDescription } = useCanvasOperations({ router });
|
||
const result = requireNodeTypeDescription(type);
|
||
|
||
expect(result).toEqual({
|
||
name: type,
|
||
displayName: type,
|
||
description: '',
|
||
defaults: {},
|
||
group: [],
|
||
inputs: [],
|
||
outputs: [],
|
||
properties: [],
|
||
version: 1,
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('addNode', () => {
|
||
it('should create node with default version when version is undefined', () => {
|
||
const { addNode } = useCanvasOperations({ router });
|
||
const result = addNode(
|
||
{
|
||
name: 'example',
|
||
type: 'type',
|
||
typeVersion: 1,
|
||
},
|
||
mockNodeTypeDescription({ name: 'type' }),
|
||
);
|
||
|
||
expect(result.typeVersion).toBe(1);
|
||
});
|
||
|
||
it('should create node with default position when position is not provided', () => {
|
||
const { addNode } = useCanvasOperations({ router });
|
||
const result = addNode(
|
||
{
|
||
type: 'type',
|
||
typeVersion: 1,
|
||
},
|
||
mockNodeTypeDescription({ name: 'type' }),
|
||
);
|
||
|
||
expect(result.position).toEqual([0, 0]); // Default last click position
|
||
});
|
||
|
||
it('should create node with provided position when position is provided', () => {
|
||
const { addNode } = useCanvasOperations({ router });
|
||
const result = addNode(
|
||
{
|
||
type: 'type',
|
||
typeVersion: 1,
|
||
position: [20, 20],
|
||
},
|
||
mockNodeTypeDescription({ name: 'type' }),
|
||
);
|
||
|
||
expect(result.position).toEqual([20, 20]);
|
||
});
|
||
|
||
it('should not assign credentials when multiple credentials are available', () => {
|
||
const credentialsStore = useCredentialsStore();
|
||
const credentialA = mock<ICredentialsResponse>({ id: '1', name: 'credA', type: 'cred' });
|
||
const credentialB = mock<ICredentialsResponse>({ id: '1', name: 'credB', type: 'cred' });
|
||
const nodeTypeName = 'type';
|
||
const nodeTypeDescription = mockNodeTypeDescription({
|
||
name: nodeTypeName,
|
||
credentials: [{ name: credentialA.name }, { name: credentialB.name }],
|
||
});
|
||
|
||
// @ts-expect-error Known pinia issue when spying on store getters
|
||
vi.spyOn(credentialsStore, 'getUsableCredentialByType', 'get').mockReturnValue(() => [
|
||
credentialA,
|
||
credentialB,
|
||
]);
|
||
|
||
const { addNode } = useCanvasOperations({ router });
|
||
const result = addNode(
|
||
{
|
||
type: 'type',
|
||
typeVersion: 1,
|
||
},
|
||
nodeTypeDescription,
|
||
);
|
||
expect(result.credentials).toBeUndefined();
|
||
});
|
||
|
||
it('should open NDV when specified', async () => {
|
||
const ndvStore = useNDVStore();
|
||
const nodeTypeDescription = mockNodeTypeDescription({ name: 'type' });
|
||
|
||
const { addNode } = useCanvasOperations({ router });
|
||
addNode(
|
||
{
|
||
type: 'type',
|
||
typeVersion: 1,
|
||
name: 'Test Name',
|
||
},
|
||
nodeTypeDescription,
|
||
{ openNDV: true },
|
||
);
|
||
|
||
await waitFor(() => expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith('Test Name'));
|
||
});
|
||
|
||
it('should not set sticky node type as active node', async () => {
|
||
const ndvStore = useNDVStore();
|
||
const nodeTypeDescription = mockNodeTypeDescription({ name: STICKY_NODE_TYPE });
|
||
|
||
const { addNode } = useCanvasOperations({ router });
|
||
addNode(
|
||
{
|
||
type: STICKY_NODE_TYPE,
|
||
typeVersion: 1,
|
||
name: 'Test Name',
|
||
},
|
||
nodeTypeDescription,
|
||
{ openNDV: true },
|
||
);
|
||
|
||
await waitFor(() => expect(ndvStore.setActiveNodeName).not.toHaveBeenCalled());
|
||
});
|
||
});
|
||
|
||
describe('resolveNodePosition', () => {
|
||
it('should return the node position if it is already set', () => {
|
||
const node = createTestNode({ position: [100, 100] });
|
||
const nodeTypeDescription = mockNodeTypeDescription();
|
||
|
||
const { resolveNodePosition } = useCanvasOperations({ router });
|
||
const position = resolveNodePosition(node, nodeTypeDescription);
|
||
|
||
expect(position).toEqual([100, 100]);
|
||
});
|
||
|
||
it('should place the node at the last cancelled connection position', () => {
|
||
const uiStore = mockedStore(useUIStore);
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const node = createTestNode({ id: '0' });
|
||
const nodeTypeDescription = mockNodeTypeDescription();
|
||
|
||
vi.spyOn(uiStore, 'lastInteractedWithNode', 'get').mockReturnValue(node);
|
||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||
createTestWorkflowObject(workflowsStore.workflow),
|
||
);
|
||
|
||
uiStore.lastInteractedWithNodeHandle = 'inputs/main/0';
|
||
uiStore.lastCancelledConnectionPosition = [200, 200];
|
||
|
||
const { resolveNodePosition } = useCanvasOperations({ router });
|
||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||
|
||
expect(position).toEqual([200, 160]);
|
||
expect(uiStore.lastCancelledConnectionPosition).toBeUndefined();
|
||
});
|
||
|
||
it('should place the node to the right of the last interacted with node', () => {
|
||
const uiStore = mockedStore(useUIStore);
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
const node = createTestNode({ id: '0' });
|
||
const nodeTypeDescription = mockNodeTypeDescription();
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
|
||
uiStore.lastInteractedWithNode = createTestNode({
|
||
position: [100, 100],
|
||
type: 'test',
|
||
typeVersion: 1,
|
||
});
|
||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowObject.getNode = vi.fn().mockReturnValue(node);
|
||
|
||
const { resolveNodePosition } = useCanvasOperations({ router });
|
||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||
|
||
expect(position).toEqual([320, 100]);
|
||
});
|
||
|
||
it('should place the node below the last interacted with node if it has non-main outputs', () => {
|
||
const uiStore = mockedStore(useUIStore);
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
const node = createTestNode({ id: '0' });
|
||
const nodeTypeDescription = mockNodeTypeDescription();
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
|
||
uiStore.lastInteractedWithNode = createTestNode({
|
||
position: [100, 100],
|
||
type: 'test',
|
||
typeVersion: 1,
|
||
});
|
||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowObject.getNode = vi.fn().mockReturnValue(node);
|
||
|
||
vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
|
||
{ type: NodeConnectionTypes.AiTool },
|
||
]);
|
||
vi.spyOn(NodeHelpers, 'getConnectionTypes')
|
||
.mockReturnValueOnce([NodeConnectionTypes.AiTool])
|
||
.mockReturnValueOnce([NodeConnectionTypes.AiTool]);
|
||
|
||
const { resolveNodePosition } = useCanvasOperations({ router });
|
||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||
|
||
expect(position).toEqual([460, 100]);
|
||
});
|
||
|
||
it('should place the node at the last clicked position if no other position is set', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
|
||
const node = createTestNode({ id: '0' });
|
||
const nodeTypeDescription = mockNodeTypeDescription();
|
||
|
||
workflowsStore.workflowTriggerNodes = [
|
||
createTestNode({ id: 'trigger', position: [100, 100] }),
|
||
];
|
||
|
||
const { resolveNodePosition, lastClickPosition } = useCanvasOperations({ router });
|
||
lastClickPosition.value = [300, 300];
|
||
|
||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||
|
||
expect(position).toEqual([300, 300]);
|
||
});
|
||
|
||
it('should place the trigger node at the root if it is the first trigger node', () => {
|
||
const node = createTestNode({ id: '0' });
|
||
const nodeTypeDescription = mockNodeTypeDescription();
|
||
|
||
const { resolveNodePosition } = useCanvasOperations({ router });
|
||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||
|
||
expect(position).toEqual([0, 0]);
|
||
});
|
||
});
|
||
|
||
describe('updateNodesPosition', () => {
|
||
it('records history for multiple node position updates when tracking is enabled', () => {
|
||
const historyStore = useHistoryStore();
|
||
const events = [
|
||
{ id: 'node1', position: { x: 100, y: 100 } },
|
||
{ id: 'node2', position: { x: 200, y: 200 } },
|
||
];
|
||
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
|
||
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
|
||
|
||
const { updateNodesPosition } = useCanvasOperations({ router });
|
||
updateNodesPosition(events, { trackHistory: true, trackBulk: true });
|
||
|
||
expect(startRecordingUndoSpy).toHaveBeenCalled();
|
||
expect(stopRecordingUndoSpy).toHaveBeenCalled();
|
||
});
|
||
|
||
it('updates positions for multiple nodes', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const events = [
|
||
{ id: 'node1', position: { x: 100, y: 100 } },
|
||
{ id: 'node2', position: { x: 200, y: 200 } },
|
||
];
|
||
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
|
||
workflowsStore.getNodeById
|
||
.mockReturnValueOnce(
|
||
createTestNode({
|
||
id: events[0].id,
|
||
position: [events[0].position.x, events[0].position.y],
|
||
}),
|
||
)
|
||
.mockReturnValueOnce(
|
||
createTestNode({
|
||
id: events[1].id,
|
||
position: [events[1].position.x, events[1].position.y],
|
||
}),
|
||
);
|
||
|
||
const { updateNodesPosition } = useCanvasOperations({ router });
|
||
updateNodesPosition(events);
|
||
|
||
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
|
||
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [100, 100]);
|
||
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [200, 200]);
|
||
});
|
||
|
||
it('does not record history when trackHistory is false', () => {
|
||
const historyStore = useHistoryStore();
|
||
const events = [{ id: 'node1', position: { x: 100, y: 100 } }];
|
||
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
|
||
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
|
||
|
||
const { updateNodesPosition } = useCanvasOperations({ router });
|
||
updateNodesPosition(events, { trackHistory: false, trackBulk: false });
|
||
|
||
expect(startRecordingUndoSpy).not.toHaveBeenCalled();
|
||
expect(stopRecordingUndoSpy).not.toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('tidyUp', () => {
|
||
it('records history for multiple node position updates', () => {
|
||
const historyStore = useHistoryStore();
|
||
const event: CanvasLayoutEvent = {
|
||
source: 'canvas-button',
|
||
target: 'all',
|
||
result: {
|
||
nodes: [
|
||
{ id: 'node1', x: 100, y: 100 },
|
||
{ id: 'node2', x: 200, y: 200 },
|
||
],
|
||
boundingBox: { height: 100, width: 100, x: 0, y: 0 },
|
||
},
|
||
};
|
||
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
|
||
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
|
||
|
||
const { tidyUp } = useCanvasOperations({ router });
|
||
tidyUp(event);
|
||
|
||
expect(startRecordingUndoSpy).toHaveBeenCalled();
|
||
expect(stopRecordingUndoSpy).toHaveBeenCalled();
|
||
});
|
||
|
||
it('updates positions for multiple nodes', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const event: CanvasLayoutEvent = {
|
||
source: 'canvas-button',
|
||
target: 'all',
|
||
result: {
|
||
nodes: [
|
||
{ id: 'node1', x: 100, y: 100 },
|
||
{ id: 'node2', x: 200, y: 200 },
|
||
],
|
||
boundingBox: { height: 100, width: 100, x: 0, y: 0 },
|
||
},
|
||
};
|
||
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
|
||
workflowsStore.getNodeById
|
||
.mockReturnValueOnce(
|
||
createTestNode({
|
||
id: event.result.nodes[0].id,
|
||
position: [event.result.nodes[0].x, event.result.nodes[0].y],
|
||
}),
|
||
)
|
||
.mockReturnValueOnce(
|
||
createTestNode({
|
||
id: event.result.nodes[1].id,
|
||
position: [event.result.nodes[1].x, event.result.nodes[1].y],
|
||
}),
|
||
);
|
||
|
||
const { tidyUp } = useCanvasOperations({ router });
|
||
tidyUp(event);
|
||
|
||
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
|
||
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [100, 100]);
|
||
expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [200, 200]);
|
||
});
|
||
|
||
it('should send a "User tidied up workflow" telemetry event', () => {
|
||
const event: CanvasLayoutEvent = {
|
||
source: 'canvas-button',
|
||
target: 'all',
|
||
result: {
|
||
nodes: [
|
||
{ id: 'node1', x: 100, y: 100 },
|
||
{ id: 'node2', x: 200, y: 200 },
|
||
],
|
||
boundingBox: { height: 100, width: 100, x: 0, y: 0 },
|
||
},
|
||
};
|
||
|
||
const { tidyUp } = useCanvasOperations({ router });
|
||
tidyUp(event);
|
||
|
||
expect(useTelemetry().track).toHaveBeenCalledWith(
|
||
'User tidied up canvas',
|
||
{
|
||
nodes_count: 2,
|
||
source: 'canvas-button',
|
||
target: 'all',
|
||
},
|
||
{ withPostHog: true },
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('updateNodePosition', () => {
|
||
it('should update node position', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const id = 'node1';
|
||
const position: CanvasNode['position'] = { x: 10, y: 20 };
|
||
const node = createTestNode({
|
||
id,
|
||
type: 'node',
|
||
position: [0, 0],
|
||
name: 'Node 1',
|
||
});
|
||
|
||
workflowsStore.getNodeById.mockReturnValueOnce(node);
|
||
|
||
const { updateNodePosition } = useCanvasOperations({ router });
|
||
updateNodePosition(id, position);
|
||
|
||
expect(workflowsStore.setNodePositionById).toHaveBeenCalledWith(id, [position.x, position.y]);
|
||
});
|
||
});
|
||
|
||
describe('setNodeSelected', () => {
|
||
it('should set last selected node when node id is provided and node exists', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const uiStore = useUIStore();
|
||
const nodeId = 'node1';
|
||
const nodeName = 'Node 1';
|
||
workflowsStore.getNodeById = vi.fn().mockReturnValue({ name: nodeName });
|
||
uiStore.lastSelectedNode = '';
|
||
|
||
const { setNodeSelected } = useCanvasOperations({ router });
|
||
setNodeSelected(nodeId);
|
||
|
||
expect(uiStore.lastSelectedNode).toBe(nodeName);
|
||
});
|
||
|
||
it('should not change last selected node when node id is provided but node does not exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const uiStore = useUIStore();
|
||
const nodeId = 'node1';
|
||
workflowsStore.getNodeById = vi.fn().mockReturnValue(undefined);
|
||
uiStore.lastSelectedNode = 'Existing Node';
|
||
|
||
const { setNodeSelected } = useCanvasOperations({ router });
|
||
setNodeSelected(nodeId);
|
||
|
||
expect(uiStore.lastSelectedNode).toBe('Existing Node');
|
||
});
|
||
|
||
it('should clear last selected node when node id is not provided', () => {
|
||
const uiStore = useUIStore();
|
||
uiStore.lastSelectedNode = 'Existing Node';
|
||
|
||
const { setNodeSelected } = useCanvasOperations({ router });
|
||
setNodeSelected();
|
||
|
||
expect(uiStore.lastSelectedNode).toBe('');
|
||
});
|
||
});
|
||
|
||
describe('addNodes', () => {
|
||
it('should add nodes at specified positions', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const nodeTypeName = 'type';
|
||
const nodes = [
|
||
mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] }),
|
||
mockNode({ name: 'Node 2', type: nodeTypeName, position: [100, 240] }),
|
||
];
|
||
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||
createTestWorkflowObject(workflowsStore.workflow),
|
||
);
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||
};
|
||
|
||
const { addNodes } = useCanvasOperations({ router });
|
||
await addNodes(nodes, {});
|
||
|
||
expect(workflowsStore.addNode).toHaveBeenCalledTimes(2);
|
||
expect(workflowsStore.addNode.mock.calls[0][0]).toMatchObject({
|
||
name: nodes[0].name,
|
||
type: nodeTypeName,
|
||
typeVersion: 1,
|
||
position: [40, 40],
|
||
parameters: {},
|
||
});
|
||
expect(workflowsStore.addNode.mock.calls[1][0]).toMatchObject({
|
||
name: nodes[1].name,
|
||
type: nodeTypeName,
|
||
typeVersion: 1,
|
||
position: [100, 240],
|
||
parameters: {},
|
||
});
|
||
});
|
||
|
||
it('should add nodes at current position when position is not specified', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const nodeTypeName = 'type';
|
||
const nodes = [
|
||
mockNode({ name: 'Node 1', type: nodeTypeName, position: [120, 120] }),
|
||
mockNode({ name: 'Node 2', type: nodeTypeName, position: [180, 320] }),
|
||
];
|
||
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||
createTestWorkflowObject(workflowsStore.workflow),
|
||
);
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||
};
|
||
|
||
const { addNodes } = useCanvasOperations({ router });
|
||
await addNodes(nodes, { position: [50, 60] });
|
||
|
||
expect(workflowsStore.addNode).toHaveBeenCalledTimes(2);
|
||
expect(workflowsStore.addNode.mock.calls[0][0].position).toEqual(
|
||
expect.arrayContaining(nodes[0].position),
|
||
);
|
||
expect(workflowsStore.addNode.mock.calls[1][0].position).toEqual(
|
||
expect.arrayContaining(nodes[1].position),
|
||
);
|
||
});
|
||
|
||
it('should adjust the position of nodes with multiple inputs', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const nodeTypeName = 'type';
|
||
const nodes = [
|
||
mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [40, 40] }),
|
||
mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [40, 40] }),
|
||
mockNode({ id: 'c', name: 'Node C', type: nodeTypeName, position: [100, 240] }),
|
||
];
|
||
|
||
const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById');
|
||
workflowsStore.getNodeByName.mockReturnValueOnce(nodes[1]).mockReturnValueOnce(nodes[2]);
|
||
workflowsStore.getNodeById.mockReturnValueOnce(nodes[1]).mockReturnValueOnce(nodes[2]);
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||
};
|
||
|
||
vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockImplementation(() =>
|
||
mock<Workflow>({
|
||
getParentNodesByDepth: () =>
|
||
nodes.map((node) => ({
|
||
name: node.name,
|
||
depth: 0,
|
||
indicies: [],
|
||
})),
|
||
}),
|
||
);
|
||
|
||
const { addNodes } = useCanvasOperations({ router });
|
||
await addNodes(nodes, {});
|
||
|
||
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
|
||
expect(setNodePositionByIdSpy).toHaveBeenCalledWith(nodes[1].id, expect.any(Object));
|
||
expect(setNodePositionByIdSpy).toHaveBeenCalledWith(nodes[2].id, expect.any(Object));
|
||
});
|
||
|
||
it('should return newly added nodes', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const nodeTypeName = 'type';
|
||
const nodes = [
|
||
mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] }),
|
||
mockNode({ name: 'Node 2', type: nodeTypeName, position: [100, 240] }),
|
||
];
|
||
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||
createTestWorkflowObject(workflowsStore.workflow),
|
||
);
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||
};
|
||
|
||
const { addNodes } = useCanvasOperations({ router });
|
||
const added = await addNodes(nodes, {});
|
||
expect(added.length).toBe(2);
|
||
});
|
||
|
||
it('should mark UI state as dirty', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const uiStore = mockedStore(useUIStore);
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const nodeTypeName = 'type';
|
||
const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })];
|
||
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||
createTestWorkflowObject(workflowsStore.workflow),
|
||
);
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||
};
|
||
|
||
const { addNodes } = useCanvasOperations({ router });
|
||
await addNodes(nodes, { keepPristine: false });
|
||
|
||
expect(uiStore.stateIsDirty).toEqual(true);
|
||
});
|
||
|
||
it('should not mark UI state as dirty if keepPristine is true', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const uiStore = mockedStore(useUIStore);
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const nodeTypeName = 'type';
|
||
const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })];
|
||
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||
createTestWorkflowObject(workflowsStore.workflow),
|
||
);
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||
};
|
||
|
||
const { addNodes } = useCanvasOperations({ router });
|
||
await addNodes(nodes, { keepPristine: true });
|
||
|
||
expect(uiStore.stateIsDirty).toEqual(false);
|
||
});
|
||
});
|
||
|
||
describe('revertAddNode', () => {
|
||
it('deletes node if it exists', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const node = createTestNode();
|
||
workflowsStore.getNodeByName.mockReturnValueOnce(node);
|
||
workflowsStore.getNodeById.mockReturnValueOnce(node);
|
||
const removeNodeByIdSpy = vi.spyOn(workflowsStore, 'removeNodeById');
|
||
|
||
const { revertAddNode } = useCanvasOperations({ router });
|
||
await revertAddNode(node.name);
|
||
|
||
expect(removeNodeByIdSpy).toHaveBeenCalledWith(node.id);
|
||
});
|
||
});
|
||
|
||
describe('deleteNode', () => {
|
||
it('should delete node and track history', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const historyStore = mockedStore(useHistoryStore);
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
|
||
|
||
const id = 'node1';
|
||
const node: INodeUi = createTestNode({
|
||
id,
|
||
type: 'node',
|
||
position: [10, 20],
|
||
name: 'Node 1',
|
||
});
|
||
|
||
workflowsStore.getNodeById.mockReturnValue(node);
|
||
|
||
const { deleteNode } = useCanvasOperations({ router });
|
||
deleteNode(id, { trackHistory: true });
|
||
|
||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(id);
|
||
expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(id);
|
||
expect(historyStore.pushCommandToUndo).toHaveBeenCalledWith(
|
||
new RemoveNodeCommand(node, expect.any(Number)),
|
||
);
|
||
});
|
||
|
||
it('should delete node without tracking history', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const historyStore = mockedStore(useHistoryStore);
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
|
||
|
||
const id = 'node1';
|
||
const node = createTestNode({
|
||
id,
|
||
type: 'node',
|
||
position: [10, 20],
|
||
name: 'Node 1',
|
||
parameters: {},
|
||
});
|
||
|
||
workflowsStore.getNodeById.mockReturnValue(node);
|
||
|
||
const { deleteNode } = useCanvasOperations({ router });
|
||
deleteNode(id, { trackHistory: false });
|
||
|
||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(id);
|
||
expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(id);
|
||
expect(historyStore.pushCommandToUndo).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('should connect adjacent nodes when deleting a node surrounded by other nodes', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[SET_NODE_TYPE]: { 1: mockNodeTypeDescription({ name: SET_NODE_TYPE }) },
|
||
};
|
||
|
||
const nodes = [
|
||
createTestNode({
|
||
id: 'input',
|
||
type: SET_NODE_TYPE,
|
||
position: [10, 20],
|
||
name: 'Input Node',
|
||
}),
|
||
createTestNode({
|
||
id: 'middle',
|
||
type: SET_NODE_TYPE,
|
||
position: [10, 20],
|
||
name: 'Middle Node',
|
||
}),
|
||
createTestNode({
|
||
id: 'output',
|
||
type: SET_NODE_TYPE,
|
||
position: [10, 20],
|
||
name: 'Output Node',
|
||
}),
|
||
];
|
||
|
||
workflowsStore.workflow.nodes = nodes;
|
||
workflowsStore.workflow.connections = {
|
||
[nodes[0].name]: {
|
||
main: [
|
||
[
|
||
{
|
||
node: nodes[1].name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
},
|
||
],
|
||
],
|
||
},
|
||
[nodes[1].name]: {
|
||
main: [
|
||
[
|
||
{
|
||
node: nodes[2].name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
},
|
||
],
|
||
],
|
||
},
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
|
||
|
||
workflowsStore.getNodeById.mockReturnValue(nodes[1]);
|
||
|
||
const { deleteNode } = useCanvasOperations({ router });
|
||
deleteNode(nodes[1].id);
|
||
|
||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
|
||
expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(nodes[1].id);
|
||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
|
||
});
|
||
|
||
it('should handle nodes with null connections for unconnected indexes', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[SET_NODE_TYPE]: { 1: mockNodeTypeDescription({ name: SET_NODE_TYPE }) },
|
||
};
|
||
|
||
const nodes = [
|
||
createTestNode({
|
||
id: 'input',
|
||
type: SET_NODE_TYPE,
|
||
position: [10, 20],
|
||
name: 'Input Node',
|
||
}),
|
||
createTestNode({
|
||
id: 'middle',
|
||
type: SET_NODE_TYPE,
|
||
position: [10, 20],
|
||
name: 'Middle Node',
|
||
}),
|
||
createTestNode({
|
||
id: 'output',
|
||
type: SET_NODE_TYPE,
|
||
position: [10, 20],
|
||
name: 'Output Node',
|
||
}),
|
||
];
|
||
|
||
workflowsStore.getNodeByName = vi
|
||
.fn()
|
||
.mockImplementation((name: string) => nodes.find((node) => node.name === name));
|
||
|
||
workflowsStore.workflow.nodes = nodes;
|
||
workflowsStore.workflow.connections = {
|
||
[nodes[0].name]: {
|
||
main: [
|
||
null,
|
||
[
|
||
{
|
||
node: nodes[1].name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
},
|
||
],
|
||
],
|
||
},
|
||
[nodes[1].name]: {
|
||
main: [
|
||
// null here to simulate no connection at index
|
||
null,
|
||
[
|
||
{
|
||
node: nodes[2].name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
},
|
||
],
|
||
],
|
||
},
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
|
||
|
||
workflowsStore.getNodeById.mockReturnValue(nodes[1]);
|
||
|
||
const { deleteNode } = useCanvasOperations({ router });
|
||
deleteNode(nodes[1].id);
|
||
|
||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
|
||
expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(nodes[1].id);
|
||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
|
||
});
|
||
});
|
||
|
||
describe('revertDeleteNode', () => {
|
||
it('should revert delete node', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
|
||
const node = createTestNode({
|
||
id: 'node1',
|
||
type: 'node',
|
||
position: [10, 20],
|
||
name: 'Node 1',
|
||
parameters: {},
|
||
});
|
||
|
||
const { revertDeleteNode } = useCanvasOperations({ router });
|
||
revertDeleteNode(node);
|
||
|
||
expect(workflowsStore.addNode).toHaveBeenCalledWith(node);
|
||
});
|
||
});
|
||
|
||
describe('renameNode', () => {
|
||
it('should rename node', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const ndvStore = mockedStore(useNDVStore);
|
||
const oldName = 'Old Node';
|
||
const newName = 'New Node';
|
||
|
||
const workflowObject = createTestWorkflowObject();
|
||
workflowObject.renameNode = vi.fn();
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
|
||
ndvStore.activeNodeName = oldName;
|
||
|
||
const { renameNode } = useCanvasOperations({ router });
|
||
await renameNode(oldName, newName);
|
||
|
||
expect(workflowObject.renameNode).toHaveBeenCalledWith(oldName, newName);
|
||
expect(ndvStore.activeNodeName).toBe(newName);
|
||
});
|
||
|
||
it('should not rename node when new name is same as old name', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const ndvStore = mockedStore(useNDVStore);
|
||
const oldName = 'Old Node';
|
||
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
|
||
ndvStore.activeNodeName = oldName;
|
||
|
||
const { renameNode } = useCanvasOperations({ router });
|
||
await renameNode(oldName, oldName);
|
||
|
||
expect(ndvStore.activeNodeName).toBe(oldName);
|
||
});
|
||
});
|
||
|
||
describe('revertRenameNode', () => {
|
||
it('should revert node renaming', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const ndvStore = mockedStore(useNDVStore);
|
||
const oldName = 'Old Node';
|
||
const currentName = 'New Node';
|
||
|
||
const workflowObject = createTestWorkflowObject();
|
||
workflowObject.renameNode = vi.fn();
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: currentName });
|
||
ndvStore.activeNodeName = currentName;
|
||
|
||
const { revertRenameNode } = useCanvasOperations({ router });
|
||
await revertRenameNode(currentName, oldName);
|
||
|
||
expect(ndvStore.activeNodeName).toBe(oldName);
|
||
});
|
||
|
||
it('should not revert node renaming when old name is same as new name', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const ndvStore = mockedStore(useNDVStore);
|
||
const oldName = 'Old Node';
|
||
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
|
||
ndvStore.activeNodeName = oldName;
|
||
|
||
const { revertRenameNode } = useCanvasOperations({ router });
|
||
await revertRenameNode(oldName, oldName);
|
||
|
||
expect(ndvStore.activeNodeName).toBe(oldName);
|
||
});
|
||
});
|
||
|
||
describe('setNodeActive', () => {
|
||
it('should set active node name when node exists', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const ndvStore = mockedStore(useNDVStore);
|
||
const nodeId = 'node1';
|
||
const nodeName = 'Node 1';
|
||
workflowsStore.getNodeById = vi.fn().mockReturnValue({ name: nodeName });
|
||
ndvStore.activeNodeName = '';
|
||
|
||
const { setNodeActive } = useCanvasOperations({ router });
|
||
setNodeActive(nodeId);
|
||
|
||
expect(ndvStore.activeNodeName).toBe(nodeName);
|
||
});
|
||
|
||
it('should not change active node name when node does not exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const ndvStore = mockedStore(useNDVStore);
|
||
const nodeId = 'node1';
|
||
workflowsStore.getNodeById = vi.fn().mockReturnValue(undefined);
|
||
ndvStore.activeNodeName = 'Existing Node';
|
||
|
||
const { setNodeActive } = useCanvasOperations({ router });
|
||
setNodeActive(nodeId);
|
||
|
||
expect(ndvStore.activeNodeName).toBe('Existing Node');
|
||
});
|
||
|
||
it('should set node as dirty when node is set active', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const node = createTestNode();
|
||
|
||
workflowsStore.getNodeById.mockImplementation(() => node);
|
||
|
||
const { setNodeActive } = useCanvasOperations({ router });
|
||
setNodeActive(node.id);
|
||
|
||
expect(workflowsStore.setNodePristine).toHaveBeenCalledWith(node.name, false);
|
||
});
|
||
});
|
||
|
||
describe('setNodeActiveByName', () => {
|
||
it('should set active node name', () => {
|
||
const ndvStore = useNDVStore();
|
||
const nodeName = 'Node 1';
|
||
ndvStore.activeNodeName = '';
|
||
|
||
const { setNodeActiveByName } = useCanvasOperations({ router });
|
||
setNodeActiveByName(nodeName);
|
||
|
||
expect(ndvStore.activeNodeName).toBe(nodeName);
|
||
});
|
||
});
|
||
|
||
describe('toggleNodesDisabled', () => {
|
||
it('disables nodes based on provided ids', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodes = [
|
||
createTestNode({ id: '1', name: 'A' }),
|
||
createTestNode({ id: '2', name: 'B' }),
|
||
];
|
||
workflowsStore.getNodesByIds.mockReturnValue(nodes);
|
||
|
||
const { toggleNodesDisabled } = useCanvasOperations({ router });
|
||
toggleNodesDisabled([nodes[0].id, nodes[1].id], {
|
||
trackHistory: true,
|
||
trackBulk: true,
|
||
});
|
||
|
||
expect(workflowsStore.updateNodeProperties).toHaveBeenCalledWith({
|
||
name: nodes[0].name,
|
||
properties: {
|
||
disabled: true,
|
||
},
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('revertToggleNodeDisabled', () => {
|
||
it('re-enables a previously disabled node', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeName = 'testNode';
|
||
const node = createTestNode({ name: nodeName });
|
||
workflowsStore.getNodeByName.mockReturnValue(node);
|
||
const updateNodePropertiesSpy = vi.spyOn(workflowsStore, 'updateNodeProperties');
|
||
|
||
const { revertToggleNodeDisabled } = useCanvasOperations({ router });
|
||
revertToggleNodeDisabled(nodeName);
|
||
|
||
expect(updateNodePropertiesSpy).toHaveBeenCalledWith({
|
||
name: nodeName,
|
||
properties: {
|
||
disabled: true,
|
||
},
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('addConnections', () => {
|
||
it('should create connections between nodes', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const nodeTypeName = SET_NODE_TYPE;
|
||
const nodeType = mockNodeTypeDescription({
|
||
name: nodeTypeName,
|
||
inputs: [NodeConnectionTypes.Main],
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
const nodes = [
|
||
mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [40, 40] }),
|
||
mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [40, 40] }),
|
||
mockNode({ id: 'c', name: 'Node C', type: nodeTypeName, position: [40, 40] }),
|
||
];
|
||
const connections = [
|
||
{
|
||
source: nodes[0].id,
|
||
sourceHandle: createCanvasConnectionHandleString({
|
||
mode: CanvasConnectionMode.Output,
|
||
index: 0,
|
||
type: NodeConnectionTypes.Main,
|
||
}),
|
||
target: nodes[1].id,
|
||
targetHandle: createCanvasConnectionHandleString({
|
||
mode: CanvasConnectionMode.Input,
|
||
index: 0,
|
||
type: NodeConnectionTypes.Main,
|
||
}),
|
||
data: {
|
||
source: { type: NodeConnectionTypes.Main, index: 0 },
|
||
target: { type: NodeConnectionTypes.Main, index: 0 },
|
||
},
|
||
},
|
||
{
|
||
source: nodes[1].id,
|
||
sourceHandle: createCanvasConnectionHandleString({
|
||
mode: CanvasConnectionMode.Output,
|
||
index: 0,
|
||
type: NodeConnectionTypes.Main,
|
||
}),
|
||
target: nodes[2].id,
|
||
targetHandle: createCanvasConnectionHandleString({
|
||
mode: CanvasConnectionMode.Input,
|
||
index: 0,
|
||
type: NodeConnectionTypes.Main,
|
||
}),
|
||
data: {
|
||
source: { type: NodeConnectionTypes.Main, index: 0 },
|
||
target: { type: NodeConnectionTypes.Main, index: 0 },
|
||
},
|
||
},
|
||
];
|
||
|
||
workflowsStore.workflow.nodes = nodes;
|
||
nodeTypesStore.nodeTypes = {
|
||
[nodeTypeName]: { 1: nodeType },
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.getNodeById.mockReturnValueOnce(nodes[0]).mockReturnValueOnce(nodes[1]);
|
||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeType);
|
||
|
||
const { addConnections } = useCanvasOperations({ router });
|
||
await addConnections(connections);
|
||
|
||
expect(workflowsStore.addConnection).toHaveBeenCalledWith({
|
||
connection: [
|
||
{
|
||
index: 0,
|
||
node: 'Node A',
|
||
type: NodeConnectionTypes.Main,
|
||
},
|
||
{
|
||
index: 0,
|
||
node: 'Node B',
|
||
type: NodeConnectionTypes.Main,
|
||
},
|
||
],
|
||
});
|
||
});
|
||
|
||
it('should set UI state as dirty', async () => {
|
||
const uiStore = mockedStore(useUIStore);
|
||
const connections: CanvasConnection[] = [];
|
||
|
||
const { addConnections } = useCanvasOperations({ router });
|
||
await addConnections(connections, { keepPristine: false });
|
||
|
||
expect(uiStore.stateIsDirty).toBe(true);
|
||
});
|
||
|
||
it('should not set UI state as dirty if keepPristine is true', async () => {
|
||
const uiStore = mockedStore(useUIStore);
|
||
const connections: CanvasConnection[] = [];
|
||
|
||
const { addConnections } = useCanvasOperations({ router });
|
||
await addConnections(connections, { keepPristine: true });
|
||
|
||
expect(uiStore.stateIsDirty).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('createConnection', () => {
|
||
it('should not create a connection if source node does not exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const uiStore = mockedStore(useUIStore);
|
||
const connection: Connection = { source: 'nonexistent', target: 'targetNode' };
|
||
|
||
workflowsStore.getNodeById.mockReturnValueOnce(undefined);
|
||
|
||
const { createConnection } = useCanvasOperations({ router });
|
||
createConnection(connection);
|
||
|
||
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
|
||
expect(uiStore.stateIsDirty).toBe(false);
|
||
});
|
||
|
||
it('should not create a connection if target node does not exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const uiStore = mockedStore(useUIStore);
|
||
const connection: Connection = { source: 'sourceNode', target: 'nonexistent' };
|
||
|
||
workflowsStore.getNodeById
|
||
.mockReturnValueOnce(createTestNode())
|
||
.mockReturnValueOnce(undefined);
|
||
|
||
const { createConnection } = useCanvasOperations({ router });
|
||
createConnection(connection);
|
||
|
||
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
|
||
expect(uiStore.stateIsDirty).toBe(false);
|
||
});
|
||
|
||
it('should create a connection if source and target nodes exist and connection is allowed', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const uiStore = mockedStore(useUIStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
const nodeTypeDescription = mockNodeTypeDescription({
|
||
name: SET_NODE_TYPE,
|
||
inputs: [NodeConnectionTypes.Main],
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
|
||
const nodeA = createTestNode({
|
||
id: 'a',
|
||
type: nodeTypeDescription.name,
|
||
name: 'Node A',
|
||
});
|
||
|
||
const nodeB = createTestNode({
|
||
id: 'b',
|
||
type: nodeTypeDescription.name,
|
||
name: 'Node B',
|
||
});
|
||
|
||
const connection: Connection = {
|
||
source: nodeA.id,
|
||
sourceHandle: `outputs/${NodeConnectionTypes.Main}/0`,
|
||
target: nodeB.id,
|
||
targetHandle: `inputs/${NodeConnectionTypes.Main}/0`,
|
||
};
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
node: { 1: nodeTypeDescription },
|
||
};
|
||
|
||
workflowsStore.workflow.nodes = [nodeA, nodeB];
|
||
workflowsStore.getNodeById.mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB);
|
||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { createConnection, editableWorkflowObject } = useCanvasOperations({ router });
|
||
|
||
editableWorkflowObject.value.nodes[nodeA.name] = nodeA;
|
||
editableWorkflowObject.value.nodes[nodeB.name] = nodeB;
|
||
|
||
createConnection(connection);
|
||
|
||
expect(workflowsStore.addConnection).toHaveBeenCalledWith({
|
||
connection: [
|
||
{ index: 0, node: nodeA.name, type: NodeConnectionTypes.Main },
|
||
{ index: 0, node: nodeB.name, type: NodeConnectionTypes.Main },
|
||
],
|
||
});
|
||
expect(uiStore.stateIsDirty).toBe(true);
|
||
});
|
||
|
||
it('should not set UI state as dirty if keepPristine is true', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const uiStore = mockedStore(useUIStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
const nodeTypeDescription = mockNodeTypeDescription({
|
||
name: SET_NODE_TYPE,
|
||
inputs: [NodeConnectionTypes.Main],
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
|
||
const nodeA = createTestNode({
|
||
id: 'a',
|
||
type: nodeTypeDescription.name,
|
||
name: 'Node A',
|
||
});
|
||
|
||
const nodeB = createTestNode({
|
||
id: 'b',
|
||
type: nodeTypeDescription.name,
|
||
name: 'Node B',
|
||
});
|
||
|
||
const connection: Connection = {
|
||
source: nodeA.id,
|
||
sourceHandle: `outputs/${NodeConnectionTypes.Main}/0`,
|
||
target: nodeB.id,
|
||
targetHandle: `inputs/${NodeConnectionTypes.Main}/0`,
|
||
};
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
node: { 1: nodeTypeDescription },
|
||
};
|
||
|
||
workflowsStore.workflow.nodes = [nodeA, nodeB];
|
||
workflowsStore.getNodeById.mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB);
|
||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { createConnection, editableWorkflowObject } = useCanvasOperations({ router });
|
||
|
||
editableWorkflowObject.value.nodes[nodeA.name] = nodeA;
|
||
editableWorkflowObject.value.nodes[nodeB.name] = nodeB;
|
||
|
||
createConnection(connection, { keepPristine: true });
|
||
|
||
expect(uiStore.stateIsDirty).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('revertCreateConnection', () => {
|
||
it('deletes connection if both source and target nodes exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const connection: [IConnection, IConnection] = [
|
||
{ node: 'sourceNode', type: NodeConnectionTypes.Main, index: 0 },
|
||
{ node: 'targetNode', type: NodeConnectionTypes.Main, index: 0 },
|
||
];
|
||
const testNode = createTestNode();
|
||
|
||
workflowsStore.getNodeByName.mockReturnValue(testNode);
|
||
workflowsStore.getNodeById.mockReturnValue(testNode);
|
||
|
||
const { revertCreateConnection } = useCanvasOperations({ router });
|
||
revertCreateConnection(connection);
|
||
|
||
expect(workflowsStore.removeConnection).toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('isConnectionAllowed', () => {
|
||
it('should return false if target node type does not have inputs', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const sourceNode = mockNode({
|
||
id: '1',
|
||
type: 'sourceType',
|
||
name: 'Source Node',
|
||
});
|
||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||
name: sourceNode.type,
|
||
outputs: [],
|
||
});
|
||
const sourceHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
const targetNode = mockNode({
|
||
id: '2',
|
||
type: 'targetType',
|
||
name: 'Target Node',
|
||
});
|
||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||
name: targetNode.type,
|
||
inputs: [],
|
||
});
|
||
const targetHandle: IConnection = {
|
||
node: targetNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
nodeTypesStore.getNodeType = vi.fn(
|
||
(nodeTypeName: string) =>
|
||
({
|
||
[sourceNode.type]: sourceNodeTypeDescription,
|
||
[targetNode.type]: targetNodeTypeDescription,
|
||
})[nodeTypeName],
|
||
);
|
||
|
||
const { isConnectionAllowed } = useCanvasOperations({ router });
|
||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||
});
|
||
|
||
it('should return false if target node does not exist in the workflow', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
const sourceNode = mockNode({
|
||
id: '1',
|
||
type: 'sourceType',
|
||
name: 'Source Node',
|
||
});
|
||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||
name: sourceNode.type,
|
||
outputs: [],
|
||
});
|
||
const sourceHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
const targetNode = mockNode({
|
||
id: '2',
|
||
type: 'targetType',
|
||
name: 'Target Node',
|
||
});
|
||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||
name: targetNode.type,
|
||
inputs: [NodeConnectionTypes.Main],
|
||
});
|
||
const targetHandle: IConnection = {
|
||
node: targetNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
nodeTypesStore.getNodeType = vi.fn(
|
||
(nodeTypeName: string) =>
|
||
({
|
||
[sourceNode.type]: sourceNodeTypeDescription,
|
||
[targetNode.type]: targetNodeTypeDescription,
|
||
})[nodeTypeName],
|
||
);
|
||
|
||
const { isConnectionAllowed } = useCanvasOperations({ router });
|
||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||
});
|
||
|
||
it('should return false if source node does not have connection type', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const sourceNode = mockNode({
|
||
id: '1',
|
||
type: 'sourceType',
|
||
name: 'Source Node',
|
||
});
|
||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||
name: sourceNode.type,
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
const sourceHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.AiTool,
|
||
index: 0,
|
||
};
|
||
|
||
const targetNode = mockNode({
|
||
id: '2',
|
||
type: 'targetType',
|
||
name: 'Target Node',
|
||
});
|
||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||
name: 'targetType',
|
||
inputs: [NodeConnectionTypes.AiTool],
|
||
});
|
||
const targetHandle: IConnection = {
|
||
node: targetNode.name,
|
||
type: NodeConnectionTypes.AiTool,
|
||
index: 0,
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||
|
||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||
nodeTypesStore.getNodeType = vi.fn(
|
||
(nodeTypeName: string) =>
|
||
({
|
||
[sourceNode.type]: sourceNodeTypeDescription,
|
||
[targetNode.type]: targetNodeTypeDescription,
|
||
})[nodeTypeName],
|
||
);
|
||
|
||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||
});
|
||
|
||
it('should return false if target node does not have connection type', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const sourceNode = mockNode({
|
||
id: '1',
|
||
type: 'sourceType',
|
||
name: 'Source Node',
|
||
});
|
||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||
name: sourceNode.type,
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
const sourceHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const targetNode = mockNode({
|
||
id: '2',
|
||
type: 'targetType',
|
||
name: 'Target Node',
|
||
});
|
||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||
name: 'targetType',
|
||
inputs: [NodeConnectionTypes.AiTool],
|
||
});
|
||
const targetHandle: IConnection = {
|
||
node: targetNode.name,
|
||
type: NodeConnectionTypes.AiTool,
|
||
index: 0,
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||
|
||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||
nodeTypesStore.getNodeType = vi.fn(
|
||
(nodeTypeName: string) =>
|
||
({
|
||
[sourceNode.type]: sourceNodeTypeDescription,
|
||
[targetNode.type]: targetNodeTypeDescription,
|
||
})[nodeTypeName],
|
||
);
|
||
|
||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||
});
|
||
|
||
it('should return false if source node type is not allowed by target node input filter', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const sourceNode = mockNode({
|
||
id: '1',
|
||
type: 'sourceType',
|
||
name: 'Source Node',
|
||
typeVersion: 1,
|
||
});
|
||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||
name: sourceNode.type,
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
const sourceHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const targetNode = mockNode({
|
||
id: '2',
|
||
type: 'targetType',
|
||
name: 'Target Node',
|
||
typeVersion: 1,
|
||
});
|
||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||
name: 'targetType',
|
||
inputs: [
|
||
{
|
||
type: NodeConnectionTypes.Main,
|
||
filter: {
|
||
nodes: ['allowedType'],
|
||
},
|
||
},
|
||
],
|
||
});
|
||
const targetHandle: IConnection = {
|
||
node: targetNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||
|
||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||
nodeTypesStore.getNodeType = vi.fn(
|
||
(nodeTypeName: string) =>
|
||
({
|
||
[sourceNode.type]: sourceNodeTypeDescription,
|
||
[targetNode.type]: targetNodeTypeDescription,
|
||
})[nodeTypeName],
|
||
);
|
||
|
||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||
});
|
||
|
||
it('should return false if source node type does not have connection type index', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const sourceNode = mockNode({
|
||
id: '1',
|
||
type: 'sourceType',
|
||
name: 'Source Node',
|
||
typeVersion: 1,
|
||
});
|
||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||
name: sourceNode.type,
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
const sourceHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 1,
|
||
};
|
||
|
||
const targetNode = mockNode({
|
||
id: '2',
|
||
type: 'targetType',
|
||
name: 'Target Node',
|
||
typeVersion: 1,
|
||
});
|
||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||
name: targetNode.type,
|
||
inputs: [
|
||
{
|
||
type: NodeConnectionTypes.Main,
|
||
filter: {
|
||
nodes: [sourceNode.type],
|
||
},
|
||
},
|
||
],
|
||
});
|
||
const targetHandle: IConnection = {
|
||
node: targetNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||
|
||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||
nodeTypesStore.getNodeType = vi.fn(
|
||
(nodeTypeName: string) =>
|
||
({
|
||
[sourceNode.type]: sourceNodeTypeDescription,
|
||
[targetNode.type]: targetNodeTypeDescription,
|
||
})[nodeTypeName],
|
||
);
|
||
|
||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||
});
|
||
|
||
it('should return false if target node type does not have connection type index', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const sourceNode = mockNode({
|
||
id: '1',
|
||
type: 'sourceType',
|
||
name: 'Source Node',
|
||
typeVersion: 1,
|
||
});
|
||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||
name: sourceNode.type,
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
const sourceHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const targetNode = mockNode({
|
||
id: '2',
|
||
type: 'targetType',
|
||
name: 'Target Node',
|
||
typeVersion: 1,
|
||
});
|
||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||
name: targetNode.type,
|
||
inputs: [
|
||
{
|
||
type: NodeConnectionTypes.Main,
|
||
filter: {
|
||
nodes: [sourceNode.type],
|
||
},
|
||
},
|
||
],
|
||
});
|
||
const targetHandle: IConnection = {
|
||
node: targetNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 1,
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||
|
||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||
nodeTypesStore.getNodeType = vi.fn(
|
||
(nodeTypeName: string) =>
|
||
({
|
||
[sourceNode.type]: sourceNodeTypeDescription,
|
||
[targetNode.type]: targetNodeTypeDescription,
|
||
})[nodeTypeName],
|
||
);
|
||
|
||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||
});
|
||
|
||
it('should return true if all conditions including filter are met', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
const sourceNode = mockNode({
|
||
id: '1',
|
||
type: 'sourceType',
|
||
name: 'Source Node',
|
||
typeVersion: 1,
|
||
});
|
||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||
name: sourceNode.type,
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
const sourceHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const targetNode = mockNode({
|
||
id: '2',
|
||
type: 'targetType',
|
||
name: 'Target Node',
|
||
typeVersion: 1,
|
||
});
|
||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||
name: targetNode.type,
|
||
inputs: [
|
||
{
|
||
type: NodeConnectionTypes.Main,
|
||
filter: {
|
||
nodes: [sourceNode.type],
|
||
},
|
||
},
|
||
],
|
||
});
|
||
const targetHandle: IConnection = {
|
||
node: targetNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||
|
||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||
nodeTypesStore.getNodeType = vi.fn(
|
||
(nodeTypeName: string) =>
|
||
({
|
||
[sourceNode.type]: sourceNodeTypeDescription,
|
||
[targetNode.type]: targetNodeTypeDescription,
|
||
})[nodeTypeName],
|
||
);
|
||
|
||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(true);
|
||
});
|
||
|
||
it('should return true if all conditions are met and no filter is set', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
const sourceNode = mockNode({
|
||
id: '1',
|
||
type: 'sourceType',
|
||
name: 'Source Node',
|
||
typeVersion: 1,
|
||
});
|
||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||
name: sourceNode.type,
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
const sourceHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const targetNode = mockNode({
|
||
id: '2',
|
||
type: 'targetType',
|
||
name: 'Target Node',
|
||
typeVersion: 1,
|
||
});
|
||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||
name: targetNode.type,
|
||
inputs: [
|
||
{
|
||
type: NodeConnectionTypes.Main,
|
||
},
|
||
],
|
||
});
|
||
const targetHandle: IConnection = {
|
||
node: targetNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||
|
||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||
nodeTypesStore.getNodeType = vi.fn(
|
||
(nodeTypeName: string) =>
|
||
({
|
||
[sourceNode.type]: sourceNodeTypeDescription,
|
||
[targetNode.type]: targetNodeTypeDescription,
|
||
})[nodeTypeName],
|
||
);
|
||
|
||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(true);
|
||
});
|
||
|
||
it('should return true if node connecting to itself', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
const sourceNode = mockNode({
|
||
id: '1',
|
||
type: 'sourceType',
|
||
name: 'Source Node',
|
||
typeVersion: 1,
|
||
});
|
||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||
name: sourceNode.type,
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
const sourceHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
const targetHandle: IConnection = {
|
||
node: sourceNode.name,
|
||
type: NodeConnectionTypes.Main,
|
||
index: 0,
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||
|
||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||
nodeTypesStore.getNodeType = vi.fn(
|
||
(nodeTypeName: string) =>
|
||
({
|
||
[sourceNode.type]: sourceNodeTypeDescription,
|
||
})[nodeTypeName],
|
||
);
|
||
|
||
expect(isConnectionAllowed(sourceNode, sourceNode, sourceHandle, targetHandle)).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('deleteConnection', () => {
|
||
it('should not delete a connection if source node does not exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const connection: Connection = { source: 'nonexistent', target: 'targetNode' };
|
||
|
||
workflowsStore.getNodeById
|
||
.mockReturnValueOnce(undefined)
|
||
.mockReturnValueOnce(createTestNode());
|
||
|
||
const { deleteConnection } = useCanvasOperations({ router });
|
||
deleteConnection(connection);
|
||
|
||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('should not delete a connection if target node does not exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const connection: Connection = { source: 'sourceNode', target: 'nonexistent' };
|
||
|
||
workflowsStore.getNodeById
|
||
.mockReturnValueOnce(createTestNode())
|
||
.mockReturnValueOnce(undefined);
|
||
|
||
const { deleteConnection } = useCanvasOperations({ router });
|
||
deleteConnection(connection);
|
||
|
||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('should delete a connection if source and target nodes exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
|
||
const nodeA = createTestNode({
|
||
id: 'a',
|
||
type: 'node',
|
||
name: 'Node A',
|
||
});
|
||
|
||
const nodeB = createTestNode({
|
||
id: 'b',
|
||
type: 'node',
|
||
name: 'Node B',
|
||
});
|
||
|
||
const connection: Connection = {
|
||
source: nodeA.id,
|
||
sourceHandle: `outputs/${NodeConnectionTypes.Main}/0`,
|
||
target: nodeB.id,
|
||
targetHandle: `inputs/${NodeConnectionTypes.Main}/0`,
|
||
};
|
||
|
||
workflowsStore.getNodeById.mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB);
|
||
|
||
const { deleteConnection } = useCanvasOperations({ router });
|
||
deleteConnection(connection);
|
||
|
||
expect(workflowsStore.removeConnection).toHaveBeenCalledWith({
|
||
connection: [
|
||
{ index: 0, node: nodeA.name, type: NodeConnectionTypes.Main },
|
||
{ index: 0, node: nodeB.name, type: NodeConnectionTypes.Main },
|
||
],
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('revertDeleteConnection', () => {
|
||
it('should revert delete connection', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
|
||
const connection: [IConnection, IConnection] = [
|
||
{ node: 'sourceNode', type: NodeConnectionTypes.Main, index: 1 },
|
||
{ node: 'targetNode', type: NodeConnectionTypes.Main, index: 2 },
|
||
];
|
||
|
||
const { revertDeleteConnection } = useCanvasOperations({ router });
|
||
revertDeleteConnection(connection);
|
||
|
||
expect(workflowsStore.addConnection).toHaveBeenCalledWith({ connection });
|
||
});
|
||
});
|
||
|
||
describe('revalidateNodeInputConnections', () => {
|
||
it('should not delete connections when target node does not exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nonexistentId = 'nonexistent';
|
||
workflowsStore.getNodeById.mockReturnValue(undefined);
|
||
|
||
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||
revalidateNodeInputConnections(nonexistentId);
|
||
|
||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('should not delete connections when node type description is not found', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const nodeId = 'test-node';
|
||
const node = createTestNode({ id: nodeId, type: 'unknown-type' });
|
||
|
||
workflowsStore.getNodeById.mockReturnValue(node);
|
||
nodeTypesStore.getNodeType = () => null;
|
||
|
||
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||
revalidateNodeInputConnections(nodeId);
|
||
|
||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('should remove invalid connections that do not match input type', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
workflowsStore.removeConnection = vi.fn();
|
||
|
||
const targetNodeId = 'target';
|
||
const targetNode = createTestNode({
|
||
id: targetNodeId,
|
||
name: 'Target Node',
|
||
type: SET_NODE_TYPE,
|
||
});
|
||
const targetNodeType = mockNodeTypeDescription({
|
||
name: SET_NODE_TYPE,
|
||
inputs: [NodeConnectionTypes.Main],
|
||
});
|
||
|
||
const sourceNodeId = 'source';
|
||
const sourceNode = createTestNode({
|
||
id: sourceNodeId,
|
||
name: 'Source Node',
|
||
type: AGENT_NODE_TYPE,
|
||
});
|
||
const sourceNodeType = mockNodeTypeDescription({
|
||
name: AGENT_NODE_TYPE,
|
||
outputs: [NodeConnectionTypes.AiTool],
|
||
});
|
||
|
||
workflowsStore.workflow.nodes = [sourceNode, targetNode];
|
||
workflowsStore.workflow.connections = {
|
||
[sourceNode.name]: {
|
||
[NodeConnectionTypes.AiTool]: [
|
||
[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||
],
|
||
},
|
||
};
|
||
|
||
workflowsStore.getNodeById
|
||
.mockReturnValueOnce(sourceNode)
|
||
.mockReturnValueOnce(targetNode)
|
||
.mockReturnValueOnce(sourceNode)
|
||
.mockReturnValueOnce(targetNode);
|
||
|
||
nodeTypesStore.getNodeType = vi
|
||
.fn()
|
||
.mockReturnValueOnce(targetNodeType)
|
||
.mockReturnValueOnce(sourceNodeType);
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||
revalidateNodeInputConnections(targetNodeId);
|
||
|
||
await nextTick();
|
||
|
||
expect(workflowsStore.removeConnection).toHaveBeenCalledWith({
|
||
connection: [
|
||
{ node: sourceNode.name, type: NodeConnectionTypes.AiTool, index: 0 },
|
||
{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 },
|
||
],
|
||
});
|
||
});
|
||
|
||
it('should keep valid connections that match input type', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
workflowsStore.removeConnection = vi.fn();
|
||
|
||
const targetNodeId = 'target';
|
||
const targetNode = createTestNode({
|
||
id: targetNodeId,
|
||
name: 'Target Node',
|
||
type: SET_NODE_TYPE,
|
||
});
|
||
const targetNodeType = mockNodeTypeDescription({
|
||
name: SET_NODE_TYPE,
|
||
inputs: [NodeConnectionTypes.Main],
|
||
});
|
||
|
||
const sourceNodeId = 'source';
|
||
const sourceNode = createTestNode({
|
||
id: sourceNodeId,
|
||
name: 'Source Node',
|
||
type: AGENT_NODE_TYPE,
|
||
});
|
||
const sourceNodeType = mockNodeTypeDescription({
|
||
name: AGENT_NODE_TYPE,
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
|
||
workflowsStore.workflow.nodes = [sourceNode, targetNode];
|
||
workflowsStore.workflow.connections = {
|
||
[sourceNode.name]: {
|
||
[NodeConnectionTypes.Main]: [
|
||
[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||
],
|
||
},
|
||
};
|
||
|
||
workflowsStore.getNodeById
|
||
.mockReturnValueOnce(sourceNode)
|
||
.mockReturnValueOnce(targetNode)
|
||
.mockReturnValueOnce(sourceNode)
|
||
.mockReturnValueOnce(targetNode);
|
||
|
||
nodeTypesStore.getNodeType = vi
|
||
.fn()
|
||
.mockReturnValueOnce(targetNodeType)
|
||
.mockReturnValueOnce(sourceNodeType);
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||
revalidateNodeInputConnections(targetNodeId);
|
||
|
||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('revalidateNodeOutputConnections', () => {
|
||
it('should not delete connections when source node does not exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nonexistentId = 'nonexistent';
|
||
workflowsStore.getNodeById.mockReturnValue(undefined);
|
||
|
||
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||
revalidateNodeOutputConnections(nonexistentId);
|
||
|
||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('should not delete connections when node type description is not found', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const nodeId = 'test-node';
|
||
const node = createTestNode({ id: nodeId, type: 'unknown-type' });
|
||
|
||
workflowsStore.getNodeById.mockReturnValue(node);
|
||
nodeTypesStore.getNodeType = () => null;
|
||
|
||
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||
revalidateNodeOutputConnections(nodeId);
|
||
|
||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('should remove invalid connections that do not match output type', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
workflowsStore.removeConnection = vi.fn();
|
||
|
||
const targetNodeId = 'target';
|
||
const targetNode = createTestNode({
|
||
id: targetNodeId,
|
||
name: 'Target Node',
|
||
type: SET_NODE_TYPE,
|
||
});
|
||
const targetNodeType = mockNodeTypeDescription({
|
||
name: SET_NODE_TYPE,
|
||
inputs: [NodeConnectionTypes.Main],
|
||
});
|
||
|
||
const sourceNodeId = 'source';
|
||
const sourceNode = createTestNode({
|
||
id: sourceNodeId,
|
||
name: 'Source Node',
|
||
type: AGENT_NODE_TYPE,
|
||
});
|
||
const sourceNodeType = mockNodeTypeDescription({
|
||
name: AGENT_NODE_TYPE,
|
||
outputs: [NodeConnectionTypes.AiTool],
|
||
});
|
||
|
||
workflowsStore.workflow.nodes = [sourceNode, targetNode];
|
||
workflowsStore.workflow.connections = {
|
||
[sourceNode.name]: {
|
||
[NodeConnectionTypes.AiTool]: [
|
||
[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||
],
|
||
},
|
||
};
|
||
|
||
workflowsStore.getNodeById
|
||
.mockReturnValueOnce(sourceNode)
|
||
.mockReturnValueOnce(targetNode)
|
||
.mockReturnValueOnce(sourceNode)
|
||
.mockReturnValueOnce(targetNode);
|
||
|
||
nodeTypesStore.getNodeType = vi
|
||
.fn()
|
||
.mockReturnValueOnce(targetNodeType)
|
||
.mockReturnValueOnce(sourceNodeType);
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||
revalidateNodeOutputConnections(sourceNodeId);
|
||
|
||
await nextTick();
|
||
|
||
expect(workflowsStore.removeConnection).toHaveBeenCalledWith({
|
||
connection: [
|
||
{ node: sourceNode.name, type: NodeConnectionTypes.AiTool, index: 0 },
|
||
{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 },
|
||
],
|
||
});
|
||
});
|
||
|
||
it('should keep valid connections that match output type', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
|
||
workflowsStore.removeConnection = vi.fn();
|
||
|
||
const targetNodeId = 'target';
|
||
const targetNode = createTestNode({
|
||
id: targetNodeId,
|
||
name: 'Target Node',
|
||
type: SET_NODE_TYPE,
|
||
});
|
||
const targetNodeType = mockNodeTypeDescription({
|
||
name: SET_NODE_TYPE,
|
||
inputs: [NodeConnectionTypes.Main],
|
||
});
|
||
|
||
const sourceNodeId = 'source';
|
||
const sourceNode = createTestNode({
|
||
id: sourceNodeId,
|
||
name: 'Source Node',
|
||
type: AGENT_NODE_TYPE,
|
||
});
|
||
const sourceNodeType = mockNodeTypeDescription({
|
||
name: AGENT_NODE_TYPE,
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
|
||
workflowsStore.workflow.nodes = [sourceNode, targetNode];
|
||
workflowsStore.workflow.connections = {
|
||
[sourceNode.name]: {
|
||
[NodeConnectionTypes.AiTool]: [
|
||
[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||
],
|
||
},
|
||
};
|
||
|
||
workflowsStore.getNodeById
|
||
.mockReturnValueOnce(sourceNode)
|
||
.mockReturnValueOnce(targetNode)
|
||
.mockReturnValueOnce(sourceNode)
|
||
.mockReturnValueOnce(targetNode);
|
||
|
||
nodeTypesStore.getNodeType = vi
|
||
.fn()
|
||
.mockReturnValueOnce(targetNodeType)
|
||
.mockReturnValueOnce(sourceNodeType);
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||
revalidateNodeOutputConnections(sourceNodeId);
|
||
|
||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('deleteConnectionsByNodeId', () => {
|
||
it('should delete all connections for a given node ID', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const { deleteConnectionsByNodeId } = useCanvasOperations({ router });
|
||
|
||
const node1 = createTestNode({ id: 'node1', name: 'Node 1' });
|
||
const node2 = createTestNode({ id: 'node2', name: 'Node 1' });
|
||
|
||
workflowsStore.workflow.connections = {
|
||
[node1.name]: {
|
||
[NodeConnectionTypes.Main]: [
|
||
[{ node: node2.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||
],
|
||
},
|
||
node2: {
|
||
[NodeConnectionTypes.Main]: [
|
||
[{ node: node1.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||
],
|
||
},
|
||
};
|
||
|
||
workflowsStore.getNodeById.mockReturnValue(node1);
|
||
workflowsStore.getNodeByName.mockReturnValueOnce(node1).mockReturnValueOnce(node2);
|
||
|
||
deleteConnectionsByNodeId(node1.id);
|
||
|
||
expect(workflowsStore.removeConnection).toHaveBeenCalledWith({
|
||
connection: [
|
||
{ node: node1.name, type: NodeConnectionTypes.Main, index: 0 },
|
||
{ node: node2.name, type: NodeConnectionTypes.Main, index: 0 },
|
||
],
|
||
});
|
||
|
||
expect(workflowsStore.removeConnection).toHaveBeenCalledWith({
|
||
connection: [
|
||
{ node: node2.name, type: NodeConnectionTypes.Main, index: 0 },
|
||
{ node: node1.name, type: NodeConnectionTypes.Main, index: 0 },
|
||
],
|
||
});
|
||
|
||
expect(workflowsStore.workflow.connections[node1.name]).toBeUndefined();
|
||
});
|
||
|
||
it('should not delete connections if node ID does not exist', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const { deleteConnectionsByNodeId } = useCanvasOperations({ router });
|
||
|
||
const nodeId = 'nonexistent';
|
||
workflowsStore.getNodeById.mockReturnValue(undefined);
|
||
|
||
deleteConnectionsByNodeId(nodeId);
|
||
|
||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('duplicateNodes', () => {
|
||
it('should duplicate nodes', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[SET_NODE_TYPE]: { 1: nodeTypeDescription },
|
||
};
|
||
|
||
const nodes = buildImportNodes();
|
||
workflowsStore.workflow.nodes = nodes;
|
||
workflowsStore.getNodesByIds.mockReturnValue(nodes);
|
||
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.getWorkflow.mockReturnValue(workflowObject);
|
||
|
||
const canvasOperations = useCanvasOperations({ router });
|
||
const duplicatedNodeIds = await canvasOperations.duplicateNodes(['1', '2']);
|
||
|
||
expect(duplicatedNodeIds.length).toBe(2);
|
||
expect(duplicatedNodeIds).not.toContain('1');
|
||
expect(duplicatedNodeIds).not.toContain('2');
|
||
});
|
||
});
|
||
|
||
describe('copyNodes', () => {
|
||
it('should copy nodes', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[SET_NODE_TYPE]: { 1: nodeTypeDescription },
|
||
};
|
||
|
||
const nodes = buildImportNodes();
|
||
workflowsStore.workflow.nodes = nodes;
|
||
workflowsStore.getNodesByIds.mockReturnValue(nodes);
|
||
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
|
||
|
||
const { copyNodes } = useCanvasOperations({ router });
|
||
await copyNodes(['1', '2']);
|
||
|
||
expect(useClipboard().copy).toHaveBeenCalledTimes(1);
|
||
expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot();
|
||
});
|
||
});
|
||
|
||
describe('cutNodes', () => {
|
||
it('should copy and delete nodes', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });
|
||
|
||
nodeTypesStore.nodeTypes = {
|
||
[SET_NODE_TYPE]: { 1: nodeTypeDescription },
|
||
};
|
||
|
||
const nodes = buildImportNodes();
|
||
workflowsStore.workflow.nodes = nodes;
|
||
workflowsStore.getNodesByIds.mockReturnValue(nodes);
|
||
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
|
||
|
||
const { cutNodes } = useCanvasOperations({ router });
|
||
await cutNodes(['1', '2']);
|
||
expect(useClipboard().copy).toHaveBeenCalledTimes(1);
|
||
expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot();
|
||
});
|
||
});
|
||
|
||
describe('resolveNodeWebhook', () => {
|
||
const nodeTypeDescription = mock<INodeTypeDescription>({
|
||
webhooks: [mock<IWebhookDescription>()],
|
||
});
|
||
|
||
it("should set webhookId if it doesn't already exist", () => {
|
||
const node = mock<INodeUi>({ webhookId: undefined });
|
||
|
||
const { resolveNodeWebhook } = useCanvasOperations({ router });
|
||
resolveNodeWebhook(node, nodeTypeDescription);
|
||
|
||
expect(node.webhookId).toBeDefined();
|
||
});
|
||
|
||
it('should not set webhookId if it already exists', () => {
|
||
const node = mock<INodeUi>({ webhookId: 'random-id' });
|
||
|
||
const { resolveNodeWebhook } = useCanvasOperations({ router });
|
||
resolveNodeWebhook(node, nodeTypeDescription);
|
||
|
||
expect(node.webhookId).toBe('random-id');
|
||
});
|
||
|
||
it("should not set webhookId if node description doesn't define any webhooks", () => {
|
||
const node = mock<INodeUi>({ webhookId: undefined });
|
||
|
||
const { resolveNodeWebhook } = useCanvasOperations({ router });
|
||
resolveNodeWebhook(node, mock<INodeTypeDescription>({ webhooks: [] }));
|
||
|
||
expect(node.webhookId).toBeUndefined();
|
||
});
|
||
|
||
test.each([WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE])(
|
||
'should update the webhook path, if the node type is %s, and the path parameter is empty',
|
||
(nodeType) => {
|
||
const node = mock<INodeUi>({
|
||
webhookId: 'random-id',
|
||
type: nodeType,
|
||
parameters: { path: '' },
|
||
});
|
||
|
||
const { resolveNodeWebhook } = useCanvasOperations({ router });
|
||
resolveNodeWebhook(node, nodeTypeDescription);
|
||
|
||
expect(node.webhookId).toBe('random-id');
|
||
expect(node.parameters.path).toBe('random-id');
|
||
},
|
||
);
|
||
});
|
||
|
||
describe('initializeWorkspace', () => {
|
||
it('should initialize the workspace', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const workflow = createTestWorkflow({
|
||
nodes: [createTestNode()],
|
||
connections: {},
|
||
});
|
||
|
||
const { initializeWorkspace } = useCanvasOperations({ router });
|
||
initializeWorkspace(workflow);
|
||
|
||
expect(workflowsStore.setNodes).toHaveBeenCalled();
|
||
expect(workflowsStore.setConnections).toHaveBeenCalled();
|
||
});
|
||
|
||
it('should initialize node data from node type description', () => {
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const type = SET_NODE_TYPE;
|
||
const version = 1;
|
||
const expectedDescription = mockNodeTypeDescription({
|
||
name: type,
|
||
version,
|
||
properties: [
|
||
{
|
||
displayName: 'Value',
|
||
name: 'value',
|
||
type: 'boolean',
|
||
default: true,
|
||
},
|
||
],
|
||
});
|
||
|
||
nodeTypesStore.nodeTypes = { [type]: { [version]: expectedDescription } };
|
||
|
||
const workflow = createTestWorkflow({
|
||
nodes: [createTestNode()],
|
||
connections: {},
|
||
});
|
||
|
||
const { initializeWorkspace } = useCanvasOperations({ router });
|
||
initializeWorkspace(workflow);
|
||
|
||
expect(workflow.nodes[0].parameters).toEqual({ value: true });
|
||
});
|
||
});
|
||
|
||
describe('filterConnectionsByNodes', () => {
|
||
it('should return filtered connections when all nodes are included', () => {
|
||
const connections: INodeConnections = {
|
||
[NodeConnectionTypes.Main]: [
|
||
[
|
||
{ node: 'node1', type: NodeConnectionTypes.Main, index: 0 },
|
||
{ node: 'node2', type: NodeConnectionTypes.Main, index: 0 },
|
||
],
|
||
[{ node: 'node3', type: NodeConnectionTypes.Main, index: 0 }],
|
||
],
|
||
};
|
||
const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);
|
||
|
||
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||
|
||
expect(result).toEqual(connections);
|
||
});
|
||
|
||
it('should return empty connections when no nodes are included', () => {
|
||
const connections: INodeConnections = {
|
||
[NodeConnectionTypes.Main]: [
|
||
[
|
||
{ node: 'node1', type: NodeConnectionTypes.Main, index: 0 },
|
||
{ node: 'node2', type: NodeConnectionTypes.Main, index: 0 },
|
||
],
|
||
[{ node: 'node3', type: NodeConnectionTypes.Main, index: 0 }],
|
||
],
|
||
};
|
||
const includeNodeNames = new Set<string>();
|
||
|
||
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||
|
||
expect(result).toEqual({
|
||
[NodeConnectionTypes.Main]: [[], []],
|
||
});
|
||
});
|
||
|
||
it('should return partially filtered connections when some nodes are included', () => {
|
||
const connections: INodeConnections = {
|
||
[NodeConnectionTypes.Main]: [
|
||
[
|
||
{ node: 'node1', type: NodeConnectionTypes.Main, index: 0 },
|
||
{ node: 'node2', type: NodeConnectionTypes.Main, index: 0 },
|
||
],
|
||
[{ node: 'node3', type: NodeConnectionTypes.Main, index: 0 }],
|
||
],
|
||
};
|
||
const includeNodeNames = new Set<string>(['node1']);
|
||
|
||
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||
|
||
expect(result).toEqual({
|
||
[NodeConnectionTypes.Main]: [
|
||
[{ node: 'node1', type: NodeConnectionTypes.Main, index: 0 }],
|
||
[],
|
||
],
|
||
});
|
||
});
|
||
|
||
it('should handle empty connections input', () => {
|
||
const connections: INodeConnections = {};
|
||
const includeNodeNames = new Set<string>(['node1']);
|
||
|
||
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||
|
||
expect(result).toEqual({});
|
||
});
|
||
|
||
it('should handle connections with no valid nodes', () => {
|
||
const connections: INodeConnections = {
|
||
[NodeConnectionTypes.Main]: [
|
||
[
|
||
{ node: 'node4', type: NodeConnectionTypes.Main, index: 0 },
|
||
{ node: 'node5', type: NodeConnectionTypes.Main, index: 0 },
|
||
],
|
||
[{ node: 'node6', type: NodeConnectionTypes.Main, index: 0 }],
|
||
],
|
||
};
|
||
const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);
|
||
|
||
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||
|
||
expect(result).toEqual({
|
||
[NodeConnectionTypes.Main]: [[], []],
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('openExecution', () => {
|
||
it('should initialize workspace and set execution data when execution is found', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const uiStore = mockedStore(useUIStore);
|
||
const { openExecution } = useCanvasOperations({ router });
|
||
|
||
const executionId = '123';
|
||
const executionData: IExecutionResponse = {
|
||
id: executionId,
|
||
finished: true,
|
||
status: 'success',
|
||
startedAt: new Date(),
|
||
createdAt: new Date(),
|
||
workflowData: createTestWorkflow(),
|
||
mode: 'manual' as WorkflowExecuteMode,
|
||
};
|
||
|
||
workflowsStore.getExecution.mockResolvedValue(executionData);
|
||
|
||
const result = await openExecution(executionId);
|
||
|
||
expect(workflowsStore.setWorkflowExecutionData).toHaveBeenCalledWith(executionData);
|
||
expect(uiStore.stateIsDirty).toBe(false);
|
||
expect(result).toEqual(executionData);
|
||
});
|
||
|
||
it('should throw error when execution data is undefined', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const executionId = '123';
|
||
const { openExecution } = useCanvasOperations({ router });
|
||
|
||
workflowsStore.getExecution.mockResolvedValue(undefined);
|
||
|
||
await expect(openExecution(executionId)).rejects.toThrow(
|
||
`Execution with id "${executionId}" could not be found!`,
|
||
);
|
||
});
|
||
|
||
it('should clear workflow pin data if execution mode is not manual', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const { openExecution } = useCanvasOperations({ router });
|
||
|
||
const executionId = '123';
|
||
const executionData: IExecutionResponse = {
|
||
id: executionId,
|
||
finished: true,
|
||
status: 'success',
|
||
startedAt: new Date(),
|
||
createdAt: new Date(),
|
||
workflowData: createTestWorkflow(),
|
||
mode: 'trigger' as WorkflowExecuteMode,
|
||
};
|
||
|
||
workflowsStore.getExecution.mockResolvedValue(executionData);
|
||
|
||
await openExecution(executionId);
|
||
|
||
expect(workflowsStore.setWorkflowPinData).toHaveBeenCalledWith({});
|
||
});
|
||
it('should show an error notification for failed executions', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const { openExecution } = useCanvasOperations({ router });
|
||
const toast = useToast();
|
||
|
||
const executionId = '123';
|
||
const executionData: IExecutionResponse = {
|
||
id: executionId,
|
||
finished: true,
|
||
status: 'error',
|
||
startedAt: new Date(),
|
||
createdAt: new Date(),
|
||
workflowData: createTestWorkflow(),
|
||
mode: 'manual',
|
||
data: {
|
||
resultData: {
|
||
error: { message: 'Crashed', node: { name: 'Step1' } },
|
||
lastNodeExecuted: 'Last Node',
|
||
},
|
||
} as IExecutionResponse['data'],
|
||
};
|
||
|
||
workflowsStore.getExecution.mockResolvedValue(executionData);
|
||
|
||
await openExecution(executionId);
|
||
|
||
expect(toast.showMessage).toHaveBeenCalledWith({
|
||
duration: 0,
|
||
message: 'Crashed',
|
||
title: 'Problem in node ‘Last Node‘',
|
||
type: 'error',
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('connectAdjacentNodes', () => {
|
||
it('should connect nodes that were connected through the removed node', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const historyStore = mockedStore(useHistoryStore);
|
||
|
||
// Create three nodes in a sequence: A -> B -> C
|
||
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
|
||
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
|
||
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
|
||
|
||
const nodeTypeDescription = mockNodeTypeDescription({
|
||
name: nodeA.type,
|
||
inputs: [NodeConnectionTypes.Main],
|
||
outputs: [NodeConnectionTypes.Main],
|
||
});
|
||
|
||
nodeTypesStore.getNodeType = vi.fn(() => nodeTypeDescription);
|
||
|
||
// Set up the workflow connections A -> B -> C
|
||
workflowsStore.workflow.nodes = [nodeA, nodeB, nodeC];
|
||
workflowsStore.workflow.connections = {
|
||
[nodeA.name]: {
|
||
main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
},
|
||
[nodeB.name]: {
|
||
main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
},
|
||
};
|
||
|
||
// Mock store methods
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.getNodeById.mockImplementation(
|
||
(id: string) =>
|
||
({
|
||
[nodeA.id]: nodeA,
|
||
[nodeB.id]: nodeB,
|
||
[nodeC.id]: nodeC,
|
||
})[id],
|
||
);
|
||
workflowsStore.getNodeByName.mockImplementation(
|
||
(name: string) =>
|
||
({
|
||
[nodeA.name]: nodeA,
|
||
[nodeB.name]: nodeB,
|
||
[nodeC.name]: nodeC,
|
||
})[name],
|
||
);
|
||
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({
|
||
main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
});
|
||
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({
|
||
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
});
|
||
|
||
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||
connectAdjacentNodes(nodeB.id, { trackHistory: true });
|
||
|
||
// Check that A was connected directly to C
|
||
expect(workflowsStore.addConnection).toHaveBeenCalledWith({
|
||
connection: [
|
||
{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 },
|
||
{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 },
|
||
],
|
||
});
|
||
|
||
// Verify the connection was tracked in history
|
||
expect(historyStore.pushCommandToUndo).toHaveBeenCalled();
|
||
});
|
||
|
||
it('should connect nodes that were connected through the removed node at different indices', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||
const historyStore = mockedStore(useHistoryStore);
|
||
|
||
// Create three nodes in a sequence: A -> B -> C
|
||
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
|
||
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
|
||
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
|
||
|
||
const nodeTypeDescription = mockNodeTypeDescription({
|
||
name: nodeA.type,
|
||
inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
|
||
outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
|
||
});
|
||
|
||
nodeTypesStore.getNodeType = vi.fn(() => nodeTypeDescription);
|
||
|
||
// Set up the workflow connections A -> B -> C
|
||
workflowsStore.workflow.nodes = [nodeA, nodeB, nodeC];
|
||
workflowsStore.workflow.connections = {
|
||
[nodeA.name]: {
|
||
main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 1 }]],
|
||
},
|
||
[nodeB.name]: {
|
||
main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
},
|
||
};
|
||
|
||
// Mock store methods
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.getNodeById.mockImplementation(
|
||
(id: string) =>
|
||
({
|
||
[nodeA.id]: nodeA,
|
||
[nodeB.id]: nodeB,
|
||
[nodeC.id]: nodeC,
|
||
})[id],
|
||
);
|
||
workflowsStore.getNodeByName.mockImplementation(
|
||
(name: string) =>
|
||
({
|
||
[nodeA.name]: nodeA,
|
||
[nodeB.name]: nodeB,
|
||
[nodeC.name]: nodeC,
|
||
})[name],
|
||
);
|
||
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({
|
||
main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 1 }]],
|
||
});
|
||
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({
|
||
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
});
|
||
|
||
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||
connectAdjacentNodes(nodeB.id, { trackHistory: true });
|
||
|
||
// Check that A was connected directly to C
|
||
expect(workflowsStore.addConnection).toHaveBeenCalledWith({
|
||
connection: [
|
||
{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 },
|
||
{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 1 },
|
||
],
|
||
});
|
||
|
||
// Verify the connection was tracked in history
|
||
expect(historyStore.pushCommandToUndo).toHaveBeenCalled();
|
||
});
|
||
|
||
it('should not create connections if middle node has no incoming connections', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
|
||
// Create nodes: B -> C (no incoming to B)
|
||
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
|
||
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
|
||
|
||
workflowsStore.workflow.nodes = [nodeB, nodeC];
|
||
workflowsStore.workflow.connections = {
|
||
[nodeB.name]: {
|
||
main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
},
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.getNodeById.mockReturnValue(nodeB);
|
||
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({
|
||
main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
});
|
||
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
|
||
|
||
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||
connectAdjacentNodes(nodeB.id);
|
||
|
||
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('should not create connections if middle node has no outgoing connections', () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
|
||
// Create nodes: A -> B (no outgoing from B)
|
||
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
|
||
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
|
||
|
||
workflowsStore.workflow.nodes = [nodeA, nodeB];
|
||
workflowsStore.workflow.connections = {
|
||
[nodeA.name]: {
|
||
main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
},
|
||
};
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
workflowsStore.getNodeById.mockReturnValue(nodeB);
|
||
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
|
||
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({
|
||
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
});
|
||
|
||
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||
connectAdjacentNodes(nodeB.id);
|
||
|
||
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('toggleChatOpen', () => {
|
||
it('should invoke workflowsStore#toggleLogsPanelOpen with 2nd argument passed through as 1st argument', async () => {
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
const { toggleChatOpen } = useCanvasOperations({ router });
|
||
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(createTestWorkflowObject());
|
||
|
||
await toggleChatOpen('main');
|
||
expect(workflowsStore.toggleLogsPanelOpen).toHaveBeenCalledWith(undefined);
|
||
|
||
await toggleChatOpen('main', true);
|
||
expect(workflowsStore.toggleLogsPanelOpen).toHaveBeenCalledWith(true);
|
||
|
||
await toggleChatOpen('main', false);
|
||
expect(workflowsStore.toggleLogsPanelOpen).toHaveBeenCalledWith(false);
|
||
});
|
||
});
|
||
|
||
describe('importTemplate', () => {
|
||
it('should import template to canvas', async () => {
|
||
const projectsStore = mockedStore(useProjectsStore);
|
||
projectsStore.currentProjectId = 'test-project-id';
|
||
|
||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||
workflowsStore.convertTemplateNodeToNodeUi.mockImplementation((node) => ({
|
||
...node,
|
||
credentials: {},
|
||
}));
|
||
|
||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||
|
||
// Create nodes: A -> B (no outgoing from B)
|
||
const nodeA: IWorkflowTemplateNode = createTestNode({
|
||
id: 'X',
|
||
name: 'Node X',
|
||
position: [80, 80],
|
||
});
|
||
const nodeB: IWorkflowTemplateNode = createTestNode({
|
||
id: 'Y',
|
||
name: 'Node Y',
|
||
position: [180, 80],
|
||
});
|
||
|
||
const workflow: IWorkflowTemplate['workflow'] = {
|
||
nodes: [nodeA, nodeB],
|
||
connections: {
|
||
[nodeA.name]: {
|
||
main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||
},
|
||
},
|
||
};
|
||
|
||
const { importTemplate } = useCanvasOperations({ router });
|
||
|
||
const templateId = 'template-id';
|
||
const templateName = 'template name';
|
||
await importTemplate({
|
||
id: templateId,
|
||
name: templateName,
|
||
workflow,
|
||
});
|
||
|
||
expect(workflowsStore.setConnections).toHaveBeenCalledWith(workflow.connections);
|
||
expect(workflowsStore.addNode).toHaveBeenNthCalledWith(1, {
|
||
...nodeA,
|
||
credentials: {},
|
||
disabled: false,
|
||
});
|
||
expect(workflowsStore.setNodePristine).toHaveBeenCalledWith(nodeA.name, true);
|
||
expect(workflowsStore.addNode).toHaveBeenNthCalledWith(2, {
|
||
...nodeB,
|
||
credentials: {},
|
||
disabled: false,
|
||
});
|
||
expect(workflowsStore.setNodePristine).toHaveBeenCalledWith(nodeB.name, true);
|
||
expect(workflowsStore.getNewWorkflowDataAndMakeShareable).toHaveBeenCalledWith(
|
||
templateName,
|
||
projectsStore.currentProjectId,
|
||
);
|
||
expect(workflowsStore.addToWorkflowMetadata).toHaveBeenCalledWith({ templateId });
|
||
});
|
||
});
|
||
});
|
||
|
||
function buildImportNodes() {
|
||
return [
|
||
mockNode({ id: '1', name: 'Node 1', type: SET_NODE_TYPE }),
|
||
mockNode({ id: '2', name: 'Node 2', type: SET_NODE_TYPE }),
|
||
].map((node) => {
|
||
// Setting position in mockNode will wrap it in a Proxy
|
||
// This causes deepCopy to remove position -> set position after instead
|
||
node.position = [40, 40];
|
||
return node;
|
||
});
|
||
}
|