Files
Battle-for-Tismo/tests/framework/dom.js
T
adrien f3e94a3d53 Added the devlog, fixed the map labels and reworked joining
- Political map country names were invisible because the label SVG had a zero-sized viewport, which some browsers never paint; size it to the world and guard it with a test.
- Added a Devlog button showing the ten latest commits, generated into client/js/devlog_data.js by scripts/generate-devlog.js and refreshed by the pre-commit hook.
- Worked through FIXME.md: the city button and a unit double-click now open the city, and the stat tooltip escapes the HUD so the unit panel cannot cover it.
- The server creates its game at startup and clients only join it; the Create game screen and the configure_game path are gone, and testing is a --testing server flag.
- A page on a loopback host signs in as adrien, claims France and gets the free, instant testing buttons, while the server grants free orders only to a loopback socket whose Origin is also loopback.
2026-09-21 23:08:05 +02:00

162 lines
4.5 KiB
JavaScript

// jsdom harness for the browser client tests. It loads the real
// client/index.html, wires jQuery to the simulated window and exposes the
// globals the client modules expect ($, window, document, requestAnimationFrame,
// a fake WebSocket). Everything is restored on teardown so the Node server
// tests keep using the real WebSocket.
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { JSDOM } from "jsdom";
const INDEX_PATH = fileURLToPath(new URL("../../client/index.html", import.meta.url));
const JQUERY_PATH = fileURLToPath(new URL("../../client/vendor/jquery.min.js", import.meta.url));
// Only the globals the client actually reads. Some Node globals (navigator,
// location) are getter-only, so they are left alone.
const MANAGED_GLOBALS = [
"window", "document", "$", "jQuery", "WebSocket", "Audio",
"requestAnimationFrame", "cancelAnimationFrame",
];
// Minimal stand-in for the browser WebSocket used by Net. Tests drive it by
// calling simulateOpen/simulateMessage/simulateClose.
export class FakeWebSocket {
static CONNECTING = 0;
static OPEN = 1;
static CLOSING = 2;
static CLOSED = 3;
static instances = [];
constructor(url) {
this.url = url;
this.readyState = FakeWebSocket.CONNECTING;
this.sent = [];
FakeWebSocket.instances.push(this);
}
send(data) {
this.sent.push(JSON.parse(data));
}
close() {
if (this.readyState === FakeWebSocket.CLOSED) return;
this.readyState = FakeWebSocket.CLOSED;
if (this.onclose) this.onclose({});
}
simulateOpen() {
this.readyState = FakeWebSocket.OPEN;
if (this.onopen) this.onopen({});
}
simulateMessage(message) {
if (this.onmessage) this.onmessage({ data: JSON.stringify(message) });
}
simulateError() {
if (this.onerror) this.onerror({});
}
simulateClose() {
this.close();
}
lastSent() {
return this.sent[this.sent.length - 1];
}
static reset() {
FakeWebSocket.instances = [];
}
static get last() {
return FakeWebSocket.instances[FakeWebSocket.instances.length - 1];
}
}
// Stand-in for the browser Audio element. jsdom's play() is unimplemented and
// noisy, and the tests only care that the right source was asked to play.
export class FakeAudio {
static instances = [];
static reset() {
FakeAudio.instances = [];
}
// Every source that was actually played, in order.
static get playedSources() {
return FakeAudio.instances.filter((audio) => audio.plays > 0).map((audio) => audio.src);
}
constructor(src) {
this.src = src;
this.volume = 1;
this.loop = false;
this.currentTime = 0;
this.paused = true;
this.plays = 0;
FakeAudio.instances.push(this);
}
play() {
this.plays += 1;
this.paused = false;
return Promise.resolve();
}
pause() {
this.paused = true;
}
}
export async function setupDom(options = {}) {
const html = await readFile(INDEX_PATH, "utf8");
const jquerySource = await readFile(JQUERY_PATH, "utf8");
const dom = new JSDOM(html, {
// `url` lets a test pose as a page on the local machine (localhost or
// 127.0.0.1), which the client treats differently from the default
// non-loopback origin.
url: options.url || "http://game.example/client/index.html",
runScripts: "outside-only",
pretendToBeVisual: false,
});
const window = dom.window;
// Evaluate the exact jQuery file the client serves into the simulated window.
window.eval(jquerySource);
const $ = window.jQuery;
const saved = new Map();
const install = (name, value) => {
saved.set(name, Object.getOwnPropertyDescriptor(globalThis, name) || null);
Object.defineProperty(globalThis, name, { value, configurable: true, writable: true });
};
install("window", window);
install("document", window.document);
install("$", $);
install("jQuery", $);
install("WebSocket", options.webSocket || FakeWebSocket);
install("Audio", options.audio || FakeAudio);
// Keep the render loop out of tests: enter() schedules a frame but the stub
// never runs it, so no timer keeps the process alive.
install("requestAnimationFrame", () => 0);
install("cancelAnimationFrame", () => {});
return { dom, window, $, saved };
}
export function teardownDom(env) {
if (!env) return;
for (const [name, descriptor] of env.saved) {
try {
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
else delete globalThis[name];
} catch {
// leave a non-configurable global as it is
}
}
try {
env.dom.window.close();
} catch {
// already closed
}
}