Skip to content

Commit b38e5ec

Browse files
committed
Refactor addAndConnectNode
1 parent d57dd35 commit b38e5ec

File tree

2 files changed

+439
-131
lines changed

2 files changed

+439
-131
lines changed
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import type { Handle, Position } from "@xyflow/react";
2+
import { beforeEach, describe, expect, test, vi } from "vitest";
3+
4+
import type { NodeManager } from "@/nodeManager";
5+
import {
6+
type ComponentReference,
7+
type ComponentSpec,
8+
isGraphImplementation,
9+
} from "@/utils/componentSpec";
10+
11+
import { addAndConnectNode } from "./addAndConnectNode";
12+
import { handleConnection } from "./handleConnection";
13+
14+
// Mock the dependencies
15+
vi.mock("./addTask", () => ({
16+
default: vi.fn((_taskType, taskSpec, _position, componentSpec) => {
17+
const newTaskId = `new-task-${Date.now()}`;
18+
return {
19+
...componentSpec,
20+
implementation: {
21+
...componentSpec.implementation,
22+
graph: {
23+
...componentSpec.implementation.graph,
24+
tasks: {
25+
...componentSpec.implementation.graph.tasks,
26+
[newTaskId]: taskSpec,
27+
},
28+
},
29+
},
30+
};
31+
}),
32+
}));
33+
34+
vi.mock("./handleConnection", () => ({
35+
handleConnection: vi.fn((graph) => graph), // Just return the graph unchanged for testing
36+
}));
37+
38+
describe("addAndConnectNode", () => {
39+
const mockNodeManager: NodeManager = {
40+
getHandleInfo: vi.fn(),
41+
getNodeId: vi.fn(),
42+
getHandleNodeId: vi.fn(),
43+
} as any;
44+
45+
const mockComponentRef: ComponentReference = {
46+
spec: {
47+
name: "TestComponent",
48+
inputs: [{ name: "input1", type: "string" }],
49+
outputs: [{ name: "output1", type: "string" }],
50+
implementation: {
51+
container: { image: "test", command: ["echo"] },
52+
},
53+
},
54+
};
55+
56+
const mockComponentSpec: ComponentSpec = {
57+
name: "Pipeline",
58+
inputs: [{ name: "pipelineInput", type: "string" }],
59+
outputs: [{ name: "pipelineOutput", type: "string" }],
60+
implementation: {
61+
graph: {
62+
tasks: {
63+
"existing-task": {
64+
annotations: {},
65+
componentRef: {
66+
spec: {
67+
name: "ExistingTask",
68+
inputs: [{ name: "taskInput", type: "string" }],
69+
outputs: [{ name: "taskOutput", type: "string" }],
70+
implementation: {
71+
container: { image: "existing", command: ["run"] },
72+
},
73+
},
74+
},
75+
},
76+
},
77+
},
78+
},
79+
};
80+
81+
const createMockHandle = (
82+
id: string | null | undefined,
83+
nodeId: string,
84+
): Handle => ({
85+
id,
86+
nodeId,
87+
x: 0,
88+
y: 0,
89+
position: "top" as Position,
90+
type: "source",
91+
width: 10,
92+
height: 10,
93+
});
94+
95+
beforeEach(() => {
96+
vi.clearAllMocks();
97+
});
98+
99+
test("should return unchanged spec when implementation is not a graph", () => {
100+
const nonGraphSpec: ComponentSpec = {
101+
name: "NonGraph",
102+
implementation: {
103+
container: { image: "test", command: ["echo"] },
104+
},
105+
};
106+
107+
const result = addAndConnectNode({
108+
componentRef: mockComponentRef,
109+
fromHandle: createMockHandle("handle1", "node1"),
110+
position: { x: 100, y: 100 },
111+
componentSpec: nonGraphSpec,
112+
nodeManager: mockNodeManager,
113+
});
114+
115+
expect(result).toBe(nonGraphSpec);
116+
});
117+
118+
test("should return unchanged spec when fromHandle has no id", () => {
119+
const result = addAndConnectNode({
120+
componentRef: mockComponentRef,
121+
fromHandle: createMockHandle(null, "node1"),
122+
position: { x: 100, y: 100 },
123+
componentSpec: mockComponentSpec,
124+
nodeManager: mockNodeManager,
125+
});
126+
127+
expect(result).toBe(mockComponentSpec);
128+
});
129+
130+
test("should return unchanged spec when fromHandle has empty string id", () => {
131+
const result = addAndConnectNode({
132+
componentRef: mockComponentRef,
133+
fromHandle: createMockHandle("", "node1"),
134+
position: { x: 100, y: 100 },
135+
componentSpec: mockComponentSpec,
136+
nodeManager: mockNodeManager,
137+
});
138+
139+
expect(result).toBe(mockComponentSpec);
140+
});
141+
142+
test("should return unchanged spec when fromHandle has undefined id", () => {
143+
const result = addAndConnectNode({
144+
componentRef: mockComponentRef,
145+
fromHandle: createMockHandle(undefined, "node1"),
146+
position: { x: 100, y: 100 },
147+
componentSpec: mockComponentSpec,
148+
nodeManager: mockNodeManager,
149+
});
150+
151+
expect(result).toBe(mockComponentSpec);
152+
});
153+
154+
test("should return unchanged spec when handle info is invalid", () => {
155+
vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue(undefined);
156+
157+
const result = addAndConnectNode({
158+
componentRef: mockComponentRef,
159+
fromHandle: createMockHandle("handle1", "node1"),
160+
position: { x: 100, y: 100 },
161+
componentSpec: mockComponentSpec,
162+
nodeManager: mockNodeManager,
163+
});
164+
165+
expect(result).toBe(mockComponentSpec);
166+
});
167+
168+
test("should return unchanged spec when handle type is invalid", () => {
169+
vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({
170+
handleType: "invalidType" as any,
171+
parentRefId: "task1",
172+
handleName: "output1",
173+
});
174+
175+
const result = addAndConnectNode({
176+
componentRef: mockComponentRef,
177+
fromHandle: createMockHandle("handle1", "node1"),
178+
position: { x: 100, y: 100 },
179+
componentSpec: mockComponentSpec,
180+
nodeManager: mockNodeManager,
181+
});
182+
183+
expect(result).toBe(mockComponentSpec);
184+
});
185+
186+
test("should add task and attempt connection from output handle", () => {
187+
vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({
188+
handleType: "outputHandle",
189+
parentRefId: "existing-task",
190+
handleName: "taskOutput",
191+
});
192+
vi.mocked(mockNodeManager.getNodeId).mockReturnValue("new-node-id");
193+
vi.mocked(mockNodeManager.getHandleNodeId).mockReturnValue("new-handle-id");
194+
195+
const result = addAndConnectNode({
196+
componentRef: mockComponentRef,
197+
fromHandle: createMockHandle("from-handle", "from-node"),
198+
position: { x: 100, y: 100 },
199+
componentSpec: mockComponentSpec,
200+
nodeManager: mockNodeManager,
201+
});
202+
203+
// Should have added a new task
204+
const graphSpec =
205+
isGraphImplementation(result.implementation) &&
206+
result.implementation.graph;
207+
if (!graphSpec) {
208+
throw new Error("Resulting implementation is not a graph");
209+
}
210+
const tasks = graphSpec?.tasks;
211+
expect(Object.keys(tasks)).toHaveLength(2);
212+
213+
// Verify handleConnection was called
214+
expect(handleConnection).toHaveBeenCalled();
215+
});
216+
217+
test("should add task and attempt connection from input handle", () => {
218+
vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({
219+
handleType: "inputHandle",
220+
parentRefId: "existing-task",
221+
handleName: "taskInput",
222+
});
223+
vi.mocked(mockNodeManager.getNodeId).mockReturnValue("new-node-id");
224+
vi.mocked(mockNodeManager.getHandleNodeId).mockReturnValue("new-handle-id");
225+
226+
const result = addAndConnectNode({
227+
componentRef: mockComponentRef,
228+
fromHandle: createMockHandle("from-handle", "from-node"),
229+
position: { x: 100, y: 100 },
230+
componentSpec: mockComponentSpec,
231+
nodeManager: mockNodeManager,
232+
});
233+
234+
// Should have added a new task
235+
const graphSpec =
236+
isGraphImplementation(result.implementation) &&
237+
result.implementation.graph;
238+
if (!graphSpec) {
239+
throw new Error("Resulting implementation is not a graph");
240+
}
241+
const tasks = graphSpec?.tasks;
242+
expect(Object.keys(tasks)).toHaveLength(2);
243+
244+
// Verify handleConnection was called
245+
expect(handleConnection).toHaveBeenCalled();
246+
});
247+
248+
test("should add task but skip connection when no compatible handle found", () => {
249+
vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({
250+
handleType: "outputHandle",
251+
parentRefId: "existing-task",
252+
handleName: "taskOutput",
253+
});
254+
255+
const incompatibleComponentRef: ComponentReference = {
256+
spec: {
257+
name: "IncompatibleComponent",
258+
inputs: [{ name: "input1", type: "number" }],
259+
outputs: [{ name: "output1", type: "boolean" }],
260+
implementation: {
261+
container: { image: "test", command: ["echo"] },
262+
},
263+
},
264+
};
265+
266+
const result = addAndConnectNode({
267+
componentRef: incompatibleComponentRef,
268+
fromHandle: createMockHandle("from-handle", "from-node"),
269+
position: { x: 100, y: 100 },
270+
componentSpec: mockComponentSpec,
271+
nodeManager: mockNodeManager,
272+
});
273+
274+
// Should still add the task even without connection
275+
const graphSpec =
276+
isGraphImplementation(result.implementation) &&
277+
result.implementation.graph;
278+
if (!graphSpec) {
279+
throw new Error("Resulting implementation is not a graph");
280+
}
281+
const tasks = graphSpec?.tasks;
282+
expect(Object.keys(tasks)).toHaveLength(2);
283+
284+
// Should not call handleConnection when no compatible handle
285+
expect(handleConnection).not.toHaveBeenCalled();
286+
});
287+
288+
test("should handle pipeline-level input/output handles", () => {
289+
vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({
290+
handleType: "inputHandle",
291+
parentRefId: "pipeline", // Pipeline-level handle
292+
handleName: "pipelineInput",
293+
});
294+
vi.mocked(mockNodeManager.getNodeId).mockReturnValue("new-node-id");
295+
vi.mocked(mockNodeManager.getHandleNodeId).mockReturnValue("new-handle-id");
296+
297+
const result = addAndConnectNode({
298+
componentRef: mockComponentRef,
299+
fromHandle: createMockHandle("from-handle", "from-node"),
300+
position: { x: 100, y: 100 },
301+
componentSpec: mockComponentSpec,
302+
nodeManager: mockNodeManager,
303+
});
304+
305+
// Should have added a new task
306+
const graphSpec =
307+
isGraphImplementation(result.implementation) &&
308+
result.implementation.graph;
309+
if (!graphSpec) {
310+
throw new Error("Resulting implementation is not a graph");
311+
}
312+
const tasks = graphSpec?.tasks;
313+
expect(Object.keys(tasks)).toHaveLength(2);
314+
});
315+
316+
test("should return unchanged spec when fromHandle is null", () => {
317+
const result = addAndConnectNode({
318+
componentRef: mockComponentRef,
319+
fromHandle: null,
320+
position: { x: 100, y: 100 },
321+
componentSpec: mockComponentSpec,
322+
nodeManager: mockNodeManager,
323+
});
324+
325+
expect(result).toBe(mockComponentSpec);
326+
});
327+
});

0 commit comments

Comments
 (0)