import type { Creature } from "@initiative/domain"; import { creatureId } from "@initiative/domain"; import { beforeEach, describe, expect, it, vi } from "vitest"; // Mock idb — the one legitimate use of vi.mock for a third-party I/O library. // We can't use real IndexedDB in jsdom; this tests the cache logic through // all public API methods with an in-memory backing store. const fakeStore = new Map(); vi.mock("idb", () => ({ openDB: vi.fn().mockResolvedValue({ put: vi.fn((_storeName: string, value: unknown) => { const record = value as { sourceCode: string }; fakeStore.set(record.sourceCode, value); return Promise.resolve(); }), get: vi.fn((_storeName: string, key: string) => Promise.resolve(fakeStore.get(key)), ), getAll: vi.fn((_storeName: string) => Promise.resolve([...fakeStore.values()]), ), delete: vi.fn((_storeName: string, key: string) => { fakeStore.delete(key); return Promise.resolve(); }), clear: vi.fn((_storeName: string) => { fakeStore.clear(); return Promise.resolve(); }), }), })); // Import after mocking const { cacheSource, isSourceCached, getCachedSources, clearSource, clearAll, loadAllCachedCreatures, } = await import("../bestiary-cache.js"); function makeCreature(id: string, name: string): Creature { return { id: creatureId(id), name, source: "MM", sourceDisplayName: "Monster Manual", size: "Small", type: "humanoid", alignment: "neutral evil", ac: 15, hp: { average: 7, formula: "2d6" }, speed: "30 ft.", abilities: { str: 8, dex: 14, con: 10, int: 10, wis: 8, cha: 8 }, cr: "1/4", initiativeProficiency: 0, proficiencyBonus: 2, passive: 9, }; } describe("bestiary-cache", () => { beforeEach(() => { fakeStore.clear(); }); describe("cacheSource", () => { it("stores creatures and metadata", async () => { const creatures = [makeCreature("mm:goblin", "Goblin")]; await cacheSource("MM", "Monster Manual", creatures); expect(fakeStore.has("MM")).toBe(true); const record = fakeStore.get("MM") as { sourceCode: string; displayName: string; creatures: Creature[]; creatureCount: number; cachedAt: number; }; expect(record.sourceCode).toBe("MM"); expect(record.displayName).toBe("Monster Manual"); expect(record.creatures).toHaveLength(1); expect(record.creatureCount).toBe(1); expect(record.cachedAt).toBeGreaterThan(0); }); }); describe("isSourceCached", () => { it("returns false for uncached source", async () => { expect(await isSourceCached("XGE")).toBe(false); }); it("returns true after caching", async () => { await cacheSource("MM", "Monster Manual", []); expect(await isSourceCached("MM")).toBe(true); }); }); describe("getCachedSources", () => { it("returns empty array when no sources cached", async () => { const sources = await getCachedSources(); expect(sources).toEqual([]); }); it("returns source info with creature counts", async () => { await cacheSource("MM", "Monster Manual", [ makeCreature("mm:goblin", "Goblin"), makeCreature("mm:orc", "Orc"), ]); await cacheSource("VGM", "Volo's Guide", [ makeCreature("vgm:flind", "Flind"), ]); const sources = await getCachedSources(); expect(sources).toHaveLength(2); const mm = sources.find((s) => s.sourceCode === "MM"); expect(mm).toBeDefined(); expect(mm?.displayName).toBe("Monster Manual"); expect(mm?.creatureCount).toBe(2); const vgm = sources.find((s) => s.sourceCode === "VGM"); expect(vgm?.creatureCount).toBe(1); }); }); describe("loadAllCachedCreatures", () => { it("returns empty map when nothing cached", async () => { const map = await loadAllCachedCreatures(); expect(map.size).toBe(0); }); it("assembles creatures from all cached sources", async () => { const goblin = makeCreature("mm:goblin", "Goblin"); const orc = makeCreature("mm:orc", "Orc"); const flind = makeCreature("vgm:flind", "Flind"); await cacheSource("MM", "Monster Manual", [goblin, orc]); await cacheSource("VGM", "Volo's Guide", [flind]); const map = await loadAllCachedCreatures(); expect(map.size).toBe(3); expect(map.get(creatureId("mm:goblin"))?.name).toBe("Goblin"); expect(map.get(creatureId("mm:orc"))?.name).toBe("Orc"); expect(map.get(creatureId("vgm:flind"))?.name).toBe("Flind"); }); }); describe("clearSource", () => { it("removes a single source", async () => { await cacheSource("MM", "Monster Manual", []); await cacheSource("VGM", "Volo's Guide", []); await clearSource("MM"); expect(await isSourceCached("MM")).toBe(false); expect(await isSourceCached("VGM")).toBe(true); }); }); describe("clearAll", () => { it("removes all cached data", async () => { await cacheSource("MM", "Monster Manual", []); await cacheSource("VGM", "Volo's Guide", []); await clearAll(); const sources = await getCachedSources(); expect(sources).toEqual([]); }); }); });