@@ -7,6 +7,7 @@ import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
77import { app } from '@/scripts/app'
88import { useComfyManagerStore } from '@/stores/comfyManagerStore'
99import { useNodeDefStore } from '@/stores/nodeDefStore'
10+ import { collectAllNodes } from '@/utils/graphTraversalUtil'
1011
1112// Mock Vue's onMounted to execute immediately for testing
1213vi . mock ( 'vue' , async ( ) => {
@@ -38,9 +39,14 @@ vi.mock('@/scripts/app', () => ({
3839 }
3940} ) )
4041
42+ vi . mock ( '@/utils/graphTraversalUtil' , ( ) => ( {
43+ collectAllNodes : vi . fn ( )
44+ } ) )
45+
4146const mockUseWorkflowPacks = vi . mocked ( useWorkflowPacks )
4247const mockUseComfyManagerStore = vi . mocked ( useComfyManagerStore )
4348const mockUseNodeDefStore = vi . mocked ( useNodeDefStore )
49+ const mockCollectAllNodes = vi . mocked ( collectAllNodes )
4450
4551describe ( 'useMissingNodes' , ( ) => {
4652 const mockWorkflowPacks = [
@@ -95,6 +101,9 @@ describe('useMissingNodes', () => {
95101 // Reset app.graph.nodes
96102 // @ts -expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
97103 app . graph . nodes = [ ]
104+
105+ // Default mock for collectAllNodes - returns empty array
106+ mockCollectAllNodes . mockReturnValue ( [ ] )
98107 } )
99108
100109 describe ( 'core filtering logic' , ( ) => {
@@ -286,14 +295,9 @@ describe('useMissingNodes', () => {
286295 it ( 'identifies missing core nodes not in nodeDefStore' , ( ) => {
287296 const coreNode1 = createMockNode ( 'CoreNode1' , 'comfy-core' , '1.2.0' )
288297 const coreNode2 = createMockNode ( 'CoreNode2' , 'comfy-core' , '1.2.0' )
289- const registeredNode = createMockNode (
290- 'RegisteredNode' ,
291- 'comfy-core' ,
292- '1.0.0'
293- )
294298
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 ] )
297301
298302 mockUseNodeDefStore . mockReturnValue ( {
299303 nodeDefsByName : {
@@ -316,8 +320,8 @@ describe('useMissingNodes', () => {
316320 const node130 = createMockNode ( 'Node130' , 'comfy-core' , '1.3.0' )
317321 const nodeNoVer = createMockNode ( 'NodeNoVer' , 'comfy-core' )
318322
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 ] )
321325
322326 // @ts -expect-error - Mocking partial NodeDefStore for testing.
323327 mockUseNodeDefStore . mockReturnValue ( {
@@ -334,11 +338,9 @@ describe('useMissingNodes', () => {
334338
335339 it ( 'ignores non-core nodes' , ( ) => {
336340 const coreNode = createMockNode ( 'CoreNode' , 'comfy-core' , '1.2.0' )
337- const customNode = createMockNode ( 'CustomNode' , 'custom-pack' , '1.0.0' )
338- const noPackNode = createMockNode ( 'NoPackNode' )
339341
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 ] )
342344
343345 // @ts -expect-error - Mocking partial NodeDefStore for testing.
344346 mockUseNodeDefStore . mockReturnValue ( {
@@ -353,33 +355,218 @@ describe('useMissingNodes', () => {
353355 } )
354356
355357 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' ,
358403 'comfy-core' ,
359404 '1.0.0'
360405 )
361- const registeredNode2 = createMockNode (
362- 'RegisteredNode2 ' ,
406+ const subgraphNode2 = createMockNode (
407+ 'SubgraphNode2 ' ,
363408 'comfy-core' ,
364409 '1.1.0'
365410 )
366411
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
369460
370461 mockUseNodeDefStore . mockReturnValue ( {
371462 nodeDefsByName : {
372463 // @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 : {
375558 // @ts -expect-error - Creating minimal mock of ComfyNodeDefImpl for testing.
376- RegisteredNode2 : { name : 'RegisteredNode2 ' }
559+ SubgraphRegistered : { name : 'SubgraphRegistered ' }
377560 }
378561 } )
379562
380563 const { missingCoreNodes } = useMissingNodes ( )
381564
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' )
383570 } )
384571 } )
385572} )
0 commit comments