orbit/dev/zorder-test.mjs

191 lines
8.1 KiB
JavaScript

/**
* 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);