@@ -4,6 +4,7 @@ import { createRoot, type Root } from 'react-dom/client';
44import { afterEach , beforeEach , expect , it , vi } from 'vitest' ;
55import GalaxyPanel , { type GalaxyPanelProps } from './GalaxyPanel' ;
66import type { GraphData , GraphNode } from './types' ;
7+ import type { OrganicLayoutRequest , OrganicLayoutResponse } from './organic-layout.worker' ;
78
89/** The layout can be capped; focused relationships come independently from RPC. */
910function graphFetch ( nodes : GraphNode [ ] , edges : GraphData [ 'edges' ] , total = nodes . length ) {
@@ -102,10 +103,24 @@ it('opens a file hierarchy without a caret symbol and follows literal marked ran
102103 expect ( fetchLayout . mock . calls . filter ( ( [ url ] ) => String ( url ) . includes ( '/api/layout' ) ) ) . toHaveLength ( 1 ) ;
103104} ) ;
104105
105- it ( 'does not report a file missing from a capped layout while its indexed scope is loading' , async ( ) => {
106+ it . each ( [ true , false ] ) ( 'does not report a file missing while its scope or layout is loading (indexed: %s)' , async indexed => {
107+ let worker ! : LayoutWorker ;
108+ class LayoutWorker {
109+ onmessage : ( ( event : MessageEvent < OrganicLayoutResponse > ) => void ) | null = null ;
110+ requests : OrganicLayoutRequest [ ] = [ ] ;
111+ constructor ( ) { worker = this ; }
112+ postMessage ( request : OrganicLayoutRequest ) { this . requests . push ( request ) ; }
113+ terminate ( ) { }
114+ reply ( ) {
115+ const { sequence, data } = this . requests . at ( - 1 ) ! ;
116+ this . onmessage ?.( new MessageEvent < OrganicLayoutResponse > ( 'message' , { data : { sequence,
117+ result : { data, groups : [ ] , groupByNode : new Map ( ) } } } ) ) ;
118+ }
119+ }
120+ vi . stubGlobal ( 'Worker' , LayoutWorker ) ;
106121 const node : GraphNode = { id : 15 , name : 'outside' , qualified_name : 'sample.outside' , label : 'Function' ,
107122 file_path : 'src/a.ts' , start_line : 1 , end_line : 10 , x : 0 , y : 0 , z : 0 , size : 1 , color : '#999999' } ;
108- const source = graphFetch ( [ node ] , [ ] ) ;
123+ const source = graphFetch ( indexed ? [ node ] : [ ] , [ ] ) ;
109124 let release ! : ( ) => void ;
110125 const pending = new Promise < void > ( resolve => { release = resolve ; } ) ;
111126 const fetchImpl = vi . fn ( async ( url : RequestInfo | URL , init ?: RequestInit ) => {
@@ -114,15 +129,27 @@ it('does not report a file missing from a capped layout while its indexed scope
114129 return source ( url , init ) ;
115130 } ) ;
116131 await act ( async ( ) => root . render ( < GalaxyPanel project = "sample" visible focusFilePath = "src/a.ts" onOpenNode = { vi . fn ( ) } fetch = { fetchImpl } /> ) ) ;
117- // Hand test K8: the status names the layer that is loading instead of an endless "Loading relationships".
118- // It may first say "Checking index…" while the index generation is read; wait for the load itself.
119- await vi . waitFor ( ( ) => expect ( host . textContent ) . toContain ( 'Loading layer 1…' ) ) ;
132+ expect ( host . textContent ) . toContain ( 'Loading layer 1…' ) ;
133+ // Finish the empty capped-layout preview while the indexed scope is held.
134+ // This leaves a missing-file note in state, hidden until the scope finishes.
135+ expect ( worker . requests ) . toHaveLength ( 1 ) ;
136+ expect ( worker . requests [ 0 ] ! . data . nodes ) . toEqual ( [ ] ) ;
137+ await act ( async ( ) => worker . reply ( ) ) ;
120138 expect ( host . querySelector ( '[data-testid="atlas-galaxy-note"]' ) ) . toBeNull ( ) ;
121139 await act ( async ( ) => { release ( ) ; } ) ;
122140 expect ( host . textContent ) . toContain ( 'All indexed direct dependencies included.' ) ;
141+ // The scope is complete, but its new picture has not been arranged yet.
142+ expect ( worker . requests ) . toHaveLength ( 2 ) ;
143+ expect ( host . querySelector ( '[role="status"][data-state="arranging"]' ) ) . not . toBeNull ( ) ;
144+ expect ( host . querySelector ( '[data-testid="atlas-galaxy-note"]' ) ) . toBeNull ( ) ;
123145 expect ( host . textContent ) . not . toContain ( 'not in the loaded graph' ) ;
146+ await act ( async ( ) => worker . reply ( ) ) ;
147+ expect ( host . querySelector ( '[role="status"][data-state="complete"]' ) ) . not . toBeNull ( ) ;
148+ expect ( JSON . parse ( host . querySelector ( '[data-testid="graph-selection"]' ) ! . textContent ! ) ) . toEqual ( indexed ? [ 15 ] : [ ] ) ;
149+ if ( indexed ) expect ( host . querySelector ( '[data-testid="atlas-galaxy-note"]' ) ) . toBeNull ( ) ;
150+ else expect ( host . querySelector ( '[data-testid="atlas-galaxy-note"]' ) ?. textContent ) . toBe ( 'src/a.ts is not in the loaded graph layout.' ) ;
124151} ) ;
125- afterEach ( async ( ) => { await act ( async ( ) => root . unmount ( ) ) ; host . remove ( ) ; globalThis . __atlasGalaxy = undefined ; } ) ;
152+ afterEach ( async ( ) => { await act ( async ( ) => root . unmount ( ) ) ; host . remove ( ) ; globalThis . __atlasGalaxy = undefined ; vi . unstubAllGlobals ( ) ; } ) ;
126153
127154it ( 'selects the whole file, narrows only to marked code, and restores the file when the mark clears' , async ( ) => {
128155 const common = { file_path : 'src/a.ts' , x : 0 , y : 0 , z : 0 , size : 1 , color : '#999999' } ;
0 commit comments