@@ -11,12 +11,16 @@ import type { AssetResponse } from '@/platform/assets/schemas/assetSchema'
1111import type { RemoteConfig } from '@/platform/remoteConfig/types'
1212import type { BillingStatusResponse } from '@/platform/workspace/api/workspaceApi'
1313
14- import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
14+ import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
1515import { ExecutionHelper } from '@e2e/fixtures/helpers/ExecutionHelper'
16+ import { onboardingFixture } from '@e2e/fixtures/tourFixture'
17+ import type { Position } from '@e2e/fixtures/types'
1618import { mockBilling } from '@e2e/fixtures/utils/cloudBillingMocks'
1719import { jsonRoute } from '@e2e/fixtures/utils/jsonRoute'
20+ import { VueNodeFixture } from '@e2e/fixtures/utils/vueNodeFixtures'
1821import { webSocketFixture } from '@e2e/fixtures/ws'
1922
23+ const test = mergeTests ( comfyPageFixture , onboardingFixture )
2024const wstest = mergeTests ( test , webSocketFixture )
2125
2226const { firstRun } = enMessages . onboardingCoachmarks
@@ -113,6 +117,82 @@ async function firstPinnedTemplateOnScreen(
113117 return templateId !
114118}
115119
120+ const DRAG_BY = { x : 120 , y : 80 }
121+
122+ /** The node the spotlight is framing, by the id on its Vue element. */
123+ async function spotlitNodeId ( page : Page , spotlight : Locator ) : Promise < string > {
124+ let nodeId : string | null = null
125+
126+ await expect
127+ . poll (
128+ async ( ) => {
129+ const box = await spotlight . boundingBox ( )
130+ if ( ! box ) return null
131+
132+ nodeId = await page . evaluate (
133+ ( { x, y } ) =>
134+ document
135+ . elementsFromPoint ( x , y )
136+ . map ( ( element ) => element . closest ( '[data-node-id]' ) )
137+ . find ( Boolean )
138+ ?. getAttribute ( 'data-node-id' ) ?? null ,
139+ { x : box . x + box . width / 2 , y : box . y + box . height / 2 }
140+ )
141+ return nodeId
142+ } ,
143+ { message : 'the spotlight never framed a Vue node' }
144+ )
145+ . not . toBeNull ( )
146+
147+ return nodeId !
148+ }
149+
150+ /** Waits until an element's position stops changing between frames. */
151+ async function settled ( element : Locator ) {
152+ let previous : number | null = null
153+
154+ await expect
155+ . poll (
156+ async ( ) => {
157+ const box = await element . boundingBox ( )
158+ const stable = box !== null && box . x === previous
159+ previous = box ?. x ?? null
160+ return stable
161+ } ,
162+ { message : 'the node never stopped moving' }
163+ )
164+ . toBe ( true )
165+ }
166+
167+ /** Where the spotlight sits relative to the node it frames. */
168+ async function framing ( node : Locator , spotlight : Locator ) {
169+ let seen : { node : Position ; offset : Position } | null = null
170+
171+ await expect
172+ . poll (
173+ async ( ) => {
174+ const [ nodeBox , spotlightBox ] = await Promise . all ( [
175+ node . boundingBox ( ) ,
176+ spotlight . boundingBox ( )
177+ ] )
178+ if ( ! nodeBox || ! spotlightBox ) return null
179+
180+ seen = {
181+ node : { x : nodeBox . x , y : nodeBox . y } ,
182+ offset : {
183+ x : spotlightBox . x - nodeBox . x ,
184+ y : spotlightBox . y - nodeBox . y
185+ }
186+ }
187+ return seen
188+ } ,
189+ { message : 'the node and its spotlight never both had layout' }
190+ )
191+ . not . toBeNull ( )
192+
193+ return seen !
194+ }
195+
116196/**
117197 * The walk the review asked for: a fresh user reaches Getting Started, picks a
118198 * template, and the tour guides them through to a result.
@@ -311,6 +391,73 @@ test.describe('First-run tour', { tag: ['@cloud', '@ui'] }, () => {
311391 await expect ( page . getByTestId ( 'coach-spotlight' ) ) . toBeVisible ( )
312392 await expect ( page . getByTestId ( 'coach-card' ) ) . toContainText ( 'Step 1 of' )
313393 } )
394+
395+ /**
396+ * The tour holds a node's layout ref for the whole tour, while the node's
397+ * own component holds the same ref only while it is mounted. Whatever
398+ * unmounts the node -- here a renderer toggle -- must not leave the
399+ * spotlight watching a ref the store has stopped notifying, or the
400+ * highlight sits on empty canvas while the user drags the node it is
401+ * meant to be pointing at.
402+ */
403+ test ( 'keeps the spotlight on its node after the node remounts' , async ( {
404+ comfyPage,
405+ comfyMouse,
406+ onboarding
407+ } ) => {
408+ const { page } = comfyPage
409+ await expect ( onboarding . spotlight ) . toBeVisible ( )
410+
411+ const nodeId = await spotlitNodeId ( page , onboarding . spotlight )
412+ const node = new VueNodeFixture ( comfyPage . vueNodes . getNodeLocator ( nodeId ) )
413+
414+ // A pan moves every node, so a second node tells a drag from a pan.
415+ const otherId = (
416+ await comfyPage . vueNodes . nodes . evaluateAll ( ( nodes ) =>
417+ nodes . map ( ( node ) => node . getAttribute ( 'data-node-id' ) ?? '' )
418+ )
419+ ) . find ( ( id ) => id && id !== nodeId )
420+ expect (
421+ otherId ,
422+ 'a tour pins a source and a sink, so its graph has more than one node'
423+ ) . toBeDefined ( )
424+ const other = comfyPage . vueNodes . getNodeLocator ( otherId ! )
425+
426+ // The node component unmounts and comes back; the tour never let go.
427+ await comfyPage . settings . setSetting ( 'Comfy.VueNodes.Enabled' , false )
428+ await expect ( comfyPage . vueNodes . nodes ) . toHaveCount ( 0 )
429+ await comfyPage . settings . setSetting ( 'Comfy.VueNodes.Enabled' , true )
430+ await comfyPage . vueNodes . waitForNodes ( )
431+ await expect ( onboarding . spotlight ) . toBeVisible ( )
432+ await settled ( node . root )
433+
434+ const before = await framing ( node . root , onboarding . spotlight )
435+ const otherBefore = await framing ( other , onboarding . spotlight )
436+
437+ await comfyMouse . dragElementBy ( node . header , DRAG_BY )
438+
439+ await expect
440+ . poll ( async ( ) => ( await node . root . boundingBox ( ) ) ?. x , {
441+ message : 'the drag never moved the node, so it proves nothing'
442+ } )
443+ . not . toBe ( before . node . x )
444+
445+ const after = await framing ( node . root , onboarding . spotlight )
446+ const otherAfter = await framing ( other , onboarding . spotlight )
447+
448+ expect (
449+ otherAfter . node ,
450+ 'the untouched node moved too, so this panned the canvas'
451+ ) . toEqual ( otherBefore . node )
452+ expect (
453+ after . offset . x ,
454+ 'the spotlight stopped following its node horizontally'
455+ ) . toBeCloseTo ( before . offset . x , 0 )
456+ expect (
457+ after . offset . y ,
458+ 'the spotlight stopped following its node vertically'
459+ ) . toBeCloseTo ( before . offset . y , 0 )
460+ } )
314461 } )
315462
316463 test . describe ( 'arriving on a link that loads nothing' , ( ) => {
0 commit comments