orbit/dev/clickout-test.mjs

191 lines
7.3 KiB
JavaScript

/**
* Click-outside the sub-menu (headless browser — NOT a Node test).
*
* Drives the REAL game's input pipeline (mousedown on the canvas) and
* checks the dismiss rule in GameScene's pointerdown handler:
*
* sub-bar OPEN + click OUTSIDE it (world) → closes, no fly-here
* sub-bar OPEN + click on its own panel → stays open
* sub-bar OPEN + a sub-bar button (Save Game) → the button still fires
* sub-bar OPEN + click on the MENU deck button → closes, no re-open
* sub-bar OPEN + compass autopilot seam → closes, no navigation
* after any of those, MENU still re-opens it
*
* Served by dev/clickout-test.html; the results land in
* `window.__CLICKOUT__` for the CDP runner (dev/cdp-firefox.mjs):
*
* python3 -m http.server 8080
* node dev/cdp-firefox.mjs http://localhost:8080/dev/clickout-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);
// A deterministic galaxy (same system every run) + a quiet run (no audio
// files to fetch in headless).
globalThis.__ORBIT_DEV_SEED = 'CLICKOUT';
const gameConfig = createGameConfig();
gameConfig.scene = [GameScene]; // GameScene boots first
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}`);
};
// WAIT — throttling-proof. This headless box starves setTimeout and
// stretches its compositor clock, so the only honest barrier is the
// GAME'S OWN CLOCK: Phaser v4 sets scene time.now from the engine loop
// time (monotonic across scene transitions). Poll it on rAF.
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) {
const start = performance.now();
setTimeout(() => resolve(), ms);
return;
}
const poll = () => {
const now = gameClock();
if (now !== null && now - base >= ms) return resolve();
requestAnimationFrame(poll);
};
requestAnimationFrame(poll);
});
// A real mouse press at CANVAS screen coords (x, y). Phaser 4.2.1 binds
// mousedown/mouseup/mousemove on the canvas element, so a dispatched
// MouseEvent drives the full input pipeline (pointer → scene handler →
// interactive objects). FIT mode scales the canvas, so map canvas →
// client space with the rendered rect (not 1:1 pixels).
const press = (x, y) => {
const canvas = game.canvas;
const r = canvas.getBoundingClientRect();
const zoom = r.width / canvas.width;
const mk = (type, buttons) => new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window,
button: 0,
buttons,
clientX: r.left + x * zoom,
clientY: r.top + y * zoom,
});
canvas.dispatchEvent(mk('mouseover', 0));
canvas.dispatchEvent(mk('mousedown', 1));
canvas.dispatchEvent(mk('mouseup', 0));
};
const OPEN_MS = 450; // openMs (340) + button decode margin
const CLOSE_MS = 350; // closeMs (200) + margin
const run = async () => {
await wait(900); // boot: galaxy, system, deck, sub-bar
const scene = game.scene.getScene('GameScene');
check('boots into GameScene', game.scene.isActive('GameScene'));
check('the sub-bar starts closed', scene.menuSubBar.isOpen === false);
// ---- 1) open with MENU, dismiss with a click in open space -----------
scene.menuAction();
await wait(OPEN_MS);
check('MENU folds the sub-bar up', scene.menuSubBar.isOpen);
const targetBefore = scene.ship.target;
press(scene.scale.width / 2, scene.scale.height * 0.4); // open space
await wait(CLOSE_MS);
check('a click OUTSIDE the sub-bar closes it',
scene.menuSubBar.isOpen === false && scene.menuSubBar.state === 'closed');
check('the dismissing click does NOT fly the ship',
scene.ship.target === targetBefore);
// ---- 2) a click on the sub-bar's own panel keeps it open -------------
scene.menuAction();
await wait(OPEN_MS);
check('the sub-bar re-opens', scene.menuSubBar.isOpen);
const bar = scene.menuSubBar.rect;
press(bar.x + 5, bar.y + bar.h / 2); // left padding — no button there
await wait(250);
check('a click on the sub-bar panel (not a button) keeps it open',
scene.menuSubBar.isOpen === true);
// ---- 3) a sub-bar button still fires while the bar is open -----------
const saveBtn = scene.menuSubBar.buttons.find((b) => b.id === 'save');
check('the Save Game button is inside the open bar',
scene.menuSubBar.contains(
scene.menuSubBar.x + saveBtn.btn.x,
scene.menuSubBar.y + saveBtn.btn.y));
press(scene.menuSubBar.x + saveBtn.btn.x, scene.menuSubBar.y + saveBtn.btn.y);
await wait(500);
check('Save Game still opens the 10-slot pop-up',
scene.savePanel.isOpen && scene.savePanel.mode === 'save');
scene.savePanel.close(); // the sub-bar stays open behind it (existing)
await wait(500);
// ---- 4) MENU toggles it closed again (toggle path intact) ------------
check('the sub-bar is still open under the closed pop-up',
scene.menuSubBar.isOpen === true);
scene.menuAction();
await wait(CLOSE_MS);
check('MENU folds the sub-bar back down', scene.menuSubBar.state === 'closed');
// ---- 5) clicking the MENU deck button closes it — no re-open ---------
scene.menuAction();
await wait(OPEN_MS);
check('the sub-bar re-opens (deck-button test)', scene.menuSubBar.isOpen);
const menuSlot = scene.actionBar.slots.find((s) => s.id === 'menu');
press(menuSlot.slot.x, menuSlot.slot.y);
await wait(CLOSE_MS);
check('clicking the MENU button closes the sub-bar (no re-open)',
scene.menuSubBar.isOpen === false && scene.menuSubBar.state === 'closed');
// ---- 6) the compass seam can't navigate while the bar is open --------
scene.menuAction();
await wait(OPEN_MS);
check('the sub-bar re-opens (compass test)', scene.menuSubBar.isOpen);
const targetBefore2 = scene.ship.target;
scene.autopilotTo('home'); // the chip's seam, with the bar open
await wait(CLOSE_MS);
check('a compass autopilot while open: bar closes, ship NOT targeted',
scene.menuSubBar.state === 'closed' && scene.ship.target === targetBefore2);
// ---- 7) all dismiss paths left the bar re-openable -------------------
scene.menuAction();
await wait(OPEN_MS);
check('the sub-bar re-opens after every dismiss path', scene.menuSubBar.isOpen);
};
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.__CLICKOUT__ = { pass, results, errors: window.__CAPTURED_ERRORS__ || [] };
done = true;
console.log(pass ? 'CLICKOUT PASS' : 'CLICKOUT FAIL');
});
// Hard timeout: a hung flow must not hang the driver forever.
setTimeout(() => {
if (!done) {
window.__CLICKOUT__ = { pass: false, results: [...results, { label: 'TIMED OUT (300s)', pass: false }] };
}
}, 300000);