Files
Its-Achievable/tests/test-helpers.mjs
Kaysser Kayyali 2f6acc0da1 v0.3.0: strip combat HUD; integrate with combat-hud-hub
- Deleted scripts/hud.js, scripts/event-translation.js, styles/hud.css,
  templates/hud.html.
- Removed mod.api.getHud/openHud/closeHud/buildHudUpdatePayload exports.
- Removed hudPosition setting (moved to combat-hud-hub).
- Added hub integration: at ready, if combat-hud-hub is installed,
  register a 'pinned-achievements' section. The chatBubble listener
  now also pushes entries into that section via hub.api.pushFeedEntry.
- Soft-dep on combat-hud-hub (optional); popover still works without
  the hub.
- Tests: 57/57 passing covering sections A-G (rule engine, catalog,
  awarding, hooks wiring, hub integration, graceful degradation).
2026-06-22 12:46:57 -04:00

208 lines
6.4 KiB
JavaScript

// tests/test-helpers.mjs — its-achievable v0.3.0
//
// Foundry stub for the no-Foundry smoke test. Installs globalThis.Hooks,
// game, ui, FormApplication, and game.modules so the moved code can be
// imported without Foundry.
import { performance } from "node:perf_hooks";
const _listeners = new Map();
const _once = new WeakMap();
const _callLog = [];
// FormApplication stub: enough surface for `CustomAchievementsApp` to
// extend. The smoke test doesn't open the form, so constructor +
// defaultOptions + render are no-ops.
class StubFormApplication {
constructor(...args) {
StubFormApplication._lastInstance = this;
this._args = args;
}
render(force) { return this; }
close() { return this; }
}
StubFormApplication.defaultOptions = { id: "stub-form", template: "", width: 600 };
StubFormApplication._lastInstance = null;
const StubHandlebarsApplicationMixin = (Base) => class extends Base {
static PARTS = {};
};
export function installStubs(opts = {}) {
resetStubs();
const { withHooksLib = true, withBattleFocus = true, withHudHub = true, systemId = "dnd5e", systemVersion = "5.2.5", foundryVersion = "13.351.0" } = opts;
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;
const snapshot = [...list];
for (const fn of snapshot) {
if (_once.has(fn)) this.off(name, fn);
try {
fn(...args);
} catch (e) {
console.error(`[stubs] Hooks.callAll(${name}) handler threw:`, e);
}
}
},
};
// settings store (in-memory)
const _settings = new Map();
const settingsApi = {
get(moduleId, key) {
return _settings.get(`${moduleId}.${key}`);
},
set: async (moduleId, key, value) => {
_settings.set(`${moduleId}.${key}`, value);
},
register(moduleId, key, def) {
if (!_settings.has(`${moduleId}.${key}`)) {
_settings.set(`${moduleId}.${key}`, def.default);
}
},
};
// modules store
const _modules = new Map();
if (withHooksLib) {
const _hooksLibSubscribers = new Map();
const _hooksLibOnce = new WeakMap();
_modules.set("foundry-hooks-lib", {
id: "foundry-hooks-lib",
active: true,
api: {
MODULE_ID: "foundry-hooks-lib",
version: "0.2.0",
REGISTERED_HOOKS: ["combatStart", "combatEnd", "updateActor", "createToken", "dnd5e.rollAttackV2", "dnd5e.rollDamageV2", "preUpdateActor", "updateToken", "preUpdateToken"],
subscribe(hookName, fn) {
_hooksLibSubscribers.set(hookName, [...(_hooksLibSubscribers.get(hookName) ?? []), fn]);
return () => {
const list = _hooksLibSubscribers.get(hookName);
if (!list) return;
const next = list.filter((f) => f !== fn);
if (next.length === 0) _hooksLibSubscribers.delete(hookName);
else _hooksLibSubscribers.set(hookName, next);
};
},
subscribeMany(map) {
const unsubs = [];
for (const [name, fn] of Object.entries(map)) {
unsubs.push(this.subscribe(name, fn));
}
return () => { for (const u of unsubs) u(); };
},
_fireForTest(hookName, ...args) {
const list = _hooksLibSubscribers.get(hookName);
if (!list) return;
for (const fn of list) {
try { fn({ ts: Date.now(), hook: hookName, args }); } catch (e) {
console.error(`[stubs] hooksLib ${hookName} handler threw:`, e);
}
}
},
_hasSubscribersFor: (hookName) => _hooksLibSubscribers.has(hookName),
},
});
}
if (withBattleFocus) {
const _enc = {
id: "enc-test",
startedAt: Date.now() - 30000,
round: 1,
turn: 0,
currentTurn: { name: "Bard", tokenId: "t1", portrait: "" },
combatants: new Map(),
isActive: () => true,
buildStats: () => ({ kills: 0, dmg: 0, crits: 0, hits: 0, attacks: 0 }),
};
_modules.set("battle-focus", {
id: "battle-focus",
active: true,
api: {
MODULE_ID: "battle-focus",
getActiveEncounter: () => _enc,
getEncounter: () => _enc,
},
});
}
if (withHudHub) {
const _sections = new Map();
const _feed = new Map();
_modules.set("combat-hud-hub", {
id: "combat-hud-hub",
active: true,
api: {
MODULE_ID: "combat-hud-hub",
version: "0.2.0",
addSection(spec) {
_sections.set(spec.id, spec);
if (!_feed.has(spec.id)) _feed.set(spec.id, []);
},
removeSection(id) {
_sections.delete(id);
_feed.delete(id);
},
listSections() {
return Array.from(_sections.values()).map(s => ({
id: s.id, label: s.label, render: s.render,
}));
},
pushFeedEntry(sectionId, entry) {
if (!_feed.has(sectionId)) _feed.set(sectionId, []);
_feed.get(sectionId).push(entry);
},
getFeed(sectionId) {
return _feed.get(sectionId) ?? [];
},
getHud() { return null; },
openHud() {},
closeHud() {},
},
});
}
globalThis.game = {
version: foundryVersion,
system: { id: systemId, version: systemVersion },
modules: _modules,
settings: settingsApi,
user: null,
ready: true,
actors: {
_store: new Map(),
get(id) { return this._store.get(id) ?? null; },
getName(name) { for (const a of this._store.values()) if (a?.name === name) return a; return null; },
},
};
globalThis.ui = {
notifications: { info: () => {}, warn: () => {}, error: () => {} },
chat: [],
};
globalThis.FormApplication = StubFormApplication;
globalThis.mergeObject = (a, b) => ({ ...a, ...b });
globalThis.foundry = undefined;
globalThis.canvas = undefined;
}
export function resetStubs() {
_listeners.clear();
_callLog.length = 0;
}
export function getCallLog() {
return [..._callLog];
}