226 lines
8.9 KiB
JavaScript
226 lines
8.9 KiB
JavaScript
/**
|
|
* Dev-only: repro + regression check for the MAP console's object click.
|
|
*
|
|
* Scenario under test:
|
|
* 1. open the map console (deck MAP button)
|
|
* 2. hover a discovered object on the chart → the tooltip
|
|
* (name + "TAP TO PLOT COURSE") must be part of the window
|
|
* 3. click it → autopilot (ship.target set)
|
|
* 4. close the map → NOTHING of the tooltip may linger on screen
|
|
*
|
|
* The known bug: ttPlate/ttName/ttSub/ttHint are created with
|
|
* scene.add() but never added to the MapWindow container (only the
|
|
* EMPTY ttCont is) — so they paint at depth 5, BEHIND the window
|
|
* (depth 80), and survive close() (nothing ever hides/clears them).
|
|
*
|
|
* node dev/server.mjs 8091
|
|
* node dev/shot-firefox.mjs \
|
|
* "http://127.0.0.1:8091/dev/map-click.html" \
|
|
* "window.__MAPCLICK ? window.__MAPCLICK.ready : null" \
|
|
* /tmp/map-click.png 30000
|
|
*/
|
|
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);
|
|
|
|
const errors = [];
|
|
const origErr = console.error.bind(console);
|
|
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
|
|
window.addEventListener('error', (e) => errors.push(String(e.message)));
|
|
window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`));
|
|
|
|
const gameConfig = createGameConfig();
|
|
gameConfig.scene = [GameScene];
|
|
const game = new Phaser.Game(gameConfig);
|
|
window.game = game;
|
|
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
async function waitScene() {
|
|
for (let i = 0; i < 100; i++) {
|
|
const s = game.scene.getScene('GameScene');
|
|
if (s && s.ship && s.mapWindow) return s;
|
|
await sleep(100);
|
|
}
|
|
throw new Error('GameScene never booted');
|
|
}
|
|
|
|
/** Walk up the container parents to the object's top-level display-list ancestor. */
|
|
function topLevel(obj) {
|
|
let top = obj;
|
|
let c = obj.parentContainer; // Phaser 4: container children track their container here
|
|
while (c) { top = c; c = c.parentContainer; }
|
|
return top;
|
|
}
|
|
/** Effective (chained) visibility down to the top-level object. */
|
|
function effVisible(obj) {
|
|
let v = true;
|
|
let o = obj;
|
|
while (o) {
|
|
v = v && o.visible !== false;
|
|
o = o.parentContainer;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
try {
|
|
const s = await waitScene();
|
|
await sleep(800);
|
|
|
|
// Discover the central body + first planet + first station so the
|
|
// chart (and _hits) has something to point at.
|
|
const sysId = s.systemRecord.id;
|
|
let known = s.discovery.bySystem.get(sysId);
|
|
if (!known) { known = new Set(); s.discovery.bySystem.set(sysId, known); }
|
|
const push = (obj) => { if (obj && !known.has(obj.id)) s.discovery.check(sysId, obj.x, obj.y, [obj]); };
|
|
if (s.isHomeSystem && s.planet) push({ id: 'home', x: 0, y: 0, radius: s.planet.radius });
|
|
if (s.systemPlanets[0]) push({ id: s.systemPlanets[0].discoveryId, x: s.systemPlanets[0].x, y: s.systemPlanets[0].y, radius: s.systemPlanets[0].radius });
|
|
if (s.systemStations[0]) push({ id: s.systemStations[0].discoveryId, x: s.systemStations[0].x, y: s.systemStations[0].y, radius: s.systemStations[0].bound ?? 60 });
|
|
|
|
// Open the console, wait for the reveal + first paint.
|
|
s.deckAction('map');
|
|
await sleep(1800);
|
|
const win = s.mapWindow;
|
|
if (!win.isOpen) throw new Error('map window did not open');
|
|
|
|
// Force a fresh snapshot so _hits is populated.
|
|
const snap = win._snapOf();
|
|
if (snap && win._fp !== win._fpOf(snap)) win._applySnap(snap);
|
|
if (!win._hits || win._hits.length === 0) throw new Error('no hits on the chart');
|
|
|
|
const hit = win._hits[0];
|
|
const sx = win.geo.mapX + hit.x;
|
|
const sy = win.geo.mapY + hit.y;
|
|
|
|
const dl = s.sys.displayList.getChildren();
|
|
const iWin = dl.indexOf(win);
|
|
const iMapImg = dl.indexOf(win.mapImg);
|
|
|
|
const describe = (o, label) => {
|
|
const top = topLevel(o);
|
|
const iTop = dl.indexOf(top);
|
|
return {
|
|
label,
|
|
visible: o.visible,
|
|
effVisible: effVisible(o),
|
|
inWindow: top === win,
|
|
topInDisplayList: top === win ? 'the map window' : (iTop >= 0 ? `${top.type || top.constructor.name} @ dl[${iTop}] (win=${iWin})` : 'NOT in display list'),
|
|
paintsAboveMap: top === win || (iTop > iWin),
|
|
};
|
|
};
|
|
|
|
// ---- 1: hover the object ------------------------------------------------
|
|
win._setHover({ x: sx, y: sy });
|
|
await sleep(120);
|
|
|
|
const before = {
|
|
ttPlate: describe(win.ttPlate, 'ttPlate'),
|
|
ttName: describe(win.ttName, 'ttName'),
|
|
ttHint: describe(win.ttHint, 'ttHint'),
|
|
ttCont: describe(win.ttCont, 'ttCont'),
|
|
hoverG: describe(win.hoverG, 'hoverG'),
|
|
hover: !!win._hover,
|
|
hoverId: win._hover?.id ?? null,
|
|
ttNameText: win.ttName.text,
|
|
mapImg: { inDL: iMapImg >= 0, interactive: !!(win.mapImg.input && win.mapImg.input.enabled) },
|
|
};
|
|
|
|
// ---- 2: click the object (through the real handler) --------------------
|
|
const targetBefore = s.ship.target;
|
|
win.mapImg.emit('pointerdown', { x: sx, y: sy });
|
|
await sleep(120);
|
|
const click = {
|
|
targetWasSet: s.ship.target !== null && s.ship.target !== targetBefore,
|
|
target: s.ship.target ? { x: Math.round(s.ship.target.x), y: Math.round(s.ship.target.y) } : null,
|
|
flash: !!win._flash,
|
|
};
|
|
|
|
// a click on EMPTY plate must not retarget
|
|
s.ship.target = null;
|
|
win.mapImg.emit('pointerdown', { x: win.geo.mapX + 4, y: win.geo.mapY + 4 });
|
|
await sleep(120);
|
|
const missClick = { targetStayedNull: s.ship.target === null };
|
|
|
|
// ?keepOpen=1 — stop here (map still open, tooltip up) so a screenshot
|
|
// harness can capture the chart + tooltip on screen.
|
|
const keepOpen = new URLSearchParams(location.search).has('keepOpen');
|
|
let after = null, reopen = null, after2 = null;
|
|
if (keepOpen) {
|
|
const pass = click.targetWasSet && missClick.targetStayedNull;
|
|
window.__MAPCLICK = {
|
|
ready: true, pass, keepOpen: true, before, click, missClick, errors,
|
|
};
|
|
console.log('MAPCLICK (keepOpen) ready');
|
|
} else {
|
|
// ---- 3: close the map, then inspect what lingers ------------------------
|
|
win.close();
|
|
await sleep(700); // close tween is 150 ms
|
|
|
|
after = {
|
|
openState: win.openState,
|
|
ttPlate: describe(win.ttPlate, 'ttPlate'),
|
|
ttName: describe(win.ttName, 'ttName'),
|
|
ttHint: describe(win.ttHint, 'ttHint'),
|
|
ttCont: describe(win.ttCont, 'ttCont'),
|
|
hoverG: describe(win.hoverG, 'hoverG'),
|
|
};
|
|
|
|
// ---- 4: re-open, hover, close again (no leftovers, no crash) -----------
|
|
win.open();
|
|
await sleep(1800);
|
|
win._setHover({ x: sx, y: sy });
|
|
await sleep(120);
|
|
reopen = { open: win.isOpen, hoverVisible: effVisible(win.ttName) };
|
|
win.close();
|
|
await sleep(700);
|
|
after2 = {
|
|
openState: win.openState,
|
|
ttContHidden: !effVisible(win.ttCont),
|
|
ttPlateHidden: !effVisible(win.ttPlate),
|
|
ttNameHidden: !effVisible(win.ttName),
|
|
ttHintHidden: !effVisible(win.ttHint),
|
|
};
|
|
}
|
|
|
|
const results = [];
|
|
const check = (label, cond) => results.push({ label, pass: !!cond });
|
|
|
|
check('tooltip (ttPlate) belongs to the map window (paints with it)',
|
|
topLevel(win.ttPlate) === win);
|
|
check('tooltip name text belongs to the map window', topLevel(win.ttName) === win);
|
|
check('tooltip hint ("TAP TO PLOT COURSE") belongs to the map window', topLevel(win.ttHint) === win);
|
|
check('while open: tooltip effectively visible on the chart',
|
|
before.ttPlate.effVisible && before.ttName.effVisible);
|
|
check('while open: tooltip paints ABOVE the map surface (inside the window)',
|
|
before.ttPlate.paintsAboveMap && before.ttName.paintsAboveMap);
|
|
check('clicking a discovered object sets the ship target (autopilot)',
|
|
click.targetWasSet);
|
|
check('clicking empty plate does NOT retarget', missClick.targetStayedNull);
|
|
if (!keepOpen) {
|
|
check('after close: tooltip plate hidden (no lingering button)',
|
|
!after.ttPlate.effVisible);
|
|
check('after close: tooltip name hidden', !after.ttName.effVisible);
|
|
check('after close: tooltip hint hidden', !after.ttHint.effVisible);
|
|
check('after close: hover ring cleared',
|
|
!after.hoverG.effVisible || win.hoverG.commandBuffer.length === 0);
|
|
check('re-open: hover tooltip visible on the chart again', reopen.open && reopen.hoverVisible);
|
|
check('second close: tooltip fully hidden (no leftovers)',
|
|
after2.ttContHidden && after2.ttPlateHidden && after2.ttNameHidden && after2.ttHintHidden);
|
|
}
|
|
check('no console errors', errors.length === 0);
|
|
|
|
const pass = results.every((r) => r.pass);
|
|
window.__MAPCLICK = { ready: true, pass, results, before, click, missClick, after, reopen, after2, errors };
|
|
console.log(pass ? 'MAPCLICK PASS' : 'MAPCLICK FAIL');
|
|
console.log(JSON.stringify({ results, before, click, after }, null, 1));
|
|
} catch (err) {
|
|
const results = [{ label: `THREW: ${err.message}`, pass: false }];
|
|
window.__MAPCLICK = { ready: true, pass: false, results, errors };
|
|
console.error('MAPCLICK FAIL (threw)', err);
|
|
}
|