@@ -199,6 +199,12 @@ function testBackgroundGameplayGatingAndOrder() {
199199 assert . equal ( gameplayResult . drawn , true ) ;
200200 assert . equal ( gameplayResult . path , "games/Asteroids/assets/images/background.png" ) ;
201201 assert . deepEqual ( order , [ "background:/games/Asteroids/assets/images/background.png" ] ) ;
202+
203+ order . length = 0 ;
204+ const backToMenuResult = layer . render ( renderer , { scene : { session : { mode : "menu" } } } ) ;
205+ assert . equal ( backToMenuResult . drawn , false ) ;
206+ assert . equal ( backToMenuResult . reason , "non-gameplay-state" ) ;
207+ assert . deepEqual ( order , [ ] ) ;
202208}
203209
204210function testGameImageConventionsAreGameAgnostic ( ) {
@@ -318,6 +324,29 @@ function testNoOpWhenBezelMissing() {
318324 assert . equal ( bezel . element . style . display , "none" ) ;
319325}
320326
327+ function testMalformedBezelImageIsTreatedAsUnavailable ( ) {
328+ const documentRef = createDocumentStub ( "/games/Asteroids/index.html" ) ;
329+ const host = createElement ( "div" , documentRef ) ;
330+ const canvas = createElement ( "canvas" , documentRef ) ;
331+ canvas . width = 960 ;
332+ canvas . height = 720 ;
333+ host . appendChild ( canvas ) ;
334+ documentRef . body . appendChild ( host ) ;
335+
336+ const bezel = new fullscreenBezel ( { canvas, documentRef } ) ;
337+ bezel . attach ( ) ;
338+ bezel . element . naturalWidth = 0 ;
339+ bezel . element . naturalHeight = 0 ;
340+ bezel . element . width = 0 ;
341+ bezel . element . height = 0 ;
342+ bezel . element . onload ?. ( ) ;
343+
344+ const result = bezel . sync ( { fullscreenActive : true , fullscreenElement : host } ) ;
345+ assert . equal ( result . visible , false ) ;
346+ assert . equal ( bezel . getState ( ) . visible , false ) ;
347+ assert . equal ( bezel . getState ( ) . canvasLayoutMode , "fallback" ) ;
348+ }
349+
321350function testTransparentWindowDetectionAndAspectFit ( ) {
322351 const width = 6 ;
323352 const height = 5 ;
@@ -461,6 +490,100 @@ function testFullscreenBezelSharedStretchAffectsAllSides() {
461490 assertNear ( stretchedCenterY , baselineCenterY , 0.51 ) ;
462491}
463492
493+ function testFullscreenBezelCyclesAndResizeKeepLayoutStable ( ) {
494+ const documentRef = createDocumentStub ( "/games/Asteroids/index.html" ) ;
495+ const host = createElement ( "div" , documentRef ) ;
496+ host . clientWidth = 1600 ;
497+ host . clientHeight = 900 ;
498+ host . offsetWidth = 1600 ;
499+ host . offsetHeight = 900 ;
500+ const canvas = createElement ( "canvas" , documentRef ) ;
501+ canvas . width = 960 ;
502+ canvas . height = 720 ;
503+ host . appendChild ( canvas ) ;
504+ documentRef . body . appendChild ( host ) ;
505+
506+ const bezel = new fullscreenBezel ( {
507+ canvas,
508+ documentRef,
509+ alphaInspector ( ) {
510+ return { x : 460 , y : 220 , width : 1000 , height : 500 } ;
511+ }
512+ } ) ;
513+ bezel . attach ( ) ;
514+ bezel . element . naturalWidth = 1920 ;
515+ bezel . element . naturalHeight = 1080 ;
516+ bezel . element . onload ?. ( ) ;
517+
518+ let result = bezel . sync ( { fullscreenActive : true , fullscreenElement : host } ) ;
519+ assert . equal ( result . visible , true ) ;
520+ assert . equal ( result . canvasLayoutMode , "transparent-window-fit" ) ;
521+ const firstWidth = parseFloat ( canvas . style . width ) ;
522+ const firstHeight = parseFloat ( canvas . style . height ) ;
523+
524+ result = bezel . sync ( { fullscreenActive : false , fullscreenElement : host } ) ;
525+ assert . equal ( result . visible , false ) ;
526+ assert . equal ( result . canvasLayoutMode , "fallback" ) ;
527+ assert . equal ( canvas . style . width , "960px" ) ;
528+ assert . equal ( canvas . style . height , "720px" ) ;
529+
530+ host . clientWidth = 1920 ;
531+ host . clientHeight = 1080 ;
532+ host . offsetWidth = 1920 ;
533+ host . offsetHeight = 1080 ;
534+ result = bezel . sync ( { fullscreenActive : true , fullscreenElement : host } ) ;
535+ assert . equal ( result . visible , true ) ;
536+ assert . equal ( result . canvasLayoutMode , "transparent-window-fit" ) ;
537+ const resizedWidth = parseFloat ( canvas . style . width ) ;
538+ const resizedHeight = parseFloat ( canvas . style . height ) ;
539+ assert . equal ( resizedWidth > firstWidth , true ) ;
540+ assert . equal ( resizedHeight > firstHeight , true ) ;
541+ }
542+
543+ function testMalformedAndExtremeStretchConfigValuesAreSafe ( ) {
544+ const documentRef = createDocumentStub ( "/games/Asteroids/index.html" ) ;
545+ const host = createElement ( "div" , documentRef ) ;
546+ host . clientWidth = 1600 ;
547+ host . clientHeight = 900 ;
548+ host . offsetWidth = 1600 ;
549+ host . offsetHeight = 900 ;
550+ const canvas = createElement ( "canvas" , documentRef ) ;
551+ canvas . width = 960 ;
552+ canvas . height = 720 ;
553+ host . appendChild ( canvas ) ;
554+ documentRef . body . appendChild ( host ) ;
555+
556+ const malformed = new fullscreenBezel ( {
557+ canvas,
558+ documentRef,
559+ alphaInspector : ( ) => ( { x : 460 , y : 220 , width : 1000 , height : 500 } ) ,
560+ stretchConfigProvider : ( ) => ( { uniformEdgeStretchPx : "abc" } )
561+ } ) ;
562+ malformed . attach ( ) ;
563+ malformed . element . naturalWidth = 1920 ;
564+ malformed . element . naturalHeight = 1080 ;
565+ malformed . element . onload ?. ( ) ;
566+ malformed . sync ( { fullscreenActive : true , fullscreenElement : host } ) ;
567+ assert . equal ( malformed . getState ( ) . uniformEdgeStretchPx , 0 ) ;
568+ malformed . detach ( ) ;
569+
570+ const extreme = new fullscreenBezel ( {
571+ canvas,
572+ documentRef,
573+ alphaInspector : ( ) => ( { x : 460 , y : 220 , width : 1000 , height : 500 } ) ,
574+ stretchConfigProvider : ( ) => ( { uniformEdgeStretchPx : Number . MAX_SAFE_INTEGER } )
575+ } ) ;
576+ extreme . attach ( ) ;
577+ extreme . element . naturalWidth = 1920 ;
578+ extreme . element . naturalHeight = 1080 ;
579+ extreme . element . onload ?. ( ) ;
580+ const result = extreme . sync ( { fullscreenActive : true , fullscreenElement : host } ) ;
581+ assert . equal ( result . visible , true ) ;
582+ assert . equal ( extreme . getState ( ) . uniformEdgeStretchPx > 0 , true ) ;
583+ assert . equal ( parseFloat ( canvas . style . width ) <= 1600.01 , true ) ;
584+ assert . equal ( parseFloat ( canvas . style . height ) <= 900.01 , true ) ;
585+ }
586+
464587async function testBezelStretchConfigAutoCreate ( ) {
465588 const tempRoot = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "bezel-stretch-config-" ) ) ;
466589 const configPath = "games/Asteroids/assets/images/bezel.stretch.override.json" ;
@@ -581,6 +704,27 @@ async function testBezelDetectionDoesNotOverwriteExistingStretchConfig() {
581704 }
582705}
583706
707+ async function testMalformedStretchConfigFileFallsBackWithoutOverwrite ( ) {
708+ const tempRoot = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "bezel-malformed-config-" ) ) ;
709+ const configPath = path . resolve ( tempRoot , "games/Asteroids/assets/images/bezel.stretch.override.json" ) ;
710+ try {
711+ await fs . mkdir ( path . dirname ( configPath ) , { recursive : true } ) ;
712+ const malformedContent = "{not-valid-json" ;
713+ await fs . writeFile ( configPath , malformedContent , "utf8" ) ;
714+
715+ const loaded = await ensureBezelStretchConfigFile ( "games/Asteroids/assets/images/bezel.stretch.override.json" , {
716+ cwd : tempRoot ,
717+ fsModule : fs ,
718+ pathModule : path
719+ } ) ;
720+ const savedAfterLoad = await fs . readFile ( configPath , "utf8" ) ;
721+ assert . deepEqual ( loaded , { uniformEdgeStretchPx : 0 } ) ;
722+ assert . equal ( savedAfterLoad , malformedContent ) ;
723+ } finally {
724+ await fs . rm ( tempRoot , { recursive : true , force : true } ) ;
725+ }
726+ }
727+
584728async function testSampleGameBezelDetectionCreatesStretchConfig ( ) {
585729 const tempRoot = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "bezel-detected-spaceinvaders-config-" ) ) ;
586730 const documentRef = createDocumentStub ( "/games/SpaceInvaders/index.html" ) ;
@@ -761,12 +905,16 @@ export async function run() {
761905 testSampleGameBackgroundAndBezelNoOpWhenMissing ( ) ;
762906 testFullscreenBezelVisibilityAndHtmlAttachment ( ) ;
763907 testNoOpWhenBezelMissing ( ) ;
908+ testMalformedBezelImageIsTreatedAsUnavailable ( ) ;
764909 testTransparentWindowDetectionAndAspectFit ( ) ;
765910 testFullscreenBezelTransparentWindowCanvasFit ( ) ;
766911 testFullscreenBezelSharedStretchAffectsAllSides ( ) ;
912+ testFullscreenBezelCyclesAndResizeKeepLayoutStable ( ) ;
913+ testMalformedAndExtremeStretchConfigValuesAreSafe ( ) ;
767914 await testBezelStretchConfigAutoCreate ( ) ;
768915 await testBezelDetectionTriggersStretchConfigAutoCreate ( ) ;
769916 await testBezelDetectionDoesNotOverwriteExistingStretchConfig ( ) ;
917+ await testMalformedStretchConfigFileFallsBackWithoutOverwrite ( ) ;
770918 await testSampleGameBezelDetectionCreatesStretchConfig ( ) ;
771919 testEngineRuntimeIntegration ( ) ;
772920}
0 commit comments