191 lines
7.3 KiB
JavaScript
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);
|