@@ -7,6 +7,7 @@ import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
7
7
import { app } from '@/scripts/app'
8
8
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
9
9
import { useNodeDefStore } from '@/stores/nodeDefStore'
10
+ import { collectAllNodes } from '@/utils/graphTraversalUtil'
10
11
11
12
// Mock Vue's onMounted to execute immediately for testing
12
13
vi . mock ( 'vue' , async ( ) => {
@@ -38,9 +39,14 @@ vi.mock('@/scripts/app', () => ({
38
39
}
39
40
} ) )
40
41
42
+ vi . mock ( '@/utils/graphTraversalUtil' , ( ) => ( {
43
+ collectAllNodes : vi . fn ( )
44
+ } ) )
45
+
41
46
const mockUseWorkflowPacks = vi . mocked ( useWorkflowPacks )
42
47
const mockUseComfyManagerStore = vi . mocked ( useComfyManagerStore )
43
48
const mockUseNodeDefStore = vi . mocked ( useNodeDefStore )
49
+ const mockCollectAllNodes = vi . mocked ( collectAllNodes )
44
50
45
51
describe ( 'useMissingNodes' , ( ) => {
46
52
const mockWorkflowPacks = [
@@ -95,6 +101,9 @@ describe('useMissingNodes', () => {
95
101
// Reset app.graph.nodes
96
102
// @ts -expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
97
103
app . graph . nodes = [ ]
104
+
105
+ // Default mock for collectAllNodes - returns empty array
106
+ mockCollectAllNodes . mockReturnValue ( [ ] )
98
107
} )
99
108
100
109
describe ( 'core filtering logic' , ( ) => {
@@ -286,14 +295,9 @@ describe('useMissingNodes', () => {
286
295
it ( 'identifies missing core nodes not in nodeDefStore' , ( ) => {
287
296
const coreNode1 = createMockNode ( 'CoreNode1' , 'comfy-core' , '1.2.0' )
288
297
const coreNode2 = createMockNode ( 'CoreNode2' , 'comfy-core' , '1.2.0' )
289
- const registeredNode = createMockNode (
290
- 'RegisteredNode' ,
291
- 'comfy-core' ,
292
- '1.0.0'
293
- )
294
298
295
- // @ts -expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
296
- app . graph . nodes = [ coreNode1 , coreNode2 , registeredNode ]
299
+ // Mock collectAllNodes to return only the filtered nodes (missing core nodes)
300
+ mockCollectAllNodes . mockReturnValue ( [ coreNode1 , coreNode2 ] )
297
301
298
302
mockUseNodeDefStore . mockReturnValue ( {
299
303
nodeDefsByName : {
@@ -316,8 +320,8 @@ describe('useMissingNodes', () => {
316
320
const node130 = createMockNode ( 'Node130' , 'comfy-core' , '1.3.0' )
317
321
const nodeNoVer = createMockNode ( 'NodeNoVer' , 'comfy-core' )
318
322
319
- // @ts -expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
320
- app . graph . nodes = [ node120 , node130 , nodeNoVer ]
323
+ // Mock collectAllNodes to return these nodes
324
+ mockCollectAllNodes . mockReturnValue ( [ node120 , node130 , nodeNoVer ] )
321
325
322
326
// @ts -expect-error - Mocking partial NodeDefStore for testing.
323
327
mockUseNodeDefStore . mockReturnValue ( {
@@ -334,11 +338,9 @@ describe('useMissingNodes', () => {
334
338
335
339
it ( 'ignores non-core nodes' , ( ) => {
336
340
const coreNode = createMockNode ( 'CoreNode' , 'comfy-core' , '1.2.0' )
337
- const customNode = createMockNode ( 'CustomNode' , 'custom-pack' , '1.0.0' )
338
- const noPackNode = createMockNode ( 'NoPackNode' )
339
341
340
- // @ts -expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
341
- app . graph . nodes = [ coreNode , customNode , noPackNode ]
342
+ // Mock collectAllNodes to return only the filtered nodes (core nodes only)
343
+ mockCollectAllNodes . mockReturnValue ( [ coreNode ] )
342
344
343
345
// @ts -expect-error - Mocking partial NodeDefStore for testing.
344
346
mockUseNodeDefStore . mockReturnValue ( {
@@ -353,33 +355,218 @@ describe('useMissingNodes', () => {
353
355
} )
354
356
355
357
it ( 'returns empty object when no core nodes are missing' , ( ) => {
356
- const registeredNode1 = createMockNode (
357
- 'RegisteredNode1' ,
358
+ // Mock collectAllNodes to return empty array (no missing nodes after filtering)
359
+ mockCollectAllNodes . mockReturnValue ( [ ] )
360
+
361
+ mockUseNodeDefStore . mockReturnValue ( {
362
+ nodeDefsByName : {
363
+ // @ts -expect-error - Creating minimal mock of ComfyNodeDefImpl for testing.
364
+ // Only including required properties for our test assertions.
365
+ RegisteredNode1 : { name : 'RegisteredNode1' } ,
366
+ // @ts -expect-error - Creating minimal mock of ComfyNodeDefImpl for testing.
367
+ RegisteredNode2 : { name : 'RegisteredNode2' }
368
+ }
369
+ } )
370
+
371
+ const { missingCoreNodes } = useMissingNodes ( )
372
+
373
+ expect ( Object . keys ( missingCoreNodes . value ) ) . toHaveLength ( 0 )
374
+ } )
375
+ } )
376
+
377
+ describe ( 'subgraph support' , ( ) => {
378
+ const createMockNode = (
379
+ type : string ,
380
+ packId ?: string ,
381
+ version ?: string
382
+ ) : LGraphNode =>
383
+ // @ts -expect-error - Creating a partial mock of LGraphNode for testing.
384
+ // We only need specific properties for our tests, not the full LGraphNode interface.
385
+ ( {
386
+ type,
387
+ properties : { cnr_id : packId , ver : version } ,
388
+ id : 1 ,
389
+ title : type ,
390
+ pos : [ 0 , 0 ] ,
391
+ size : [ 100 , 100 ] ,
392
+ flags : { } ,
393
+ graph : null ,
394
+ mode : 0 ,
395
+ inputs : [ ] ,
396
+ outputs : [ ]
397
+ } )
398
+
399
+ it ( 'detects missing core nodes from subgraphs via collectAllNodes' , ( ) => {
400
+ const mainNode = createMockNode ( 'MainNode' , 'comfy-core' , '1.0.0' )
401
+ const subgraphNode1 = createMockNode (
402
+ 'SubgraphNode1' ,
358
403
'comfy-core' ,
359
404
'1.0.0'
360
405
)
361
- const registeredNode2 = createMockNode (
362
- 'RegisteredNode2 ' ,
406
+ const subgraphNode2 = createMockNode (
407
+ 'SubgraphNode2 ' ,
363
408
'comfy-core' ,
364
409
'1.1.0'
365
410
)
366
411
367
- // @ts -expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
368
- app . graph . nodes = [ registeredNode1 , registeredNode2 ]
412
+ // Mock collectAllNodes to return all nodes including subgraph nodes
413
+ mockCollectAllNodes . mockReturnValue ( [
414
+ mainNode ,
415
+ subgraphNode1 ,
416
+ subgraphNode2
417
+ ] )
418
+
419
+ // Mock none of the nodes as registered
420
+ // @ts -expect-error - Mocking partial NodeDefStore for testing.
421
+ mockUseNodeDefStore . mockReturnValue ( {
422
+ nodeDefsByName : { }
423
+ } )
424
+
425
+ const { missingCoreNodes } = useMissingNodes ( )
426
+
427
+ // Should detect all 3 nodes as missing
428
+ expect ( Object . keys ( missingCoreNodes . value ) ) . toHaveLength ( 2 ) // 2 versions: 1.0.0, 1.1.0
429
+ expect ( missingCoreNodes . value [ '1.0.0' ] ) . toHaveLength ( 2 ) // MainNode + SubgraphNode1
430
+ expect ( missingCoreNodes . value [ '1.1.0' ] ) . toHaveLength ( 1 ) // SubgraphNode2
431
+ } )
432
+
433
+ it ( 'calls collectAllNodes with the app graph and filter function' , ( ) => {
434
+ const mockGraph = { nodes : [ ] , subgraphs : new Map ( ) }
435
+ // @ts -expect-error - Mocking app.graph for testing
436
+ app . graph = mockGraph
437
+
438
+ const { missingCoreNodes } = useMissingNodes ( )
439
+ // Access the computed to trigger the function
440
+ void missingCoreNodes . value
441
+
442
+ expect ( mockCollectAllNodes ) . toHaveBeenCalledWith (
443
+ mockGraph ,
444
+ expect . any ( Function )
445
+ )
446
+ } )
447
+
448
+ it ( 'handles collectAllNodes returning empty array' , ( ) => {
449
+ mockCollectAllNodes . mockReturnValue ( [ ] )
450
+
451
+ const { missingCoreNodes } = useMissingNodes ( )
452
+
453
+ expect ( Object . keys ( missingCoreNodes . value ) ) . toHaveLength ( 0 )
454
+ } )
455
+
456
+ it ( 'filter function correctly identifies missing core nodes' , ( ) => {
457
+ const mockGraph = { nodes : [ ] , subgraphs : new Map ( ) }
458
+ // @ts -expect-error - Mocking app.graph for testing
459
+ app . graph = mockGraph
369
460
370
461
mockUseNodeDefStore . mockReturnValue ( {
371
462
nodeDefsByName : {
372
463
// @ts -expect-error - Creating minimal mock of ComfyNodeDefImpl for testing.
373
- // Only including required properties for our test assertions.
374
- RegisteredNode1 : { name : 'RegisteredNode1' } ,
464
+ RegisteredCore : { name : 'RegisteredCore' }
465
+ }
466
+ } )
467
+
468
+ let capturedFilterFunction : ( ( node : LGraphNode ) => boolean ) | undefined
469
+
470
+ mockCollectAllNodes . mockImplementation ( ( _graph , filter ) => {
471
+ capturedFilterFunction = filter
472
+ return [ ]
473
+ } )
474
+
475
+ const { missingCoreNodes } = useMissingNodes ( )
476
+ void missingCoreNodes . value
477
+
478
+ expect ( capturedFilterFunction ) . toBeDefined ( )
479
+
480
+ if ( capturedFilterFunction ) {
481
+ const missingCoreNode = createMockNode (
482
+ 'MissingCore' ,
483
+ 'comfy-core' ,
484
+ '1.0.0'
485
+ )
486
+ const registeredCoreNode = createMockNode (
487
+ 'RegisteredCore' ,
488
+ 'comfy-core' ,
489
+ '1.0.0'
490
+ )
491
+ const customNode = createMockNode ( 'CustomNode' , 'custom-pack' , '1.0.0' )
492
+ const nodeWithoutPack = createMockNode ( 'NodeWithoutPack' )
493
+
494
+ expect ( capturedFilterFunction ( missingCoreNode ) ) . toBe ( true )
495
+ expect ( capturedFilterFunction ( registeredCoreNode ) ) . toBe ( false )
496
+ expect ( capturedFilterFunction ( customNode ) ) . toBe ( false )
497
+ expect ( capturedFilterFunction ( nodeWithoutPack ) ) . toBe ( false )
498
+ }
499
+ } )
500
+
501
+ it ( 'integrates with collectAllNodes to find nodes from subgraphs' , ( ) => {
502
+ mockCollectAllNodes . mockImplementation ( ( graph , filter ) => {
503
+ const allNodes : LGraphNode [ ] = [ ]
504
+
505
+ for ( const node of graph . nodes ) {
506
+ if ( node . isSubgraphNode ?.( ) && node . subgraph ) {
507
+ for ( const subNode of node . subgraph . nodes ) {
508
+ if ( ! filter || filter ( subNode ) ) {
509
+ allNodes . push ( subNode )
510
+ }
511
+ }
512
+ }
513
+
514
+ if ( ! filter || filter ( node ) ) {
515
+ allNodes . push ( node )
516
+ }
517
+ }
518
+
519
+ return allNodes
520
+ } )
521
+
522
+ const mainMissingNode = createMockNode (
523
+ 'MainMissing' ,
524
+ 'comfy-core' ,
525
+ '1.0.0'
526
+ )
527
+ const subgraphMissingNode = createMockNode (
528
+ 'SubgraphMissing' ,
529
+ 'comfy-core' ,
530
+ '1.1.0'
531
+ )
532
+ const subgraphRegisteredNode = createMockNode (
533
+ 'SubgraphRegistered' ,
534
+ 'comfy-core' ,
535
+ '1.0.0'
536
+ )
537
+
538
+ const mockSubgraph = {
539
+ nodes : [ subgraphMissingNode , subgraphRegisteredNode ]
540
+ }
541
+
542
+ const mockSubgraphNode = {
543
+ isSubgraphNode : ( ) => true ,
544
+ subgraph : mockSubgraph ,
545
+ type : 'SubgraphContainer' ,
546
+ properties : { cnr_id : 'custom-pack' }
547
+ }
548
+
549
+ const mockMainGraph = {
550
+ nodes : [ mainMissingNode , mockSubgraphNode ]
551
+ }
552
+
553
+ // @ts -expect-error - Mocking app.graph for testing
554
+ app . graph = mockMainGraph
555
+
556
+ mockUseNodeDefStore . mockReturnValue ( {
557
+ nodeDefsByName : {
375
558
// @ts -expect-error - Creating minimal mock of ComfyNodeDefImpl for testing.
376
- RegisteredNode2 : { name : 'RegisteredNode2 ' }
559
+ SubgraphRegistered : { name : 'SubgraphRegistered ' }
377
560
}
378
561
} )
379
562
380
563
const { missingCoreNodes } = useMissingNodes ( )
381
564
382
- expect ( Object . keys ( missingCoreNodes . value ) ) . toHaveLength ( 0 )
565
+ expect ( Object . keys ( missingCoreNodes . value ) ) . toHaveLength ( 2 )
566
+ expect ( missingCoreNodes . value [ '1.0.0' ] ) . toHaveLength ( 1 )
567
+ expect ( missingCoreNodes . value [ '1.1.0' ] ) . toHaveLength ( 1 )
568
+ expect ( missingCoreNodes . value [ '1.0.0' ] [ 0 ] . type ) . toBe ( 'MainMissing' )
569
+ expect ( missingCoreNodes . value [ '1.1.0' ] [ 0 ] . type ) . toBe ( 'SubgraphMissing' )
383
570
} )
384
571
} )
385
572
} )
0 commit comments