@@ -316,6 +316,165 @@ test('SettingsTourFlow owns linear day four gaze follow scene body', async () =>
316316 ] ) ;
317317} ) ;
318318
319+ test ( 'SettingsTourFlow refreshes visible day five character panel before panic narration' , async ( ) => {
320+ const calls = [ ] ;
321+ const characterSettingsPanel = { id : 'character-settings-panel' } ;
322+ const characterSettingsButton = { id : 'character-settings-button' } ;
323+ const director = {
324+ sceneRunId : 17 ,
325+ destroyed : false ,
326+ angryExitTriggered : false ,
327+ currentStep : 'day5-panic' ,
328+ overlay : {
329+ clearActionSpotlight ( ) {
330+ calls . push ( [ 'clear-action' ] ) ;
331+ } ,
332+ clearPersistentSpotlight ( ) {
333+ calls . push ( [ 'clear-persistent' ] ) ;
334+ }
335+ } ,
336+ prepareNarration ( scene ) {
337+ calls . push ( [ 'prepare' , scene . id ] ) ;
338+ return { text : 'line' , voiceKey : 'voice' , canHandleSceneButtons : false , actionWaitPromise : null } ;
339+ } ,
340+ getCharacterSettingsSidePanel ( ) {
341+ calls . push ( [ 'get-panel' ] ) ;
342+ return characterSettingsPanel ;
343+ } ,
344+ isElementVisible ( panel ) {
345+ calls . push ( [ 'visible' , panel . id ] ) ;
346+ return true ;
347+ } ,
348+ ensureAvatarFloatingSettingsSidePanel ( panelId ) {
349+ calls . push ( [ 'ensure-panel' , panelId ] ) ;
350+ return Promise . resolve ( characterSettingsPanel ) ;
351+ } ,
352+ getDay5CharacterSettingsButtonTarget ( ) {
353+ calls . push ( [ 'get-button' ] ) ;
354+ return characterSettingsButton ;
355+ } ,
356+ refreshAvatarFloatingSettingsPanelLayout ( panel ) {
357+ calls . push ( [ 'refresh-layout' , panel . id ] ) ;
358+ } ,
359+ applyGuideHighlights ( config ) {
360+ calls . push ( [ 'highlight' , config . key , config . primary . id , config . persistent . id ] ) ;
361+ } ,
362+ moveCursorToElement ( element , durationMs , options ) {
363+ const normalizedOptions = options || { } ;
364+ calls . push ( [ 'move' , element . id , durationMs , normalizedOptions . exactDuration ] ) ;
365+ return Promise . resolve ( true ) ;
366+ } ,
367+ enableInterrupts ( step ) {
368+ calls . push ( [ 'interrupts' , step ] ) ;
369+ } ,
370+ createNarrationPromise ( scene , text , voiceKey ) {
371+ calls . push ( [ 'narration' , scene . id , text , voiceKey ] ) ;
372+ return Promise . resolve ( ) ;
373+ } ,
374+ getAvatarFloatingNarrationDurationMs ( voiceKey , text ) {
375+ calls . push ( [ 'duration' , voiceKey , text ] ) ;
376+ return 900 ;
377+ } ,
378+ getElementRect ( target ) {
379+ calls . push ( [ 'rect' , target . id ] ) ;
380+ return { left : 10 , top : 20 , width : 100 , height : 200 } ;
381+ } ,
382+ runSettingsPeekPanicPerformance ( options ) {
383+ calls . push ( [ 'panic' , options . targetRect . width , options . totalDurationMs , options . runId ] ) ;
384+ return Promise . resolve ( ) ;
385+ } ,
386+ isStopping ( ) {
387+ return false ;
388+ } ,
389+ collapseCharacterSettingsSidePanel ( ) {
390+ calls . push ( [ 'collapse-character' ] ) ;
391+ } ,
392+ finalizeScene ( sceneRunId , options ) {
393+ calls . push ( [ 'finalize' , sceneRunId , options . index , options . total ] ) ;
394+ return Promise . resolve ( true ) ;
395+ }
396+ } ;
397+ const flow = new SettingsTourFlow ( director ) ;
398+
399+ const result = await flow . play ( { id : 'day5_character_panic' } , {
400+ sceneRunId : 17 ,
401+ index : 2 ,
402+ total : 4
403+ } ) ;
404+
405+ assert . equal ( result , true ) ;
406+ assert . deepEqual ( calls , [
407+ [ 'prepare' , 'day5_character_panic' ] ,
408+ [ 'get-panel' ] ,
409+ [ 'visible' , 'character-settings-panel' ] ,
410+ [ 'get-button' ] ,
411+ [ 'refresh-layout' , 'character-settings-panel' ] ,
412+ [ 'highlight' , 'day5_character_panic-character-settings-panel' , 'character-settings-panel' , 'character-settings-button' ] ,
413+ [ 'move' , 'character-settings-panel' , 0 , true ] ,
414+ [ 'interrupts' , 'day5-panic' ] ,
415+ [ 'narration' , 'day5_character_panic' , 'line' , 'voice' ] ,
416+ [ 'duration' , 'voice' , 'line' ] ,
417+ [ 'rect' , 'character-settings-panel' ] ,
418+ [ 'panic' , 100 , 900 , 17 ] ,
419+ [ 'clear-action' ] ,
420+ [ 'clear-persistent' ] ,
421+ [ 'collapse-character' ] ,
422+ [ 'finalize' , 17 , 2 , 4 ]
423+ ] ) ;
424+ assert . equal ( calls . some ( ( call ) => call [ 0 ] === 'ensure-panel' ) , false ) ;
425+ } ) ;
426+
427+ test ( 'SettingsTourFlow stops day five panic scene after stale async panel ensure' , async ( ) => {
428+ const calls = [ ] ;
429+ const characterSettingsPanel = { id : 'character-settings-panel' } ;
430+ const director = {
431+ sceneRunId : 17 ,
432+ destroyed : false ,
433+ angryExitTriggered : false ,
434+ currentStep : 'day5-panic' ,
435+ prepareNarration ( scene ) {
436+ calls . push ( [ 'prepare' , scene . id ] ) ;
437+ return { text : 'line' , voiceKey : 'voice' , canHandleSceneButtons : false , actionWaitPromise : null } ;
438+ } ,
439+ getCharacterSettingsSidePanel ( ) {
440+ calls . push ( [ 'get-panel' ] ) ;
441+ return null ;
442+ } ,
443+ ensureAvatarFloatingSettingsSidePanel ( panelId ) {
444+ calls . push ( [ 'ensure-panel' , panelId ] ) ;
445+ this . sceneRunId = 18 ;
446+ return Promise . resolve ( characterSettingsPanel ) ;
447+ } ,
448+ getDay5CharacterSettingsButtonTarget ( ) {
449+ calls . push ( [ 'get-button' ] ) ;
450+ return { id : 'character-settings-button' } ;
451+ } ,
452+ refreshAvatarFloatingSettingsPanelLayout ( panel ) {
453+ calls . push ( [ 'refresh-layout' , panel && panel . id ] ) ;
454+ } ,
455+ applyGuideHighlights ( config ) {
456+ calls . push ( [ 'highlight' , config . key ] ) ;
457+ } ,
458+ isStopping ( ) {
459+ return false ;
460+ }
461+ } ;
462+ const flow = new SettingsTourFlow ( director ) ;
463+
464+ const result = await flow . play ( { id : 'day5_character_panic' } , {
465+ sceneRunId : 17 ,
466+ index : 2 ,
467+ total : 4
468+ } ) ;
469+
470+ assert . equal ( result , false ) ;
471+ assert . deepEqual ( calls , [
472+ [ 'prepare' , 'day5_character_panic' ] ,
473+ [ 'get-panel' ] ,
474+ [ 'ensure-panel' , 'character-settings' ]
475+ ] ) ;
476+ } ) ;
477+
319478test ( 'SettingsTourFlow delegates narration and finalize to the director' , async ( ) => {
320479 const calls = [ ] ;
321480 const director = {
0 commit comments