Spaces:
Paused
Paused
Download test/_dom.js from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 9.46 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/test/_dom.js
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities@d19cbc4fab45274de920705d8b5b01da170c7e89/test/_dom.js
-
curl -L -o _dom.js https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/test/_dom.js
9.46 kB
| /* ============================================================ | |
| The shared DOM double for the browser-layer tests. | |
| Eight test files each grew their own `class FakeElement extends EventTarget` | |
| β hud, hudSelection, observer, overlays, saveload, starmap, techChart, | |
| update β plus four copies of the same no-op-Proxy `fakeCtx()` and eight | |
| copies of a byte-identical `fakeDocument()`. They were ~90% the same code, | |
| and the 10% that differed was not a considered difference: it was whichever | |
| subset of the DOM the file that copied it happened to need that week. So | |
| overlays' copy grew real `.remove()` parent tracking and hudSelection's did | |
| not; hud's grew a real `querySelector` that walks the tree and update's kept | |
| the always-null stub; observer's `getBoundingClientRect` returned a real | |
| 800x600 box and saveload's returned zeros. A test written against a weak | |
| copy silently asserts less than the same test written against a strong one, | |
| and nothing tells you which copy you landed on. | |
| This is the union, with the strongest implementation of each member β real | |
| child arrays, real per-instance class state, real parent tracking so | |
| `.remove()` splices, and a real (if shallow) `querySelector` β so every file | |
| starts from the best version rather than the weakest. | |
| TWO THINGS ARE DELIBERATELY CONFIGURABLE, because making them uniform would | |
| change what the code under test actually does: | |
| β’ `context` β what `getContext()` hands back. dom.js calls | |
| `.getContext("2d")` on the canvas handles at module scope no matter what | |
| the test is about, so it always has to succeed. Returning null (the | |
| default) leaves modules that guard `if (!ctx) return` inert; returning | |
| `fakeCtx()` lets them run all the way through. Files whose subject | |
| actually draws (render.js's spriteIcon reads `canvas.toDataURL()` back, | |
| and its own catch fires a console.error on EVERY icon button of EVERY | |
| render if either half fails) pass `{ context: fakeCtx }`. | |
| β’ `clientWidth` / `clientHeight` β the viewport `getBoundingClientRect` | |
| reports. Camera and pointer-mapping code divides by these, so zeros and | |
| 800x600 are genuinely different test fixtures, not a detail. | |
| USAGE β dom.js resolves every element handle exactly ONCE, at import time, | |
| via `doc.getElementById(id)`, so the document has to exist BEFORE anything | |
| that transitively imports dom.js is first imported. Under `node --test`'s | |
| native ESM loader that means installFakeDom() at the top of the file and | |
| `await import(...)` for the modules under test, never a static import: | |
| import { installFakeDom, fakeCtx } from "./_dom.js"; | |
| installFakeDom({ context: fakeCtx }); | |
| const { renderSelectionPanel } = await import("../hudSelection.js"); | |
| ============================================================ */ | |
| ; | |
| // Module-level rather than threaded through every constructor: `node --test` gives each test | |
| // FILE its own process, so there is exactly one document per process and installFakeDom() sets | |
| // this once, before the first dynamic import. | |
| const DEFAULTS = { context: () => null, clientWidth: 800, clientHeight: 600 }; | |
| let config = { ...DEFAULTS }; | |
| // Any method call is a silent no-op; any property read/write round-trips through a plain backing | |
| // object. That tolerates whatever drawUnitShape/drawBuildingShape happen to call (scale/translate/ | |
| // fillStyle/strokeStyle/gradients/β¦) without hand-enumerating the 2D canvas API, and stays robust | |
| // to unrelated changes in those drawing functions. Tests that need to ASSERT on the draw calls | |
| // want a recording proxy instead β see the golden-trace harness in test/render.test.js. | |
| export function fakeCtx() { | |
| return new Proxy({}, { get: (t, p) => (p in t ? t[p] : () => {}) }); | |
| } | |
| export class FakeElement extends EventTarget { | |
| constructor(tag = "div") { | |
| super(); | |
| this.tagName = tag; | |
| this.children = []; | |
| this.dataset = {}; | |
| this.style = {}; | |
| this.clientWidth = config.clientWidth; | |
| this.clientHeight = config.clientHeight; | |
| this._classes = new Set(); | |
| this.classList = { | |
| add: (...c) => c.forEach(x => this._classes.add(x)), | |
| remove: (...c) => c.forEach(x => this._classes.delete(x)), | |
| toggle: (c, f) => { | |
| const on = f === undefined ? !this._classes.has(c) : f; | |
| if (on) this._classes.add(c); else this._classes.delete(c); | |
| return on; // real classList.toggle returns the resulting state | |
| }, | |
| contains: c => this._classes.has(c), | |
| }; | |
| } | |
| get className() { return [...this._classes].join(" "); } | |
| set className(v) { this._classes = new Set(String(v).split(/\s+/).filter(Boolean)); } | |
| // Parent tracking is what makes `.remove()` real: showGalaxyToast's eviction (overlays.js) and | |
| // showBanner's "replace any prior banner" (update.js) both call .remove() on a CHILD element, | |
| // which in the real DOM splices itself out of its own parent. | |
| appendChild(c) { c._parent = this; this.children.push(c); return c; } | |
| append(...cs) { cs.forEach(c => { c._parent = this; this.children.push(c); }); } | |
| prepend(c) { c._parent = this; this.children.unshift(c); return c; } | |
| removeChild(c) { | |
| const i = this.children.indexOf(c); | |
| if (i !== -1) { this.children.splice(i, 1); c._parent = null; } | |
| return c; | |
| } | |
| remove() { | |
| if (!this._parent) return; | |
| const i = this._parent.children.indexOf(this); | |
| if (i !== -1) this._parent.children.splice(i, 1); | |
| this._parent = null; | |
| } | |
| // children are pushed in append order, so index 0 IS the oldest β what showGalaxyToast's | |
| // all-clickable eviction branch (`stack.firstElementChild`) needs. | |
| get firstElementChild() { return this.children[0] || null; } | |
| closest(selector) { | |
| const cls = selector.startsWith(".") ? selector.slice(1) : null; | |
| for (let n = this; n; n = n._parent) if (cls && n._classes?.has(cls)) return n; | |
| return null; | |
| } | |
| // A real (if shallow) innerHTML round-trip: `= ""` clears the children the way the real thing | |
| // does β that is the only value most rebuild paths ever assign β and any other string is | |
| // readable back as the string it was, so a panel built from a raw template (showGameOver's | |
| // score breakdown, showScenarioEnd) is inspectable rather than always reading empty. No real | |
| // HTML PARSING: nothing here needs the assigned string to become child nodes. | |
| set innerHTML(v) { this._html = v; if (v === "") this.children = []; } | |
| get innerHTML() { return this._html ?? ""; } | |
| // A minimal REAL selector. Every call site in the browser layer queries a single class name | |
| // (".market-row", ".queue-label", ".recycle-progress", β¦) β never a combinator or an attribute | |
| // selector β so a depth-first walk matching one class per node is enough. The always-null stub | |
| // some copies used is tolerated by every live-patch call site (`if (row) β¦` / `rows[i] && β¦`), | |
| // which is exactly the problem: a test written against it can never tell "the code patched | |
| // nothing" apart from "the code patched a node I could not find". | |
| _queryAll(selector) { | |
| const cls = selector.startsWith(".") ? selector.slice(1) : null; | |
| const out = []; | |
| const walk = kids => { for (const c of kids) { if (cls && c._classes?.has(cls)) out.push(c); if (c.children) walk(c.children); } }; | |
| walk(this.children); | |
| return out; | |
| } | |
| querySelector(selector) { return this._queryAll(selector)[0] || null; } | |
| querySelectorAll(selector) { return this._queryAll(selector); } | |
| // Attributes are kept as a real map rather than no-ops: openLandingPicker (boot.js) sets | |
| // type/aria attributes on the controls it builds, and a no-op setter makes "did it set the | |
| // attribute?" permanently unanswerable. | |
| setAttribute(name, value) { (this._attrs ??= {})[name] = String(value); } | |
| getAttribute(name) { return this._attrs?.[name] ?? null; } | |
| removeAttribute(name) { if (this._attrs) delete this._attrs[name]; } | |
| focus() { this._focused = true; } | |
| blur() { this._focused = false; } | |
| getContext() { return config.context(); } | |
| toDataURL() { return "data:image/fake,"; } | |
| getBoundingClientRect() { return { left: 0, top: 0, width: this.clientWidth, height: this.clientHeight }; } | |
| // Mirrors real HTMLElement#click(); hudSelection.js's own prodButton calls it for hotkey replay. | |
| click() { this.dispatchEvent(new Event("click")); } | |
| } | |
| // Real per-id identity. dom.js resolves each handle exactly ONCE at import time and every later | |
| // doc.getElementById(sameId) must keep returning that SAME object, or dom.js's exported handles | |
| // (panelEl, galaxyToastEl, starmapEl, β¦) silently diverge from what the code under test is | |
| // actually appending to β and the test then inspects an empty tree that nothing ever wrote. | |
| export function fakeDocument() { | |
| const byId = new Map(); | |
| const body = new FakeElement("body"); | |
| return { | |
| getElementById(id) { if (!byId.has(id)) byId.set(id, new FakeElement("div")); return byId.get(id); }, | |
| createElement(tag) { return new FakeElement(tag); }, | |
| body, | |
| }; | |
| } | |
| /** | |
| * Install a fake `document` on globalThis and return it. Call BEFORE the first | |
| * `await import()` of anything that reaches dom.js. | |
| * @param {{context?: () => any, clientWidth?: number, clientHeight?: number}} [opts] | |
| */ | |
| export function installFakeDom(opts = {}) { | |
| config = { ...DEFAULTS, ...opts }; | |
| globalThis.document = fakeDocument(); | |
| return globalThis.document; | |
| } | |