// OrderDraftStore unit tests under JSDOM with `fake-indexeddb` // standing in for the browser's IndexedDB factory. The store is // driven directly with a real `IDBCache` so persistence is exercised // the same way it would be inside the in-game shell layout. // // Each case opens a freshly named database so state cannot leak // across tests; per-game isolation is verified explicitly by mixing // drafts under different `gameId`s through one shared cache. import "@testing-library/jest-dom/vitest"; import "fake-indexeddb/auto"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import type { IDBPDatabase } from "idb"; import { IDBCache } from "../src/platform/store/idb-cache"; import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb"; import type { Cache } from "../src/platform/store/index"; import { OrderDraftStore } from "../src/sync/order-draft.svelte"; import type { OrderCommand } from "../src/sync/order-types"; let db: IDBPDatabase; let dbName: string; let cache: Cache; beforeEach(async () => { dbName = `galaxy-order-draft-test-${crypto.randomUUID()}`; db = await openGalaxyDB(dbName); cache = new IDBCache(db); }); afterEach(async () => { db.close(); await new Promise((resolve) => { const req = indexedDB.deleteDatabase(dbName); req.onsuccess = () => resolve(); req.onerror = () => resolve(); req.onblocked = () => resolve(); }); }); const GAME_ID = "11111111-2222-3333-4444-555555555555"; function placeholder(id: string, label: string): OrderCommand { return { kind: "placeholder", id, label }; } describe("OrderDraftStore", () => { test("init on empty cache yields ready status with no commands", async () => { const store = new OrderDraftStore(); expect(store.status).toBe("idle"); await store.init({ cache, gameId: GAME_ID }); expect(store.status).toBe("ready"); expect(store.commands).toEqual([]); store.dispose(); }); test("add appends commands and persists across instances", async () => { const a = new OrderDraftStore(); await a.init({ cache, gameId: GAME_ID }); await a.add(placeholder("c1", "first")); await a.add(placeholder("c2", "second")); expect(a.commands.map((c) => c.id)).toEqual(["c1", "c2"]); a.dispose(); const b = new OrderDraftStore(); await b.init({ cache, gameId: GAME_ID }); expect(b.commands.map((c) => c.id)).toEqual(["c1", "c2"]); expect(b.commands[1]).toEqual(placeholder("c2", "second")); b.dispose(); }); test("remove drops the matching command and persists the removal", async () => { const a = new OrderDraftStore(); await a.init({ cache, gameId: GAME_ID }); await a.add(placeholder("c1", "first")); await a.add(placeholder("c2", "second")); await a.add(placeholder("c3", "third")); await a.remove("c2"); expect(a.commands.map((c) => c.id)).toEqual(["c1", "c3"]); a.dispose(); const b = new OrderDraftStore(); await b.init({ cache, gameId: GAME_ID }); expect(b.commands.map((c) => c.id)).toEqual(["c1", "c3"]); b.dispose(); }); test("remove on a missing id is a silent no-op", async () => { const store = new OrderDraftStore(); await store.init({ cache, gameId: GAME_ID }); await store.add(placeholder("c1", "first")); await store.remove("absent"); expect(store.commands.map((c) => c.id)).toEqual(["c1"]); store.dispose(); }); test("move reorders the commands and persists the new order", async () => { const a = new OrderDraftStore(); await a.init({ cache, gameId: GAME_ID }); await a.add(placeholder("c1", "first")); await a.add(placeholder("c2", "second")); await a.add(placeholder("c3", "third")); await a.move(0, 2); expect(a.commands.map((c) => c.id)).toEqual(["c2", "c3", "c1"]); a.dispose(); const b = new OrderDraftStore(); await b.init({ cache, gameId: GAME_ID }); expect(b.commands.map((c) => c.id)).toEqual(["c2", "c3", "c1"]); b.dispose(); }); test("move with out-of-range or identical indices is a no-op", async () => { const store = new OrderDraftStore(); await store.init({ cache, gameId: GAME_ID }); await store.add(placeholder("c1", "first")); await store.add(placeholder("c2", "second")); await store.move(1, 1); await store.move(-1, 0); await store.move(0, 5); expect(store.commands.map((c) => c.id)).toEqual(["c1", "c2"]); store.dispose(); }); test("drafts under different game ids do not bleed through one cache", async () => { const otherGame = "99999999-9999-9999-9999-999999999999"; const a = new OrderDraftStore(); await a.init({ cache, gameId: GAME_ID }); await a.add(placeholder("a1", "from-a")); a.dispose(); const b = new OrderDraftStore(); await b.init({ cache, gameId: otherGame }); expect(b.commands).toEqual([]); await b.add(placeholder("b1", "from-b")); b.dispose(); const reloadA = new OrderDraftStore(); await reloadA.init({ cache, gameId: GAME_ID }); expect(reloadA.commands.map((c) => c.id)).toEqual(["a1"]); reloadA.dispose(); const reloadB = new OrderDraftStore(); await reloadB.init({ cache, gameId: otherGame }); expect(reloadB.commands.map((c) => c.id)).toEqual(["b1"]); reloadB.dispose(); }); test("mutations made before init resolves are ignored", async () => { const store = new OrderDraftStore(); await store.add(placeholder("c1", "first")); await store.remove("c1"); await store.move(0, 1); expect(store.status).toBe("idle"); expect(store.commands).toEqual([]); await store.init({ cache, gameId: GAME_ID }); expect(store.commands).toEqual([]); store.dispose(); }); test("dispose suppresses persistence side effects of in-flight mutations", async () => { const store = new OrderDraftStore(); await store.init({ cache, gameId: GAME_ID }); await store.add(placeholder("c1", "first")); store.dispose(); // Adding after dispose is a no-op because status remains // `ready` but the cache pointer is null and the destroyed flag // blocks the persist path. await store.add(placeholder("c2", "second")); const reload = new OrderDraftStore(); await reload.init({ cache, gameId: GAME_ID }); expect(reload.commands.map((c) => c.id)).toEqual(["c1"]); reload.dispose(); }); });