Skip to content

Commit 224ea10

Browse files
[test] Add unit tests for widget promotion system
- Test parentSubgraphNode property assignment - Test widget-promoted event dispatch - Test widget-unpromoted event dispatch - Test cleanup in onRemoved() method - Test AbortController error handling
1 parent 80ecd0c commit 224ea10

File tree

1 file changed

+340
-0
lines changed

1 file changed

+340
-0
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
import { describe, expect, it, vi } from "vitest"
2+
3+
import { LGraph } from "@/LGraph"
4+
import { Subgraph } from "@/subgraph/Subgraph"
5+
import { SubgraphNode } from "@/subgraph/SubgraphNode"
6+
import { BaseWidget } from "@/widgets/BaseWidget"
7+
import type { IBaseWidget } from "@/types/widgets"
8+
9+
describe("SubgraphWidgetPromotion", () => {
10+
function createTestSetup() {
11+
const rootGraph = new LGraph()
12+
const subgraph = new Subgraph(rootGraph, {
13+
id: "test-subgraph",
14+
name: "Test Subgraph",
15+
nodes: [],
16+
links: [],
17+
version: 1,
18+
state: 0,
19+
revision: 0,
20+
inputNode: { pos: [0, 0] },
21+
outputNode: { pos: [200, 0] },
22+
inputs: [],
23+
outputs: [],
24+
widgets: [],
25+
groups: [],
26+
config: {},
27+
extra: {},
28+
})
29+
30+
const subgraphNode = new SubgraphNode(rootGraph, subgraph, {
31+
id: 1,
32+
type: subgraph.id,
33+
pos: [100, 100],
34+
size: [200, 100],
35+
})
36+
37+
return { rootGraph, subgraph, subgraphNode }
38+
}
39+
40+
describe("parentSubgraphNode property", () => {
41+
it("should set parentSubgraphNode for all promoted widgets", () => {
42+
const { subgraph, subgraphNode } = createTestSetup()
43+
44+
// Add an input that will get a widget
45+
const input = subgraph.addInput("test_input", "number")
46+
47+
// Create a mock widget
48+
const mockWidget: IBaseWidget = {
49+
name: "test_widget",
50+
type: "number",
51+
value: 42,
52+
y: 0,
53+
options: {},
54+
createCopyForNode: vi.fn().mockImplementation(function(this: IBaseWidget, node) {
55+
return {
56+
...this,
57+
node,
58+
parentSubgraphNode: undefined,
59+
}
60+
}),
61+
}
62+
63+
// Simulate connecting a widget to the input
64+
input._widget = mockWidget
65+
66+
// Trigger widget promotion by reconfiguring
67+
subgraphNode.configure({
68+
id: 1,
69+
type: subgraph.id,
70+
pos: [100, 100],
71+
size: [200, 100],
72+
})
73+
74+
// Check that the promoted widget has parentSubgraphNode set
75+
const promotedWidget = subgraphNode.widgets[0]
76+
expect(promotedWidget).toBeDefined()
77+
expect(promotedWidget.parentSubgraphNode).toBe(subgraphNode)
78+
})
79+
80+
it("should set parentSubgraphNode for DOM widgets", () => {
81+
const { subgraph, subgraphNode } = createTestSetup()
82+
83+
const input = subgraph.addInput("dom_input", "string")
84+
85+
// Create a mock DOM widget
86+
const mockDOMWidget: IBaseWidget & { element: HTMLElement } = {
87+
name: "dom_widget",
88+
type: "custom",
89+
value: "test",
90+
y: 0,
91+
options: {},
92+
element: document.createElement("div"),
93+
isDOMWidget: () => true,
94+
createCopyForNode: vi.fn().mockImplementation(function(this: IBaseWidget, node) {
95+
return {
96+
...this,
97+
node,
98+
parentSubgraphNode: undefined,
99+
}
100+
}),
101+
}
102+
103+
input._widget = mockDOMWidget
104+
105+
subgraphNode.configure({
106+
id: 1,
107+
type: subgraph.id,
108+
pos: [100, 100],
109+
size: [200, 100],
110+
})
111+
112+
const promotedWidget = subgraphNode.widgets[0]
113+
expect(promotedWidget).toBeDefined()
114+
expect(promotedWidget.parentSubgraphNode).toBe(subgraphNode)
115+
})
116+
})
117+
118+
describe("widget-promoted event", () => {
119+
it("should dispatch widget-promoted event when widget is added", () => {
120+
const { subgraph, subgraphNode } = createTestSetup()
121+
122+
const promotedHandler = vi.fn()
123+
subgraph.events.addEventListener("widget-promoted", promotedHandler)
124+
125+
const input = subgraph.addInput("test_input", "number")
126+
const mockWidget: IBaseWidget = {
127+
name: "test_widget",
128+
type: "number",
129+
value: 42,
130+
y: 0,
131+
options: {},
132+
createCopyForNode: vi.fn().mockImplementation(function(this: IBaseWidget, node) {
133+
return {
134+
...this,
135+
node,
136+
parentSubgraphNode: undefined,
137+
}
138+
}),
139+
}
140+
141+
input._widget = mockWidget
142+
143+
subgraphNode.configure({
144+
id: 1,
145+
type: subgraph.id,
146+
pos: [100, 100],
147+
size: [200, 100],
148+
})
149+
150+
expect(promotedHandler).toHaveBeenCalledTimes(1)
151+
expect(promotedHandler).toHaveBeenCalledWith(
152+
expect.objectContaining({
153+
detail: {
154+
widget: expect.objectContaining({
155+
name: "test_input", // Name gets overridden to match input
156+
parentSubgraphNode: subgraphNode,
157+
}),
158+
subgraphNode: subgraphNode,
159+
},
160+
})
161+
)
162+
})
163+
})
164+
165+
describe("widget-unpromoted event", () => {
166+
it("should dispatch widget-unpromoted event when widget is removed by name", () => {
167+
const { subgraph, subgraphNode } = createTestSetup()
168+
169+
// Set up a promoted widget first
170+
const input = subgraph.addInput("test_input", "number")
171+
const mockWidget: IBaseWidget = {
172+
name: "test_widget",
173+
type: "number",
174+
value: 42,
175+
y: 0,
176+
options: {},
177+
createCopyForNode: vi.fn().mockImplementation(function(this: IBaseWidget, node) {
178+
return {
179+
...this,
180+
node,
181+
parentSubgraphNode: undefined,
182+
}
183+
}),
184+
}
185+
186+
input._widget = mockWidget
187+
subgraphNode.configure({
188+
id: 1,
189+
type: subgraph.id,
190+
pos: [100, 100],
191+
size: [200, 100],
192+
})
193+
194+
// Now listen for unpromoted event
195+
const unpromotedHandler = vi.fn()
196+
subgraph.events.addEventListener("widget-unpromoted", unpromotedHandler)
197+
198+
// Remove the widget
199+
subgraphNode.removeWidgetByName("test_input")
200+
201+
expect(unpromotedHandler).toHaveBeenCalledTimes(1)
202+
expect(unpromotedHandler).toHaveBeenCalledWith(
203+
expect.objectContaining({
204+
detail: {
205+
widget: expect.objectContaining({
206+
name: "test_input",
207+
}),
208+
subgraphNode: subgraphNode,
209+
},
210+
})
211+
)
212+
})
213+
214+
it("should dispatch widget-unpromoted event when widget is removed directly", () => {
215+
const { subgraph, subgraphNode } = createTestSetup()
216+
217+
// Set up a promoted widget
218+
const input = subgraph.addInput("test_input", "number")
219+
const mockWidget: IBaseWidget = {
220+
name: "test_widget",
221+
type: "number",
222+
value: 42,
223+
y: 0,
224+
options: {},
225+
createCopyForNode: vi.fn().mockImplementation(function(this: IBaseWidget, node) {
226+
return {
227+
...this,
228+
node,
229+
parentSubgraphNode: undefined,
230+
}
231+
}),
232+
}
233+
234+
input._widget = mockWidget
235+
subgraphNode.configure({
236+
id: 1,
237+
type: subgraph.id,
238+
pos: [100, 100],
239+
size: [200, 100],
240+
})
241+
242+
const promotedWidget = subgraphNode.widgets[0]
243+
244+
const unpromotedHandler = vi.fn()
245+
subgraph.events.addEventListener("widget-unpromoted", unpromotedHandler)
246+
247+
// Remove the widget directly
248+
subgraphNode.ensureWidgetRemoved(promotedWidget)
249+
250+
expect(unpromotedHandler).toHaveBeenCalledTimes(1)
251+
expect(unpromotedHandler).toHaveBeenCalledWith(
252+
expect.objectContaining({
253+
detail: {
254+
widget: promotedWidget,
255+
subgraphNode: subgraphNode,
256+
},
257+
})
258+
)
259+
})
260+
261+
it("should dispatch widget-unpromoted events for all widgets when subgraph node is removed", () => {
262+
const { subgraph, subgraphNode } = createTestSetup()
263+
264+
// Set up multiple promoted widgets
265+
const input1 = subgraph.addInput("input1", "number")
266+
const input2 = subgraph.addInput("input2", "string")
267+
268+
const mockWidget1: IBaseWidget = {
269+
name: "widget1",
270+
type: "number",
271+
value: 42,
272+
y: 0,
273+
options: {},
274+
createCopyForNode: vi.fn().mockImplementation(function(this: IBaseWidget, node) {
275+
return {
276+
...this,
277+
node,
278+
parentSubgraphNode: undefined,
279+
}
280+
}),
281+
}
282+
283+
const mockWidget2: IBaseWidget = {
284+
name: "widget2",
285+
type: "string",
286+
value: "test",
287+
y: 0,
288+
options: {},
289+
createCopyForNode: vi.fn().mockImplementation(function(this: IBaseWidget, node) {
290+
return {
291+
...this,
292+
node,
293+
parentSubgraphNode: undefined,
294+
}
295+
}),
296+
}
297+
298+
input1._widget = mockWidget1
299+
input2._widget = mockWidget2
300+
301+
subgraphNode.configure({
302+
id: 1,
303+
type: subgraph.id,
304+
pos: [100, 100],
305+
size: [200, 100],
306+
})
307+
308+
const unpromotedHandler = vi.fn()
309+
subgraph.events.addEventListener("widget-unpromoted", unpromotedHandler)
310+
311+
// Remove the subgraph node
312+
subgraphNode.onRemoved()
313+
314+
expect(unpromotedHandler).toHaveBeenCalledTimes(2)
315+
316+
// Check that parentSubgraphNode was cleared
317+
subgraphNode.widgets.forEach(widget => {
318+
expect(widget.parentSubgraphNode).toBeUndefined()
319+
})
320+
})
321+
})
322+
323+
describe("AbortController cleanup", () => {
324+
it("should handle missing abort method gracefully", () => {
325+
const { subgraph, subgraphNode } = createTestSetup()
326+
327+
// Add an input with a mock controller that doesn't have abort
328+
const input = subgraph.addInput("test", "number")
329+
const subgraphInput = subgraphNode.inputs[0]
330+
331+
// Mock a controller without abort method
332+
subgraphInput._listenerController = { someOtherProp: true } as any
333+
334+
// This should not throw
335+
expect(() => {
336+
subgraphNode.onRemoved()
337+
}).not.toThrow()
338+
})
339+
})
340+
})

0 commit comments

Comments
 (0)