Introduce adapter injection and migrate test suite
Replace direct adapter/persistence imports with context-based injection (AdapterContext + useAdapters) so tests use in-memory implementations instead of vi.mock. Migrate component tests from context mocking to AllProviders with real hooks. Extract export/import logic from ActionBar into useEncounterExportImport hook. Add bestiary-cache and bestiary-index-adapter test suites. Raise adapter coverage thresholds (68→80 lines, 56→62 branches). 77 test files, 891 tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
174
apps/web/src/adapters/__tests__/bestiary-cache.test.ts
Normal file
174
apps/web/src/adapters/__tests__/bestiary-cache.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
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<string, unknown>();
|
||||
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user