/** * Z-order + input-state probe (headless browser — NOT a Node test). * * Verifies the two menu fixes against the real engine: * * Z-ORDER (paint order — the scene display list is depth-sorted * ascending, and containers expand their children inline at their * slot, so list order IS back-to-front paint order): * stars(0-2) < planet(5) < ship(10) < HUD(30) < compass(40) * < toast(45) < command deck(50) < menu sub-bar(60) * < save pop-up(70) * ... and the confirm dialog is the pop-up's last content child (only * the toast trails it, and it sits outside the dialog's area), so it * paints on top of the pop-up. * * INPUT STATE (v4 gates hit-testing on `input.enabled`): * sub-bar buttons inert while closed, live while open * pop-up scrim / cards / footer inert while hidden, live while shown * confirm scrim + buttons inert while the dialog is hidden, live * while it is up * * Served by dev/zorder-test.html; results land in `window.__ZORDER__`: * * python3 -m http.server 8091 * node dev/cdp-firefox.mjs http://127.0.0.1:8091/dev/zorder-test.html */ import Phaser from '../js/vendor/phaser.js'; import { config } from '../js/config/Config.js'; import { ConfigLoader } from '../js/config/ConfigLoader.js'; import { createGameConfig } from '../js/config/GameConfig.js'; import { GameScene } from '../js/scenes/GameScene.js'; const data = await ConfigLoader.load(); config.init(data); globalThis.__ORBIT_DEV_SEED = 'ZORDER'; const gameConfig = createGameConfig(); gameConfig.scene = [GameScene]; if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true; const game = new Phaser.Game(gameConfig); window.game = game; const results = []; const check = (label, cond) => { const pass = !!cond; results.push({ label, pass }); console.log(`${pass ? '✔' : '✘ FAIL'} ${label}`); }; // Throttling-proof wait on the game's own clock (see clickout-test). const gameClock = () => { try { const s = window.game.scene.getScenes(true)[0]; if (s && typeof s.time.now === 'number') return s.time.now; } catch {} return null; }; const wait = (ms) => new Promise((resolve) => { const base = gameClock(); if (base === null) { setTimeout(() => resolve(), ms); return; } const poll = () => { const now = gameClock(); if (now !== null && now - base >= ms) return resolve(); requestAnimationFrame(poll); }; requestAnimationFrame(poll); }); const run = async () => { await wait(900); // boot const scene = game.scene.getScene('GameScene'); check('boots into GameScene', game.scene.isActive('GameScene')); // ---- declared depths --------------------------------------------------- check('planets sit at depth 5', scene.planet.depth === 5); check('the ship sits at depth 10', scene.ship.depth === 10); check('the command deck sits at depth 50', scene.actionBar.depth === 50); check('the menu sub-bar sits at depth 60', scene.menuSubBar.depth === 60); check('the save pop-up sits at depth 70', scene.savePanel.depth === 70); // ---- paint order: the scene display list ------------------------------- const dl = scene.sys.displayList.getChildren(); const idx = (o) => dl.indexOf(o); const iPlanet = idx(scene.planet); const iShip = idx(scene.ship); const iCompass = idx(scene.compass); const iDeck = idx(scene.actionBar); const iSubBar = idx(scene.menuSubBar); const iPanel = idx(scene.savePanel); const name = (o) => o === scene.planet ? 'planet' : o === scene.ship ? 'ship' : o === scene.compass ? 'compass' : o === scene.actionBar ? 'actionBar' : o === scene.menuSubBar ? 'menuSubBar' : o === scene.savePanel ? 'savePanel' : (o.type || 'obj') + '@' + (o._depth !== undefined ? o._depth : o.depth); console.log('displayList tail:', dl.slice(-8).map(name).join(' < ')); check('every probed object is in the display list', [iPlanet, iShip, iCompass, iDeck, iSubBar, iPanel].every((i) => i >= 0)); results.push({ label: `DL planet=${iPlanet} ship=${iShip} compass=${iCompass} actionBar=${iDeck} menuSubBar=${iSubBar} savePanel=${iPanel} len=${dl.length}`, pass: true }); check('the display list is depth-sorted (ascending)', dl.slice(-20).every((o, i, a) => i === 0 || o._depth >= a[i - 1]._depth)); check('world (planet, ship) paints UNDER the command deck', iPlanet < iDeck && iShip < iDeck); check('the command deck paints UNDER the sub-bar', iDeck < iSubBar); check('the sub-bar paints UNDER the save pop-up', iSubBar < iPanel); const kids = (() => { const p = scene.savePanel; if (p.list && Array.isArray(p.list)) return p.list; // v4 Container children live in `list` const c = p.children; if (!c) return null; if (Array.isArray(c)) return c; if (c.list && Array.isArray(c.list)) return c.list; if (typeof c.getChildren === 'function') return c.getChildren(); return null; })(); check('the confirm dialog paints above the panel contents (only the toast trails it)', (() => { if (!Array.isArray(kids) || kids.length === 0) return false; const ci = kids.indexOf(scene.savePanel.confirm); return ci >= 0 && kids.slice(ci + 1).every((c) => c === scene.savePanel.toast); })()); // ---- input states (v4: input.enabled) ---------------------------------- const enabled = (o) => !!(o.input && o.input.enabled); check('sub-bar buttons are input-INERT while the bar is closed', scene.menuSubBar.buttons.every((s) => !enabled(s.btn.panel))); check('pop-up scrim/cards/footer are input-INERT while hidden', !enabled(scene.savePanel.scrim) && scene.savePanel.cards.every((c) => !enabled(c.panel)) && !enabled(scene.savePanel.cancelBtn.panel) && !enabled(scene.savePanel.downloadBtn.panel)); check('confirm scrim + buttons are input-INERT while the dialog is hidden', !enabled(scene.savePanel.confirm.scrim) && !enabled(scene.savePanel.confirm.confirmBtn.panel) && !enabled(scene.savePanel.confirm.cancelBtn.panel)); scene.menuAction(); // open the sub-bar await wait(450); check('sub-bar buttons are input-LIVE while the bar is open', scene.menuSubBar.isOpen && scene.menuSubBar.buttons.every((s) => enabled(s.btn.panel))); scene.subBarAction('save'); // open the pop-up (save mode) await wait(450); check('pop-up scrim + cards + footer are input-LIVE while shown', scene.savePanel.isOpen && enabled(scene.savePanel.scrim) && scene.savePanel.cards.every((c) => enabled(c.panel)) && enabled(scene.savePanel.cancelBtn.panel) && enabled(scene.savePanel.downloadBtn.panel)); scene.savePanel.confirmOverwrite(1, { galaxyName: 'X', savedAt: new Date().toISOString() }); await wait(300); check('confirm scrim + buttons are input-LIVE while the dialog is up', scene.savePanel.confirm.isOpen && enabled(scene.savePanel.confirm.scrim) && enabled(scene.savePanel.confirm.confirmBtn.panel) && enabled(scene.savePanel.confirm.cancelBtn.panel)); scene.savePanel.confirm.cancel(); await wait(350); check('confirm goes inert again after CANCEL', scene.savePanel.confirm.isOpen === false && !enabled(scene.savePanel.confirm.scrim) && !enabled(scene.savePanel.confirm.confirmBtn.panel)); scene.savePanel.close(); await wait(400); scene.menuSubBar.close(); await wait(400); check('sub-bar goes inert again after close', scene.menuSubBar.state === 'closed' && scene.menuSubBar.buttons.every((s) => !enabled(s.btn.panel))); check('pop-up goes inert again after close', scene.savePanel.isOpen === false && !enabled(scene.savePanel.scrim) && scene.savePanel.cards.every((c) => !enabled(c.panel))); }; let done = false; game.events.once('ready', async () => { try { await run(); } catch (err) { results.push({ label: `THREW: ${String(err && err.message || err)}`, pass: false }); } const pass = results.length > 0 && results.every((r) => r.pass); window.__ZORDER__ = { pass, results, errors: window.__CAPTURED_ERRORS__ || [] }; done = true; console.log(pass ? 'ZORDER PASS' : 'ZORDER FAIL'); }); setTimeout(() => { if (!done) window.__ZORDER__ = { pass: false, results: [...results, { label: 'TIMED OUT (300s)', pass: false }] }; }, 300000);