Files
hooks-lib/tests/test-helpers.mjs
Kaysser Kayyali d038eb8c67 v0.3.0: rename module id hax-hooks-lib -> foundry-hooks-lib
User callout: 'Hax' is Kaysser's nickname. The module id should
not use it. Rename the Foundry module id from 'hax-hooks-lib' to
'foundry-hooks-lib'. Gitea repo name stays as 'hooks-lib' (kept
for the user-facing URL); the Gitea manifest URL is unchanged.

**Scope of rename:**
- module.json: id, title, version (0.2.0 -> 0.3.0), download URL
- package.json: name
- README.md, HOOK_CONTRACT.md, LICENSE: branding text
- All 6 production JS files: MODULE_ID constant + comments
- 4 active test files: console.log strings + test descriptions
- Rename of release zips in git: hooks-lib-X.Y.Z.zip ->
  foundry-hooks-lib-X.Y.Z.zip (preserves the v0.1.0 and v0.2.0
  zips as historical artifacts; the v0.3.0 zip is the new
  release artifact)
- .gitignore: glob + un-ignore lines updated to match

**Out of scope (deliberate):**
- Gitea repo name 'kaykayyali/hooks-lib' stays. Per the user's
  direction, only the module id is renamed; the Gitea URL path
  is preserved for the existing 'url', 'manifest', 'download'
  fields.
- scripts/_archive/v0.1.0/*: historical v0.1.0 code is left
  as-is. Those files tested 'hax-hooks-lib v0.1.0'; rewriting
  the history would be misleading.
- tests/_archive_v0.1.0_*.mjs: same reason, left untouched.
- .hermes/plans/* session-historian plans that reference
  'Hax's Tools split': session artifact, not a release asset.

**Verification:** 554/554 smoke assertions pass, 6/6 perf
assertions pass, median 0.0004ms/fire (well under 0.1ms
budget). No logic change; rename is string-only.

**Consumer action required:** battle-focus and its-achievable
both declare 'relationships.requires' pointing to
'hax-hooks-lib'. The next commits on those repos will update
their relationships to 'foundry-hooks-lib' + bump their
versions. Foundry instances with v0.2.0 of the old id
installed will need to be reinstalled as v0.3.0 of the new
id.
2026-06-20 16:53:37 -04:00

93 lines
2.8 KiB
JavaScript

// tests/test-helpers.mjs — v0.3.0
//
// Foundry hook stub for the no-Foundry smoke test. Installs globalThis.Hooks
// with the same semantics as Foundry v14:
// - Hooks.on(name, fn) — register
// - Hooks.once(name, fn) — register (fires once on next callAll)
// - Hooks.off(name, fn) — remove
// - Hooks.callAll(name, ...args) — synchronous fan-out
//
// Also stubs globalThis.game and globalThis.ui so the library's init
// and ready hooks can run in pure Node.
import { performance } from "node:perf_hooks";
const _listeners = new Map(); // hookName -> [fn, ...]
const _once = new WeakMap(); // fn -> { hookName } for once-tracking
const _callLog = []; // every Hooks.callAll(name, ...args) recorded
export function installStubs() {
resetStubs();
globalThis.Hooks = {
on(name, fn) {
_listeners.set(name, [...(_listeners.get(name) ?? []), fn]);
},
once(name, fn) {
_listeners.set(name, [...(_listeners.get(name) ?? []), fn]);
_once.set(fn, { hookName: name });
},
off(name, fn) {
const list = _listeners.get(name);
if (!list) return;
const next = list.filter((f) => f !== fn);
if (next.length === 0) _listeners.delete(name);
else _listeners.set(name, next);
_once.delete(fn);
},
callAll(name, ...args) {
_callLog.push({ name, args, ts: performance.now() });
const list = _listeners.get(name);
if (!list) return;
// Snapshot the list because handlers may add/remove listeners.
const snapshot = [...list];
for (const fn of snapshot) {
if (_once.has(fn)) {
// Once handler — remove before firing so re-entrant callAll doesn't re-fire.
this.off(name, fn);
}
try {
fn(...args);
} catch (e) {
// Mirror Foundry: errors in handlers are caught (the library
// adds its own error containment on top).
console.error(`[stubs] Hooks.callAll(${name}) handler threw:`, e);
}
}
},
};
globalThis.game = {
version: "13.351.0",
system: { id: "test-system", version: "0.0.0" },
modules: new Map(),
user: null,
ready: true,
};
globalThis.ui = { notifications: { info: () => {}, warn: () => {}, error: () => {} } };
}
export function resetStubs() {
_listeners.clear();
_callLog.length = 0;
}
export function getListeners(hookName) {
return _listeners.get(hookName) ?? [];
}
export function getAllCallLog() {
return [..._callLog];
}
export function clearCallLog() {
_callLog.length = 0;
}
export function setGameVersion(version) {
if (typeof globalThis.game === "undefined") globalThis.game = {};
globalThis.game.version = version;
}
export function setGameSystem(system) {
if (typeof globalThis.game === "undefined") globalThis.game = {};
globalThis.game.system = system;
}