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