import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { PlayerSession } from "./types"; function storage() { const values = new Map(); return { getItem: (key: string) => values.get(key) ?? null, setItem: (key: string, value: string) => { values.set(key, value); }, removeItem: (key: string) => { values.delete(key); }, }; } const draft = { ownerId: "account-one", name: "Collector", raceId: "human" as const, classId: "priest" as const, specializationId: "priest:holy", gender: "female" as const, appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 }, }; const session: PlayerSession = { kind: "account", ownerId: draft.ownerId, displayName: "Collector", accessToken: "test-token" }; const cards = { counts: { "wow:lady anacondra": { normal: 7, rare: 1 } }, lastClaims: { spawn: "200:1:spawn" } }; beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); }); describe("account boss card integration", () => { it("keeps earned cards when switching characters and isolates account sign-in", async () => { vi.stubGlobal("window", { localStorage: storage(), sessionStorage: storage() }); const repository = await import("./accountRepository"); const { useShellStore } = await import("./shellStore"); const { useCombatStore } = await import("../game/combatStore"); const first = repository.createCharacter(draft).character!; const second = repository.createCharacter({ ...draft, name: "Other" }).character!; const offlineSession: PlayerSession = { ...session, accessToken: undefined }; const shell = useShellStore.getState(); shell.beginSession(offlineSession); useShellStore.setState({ phase: "game", activeCharacter: first }); shell.saveAccountBossCards(cards); shell.returnToCharacters(); shell.selectCharacter(second.id); expect(useShellStore.getState().bossCards).toEqual(cards); useCombatStore.getState().initializeCharacter({ ...second, bossCards: useShellStore.getState().bossCards }); useCombatStore.getState().collectSharedBossCards({ bossKey: "wow:lady anacondra", spawnId: "spawn", claimId: "300:1:spawn", rarities: ["normal"] }, "Lady Anacondra", 300); shell.saveAccountBossCards(useCombatStore.getState().bossCards); shell.selectCharacter(first.id); expect(useShellStore.getState().bossCards.counts["wow:lady anacondra"]?.normal).toBe(8); expect(repository.listCharacters(session.ownerId).every((character) => character.bossCards === undefined)).toBe(true); shell.signOut(); expect(useShellStore.getState().bossCards.counts).toEqual({}); shell.beginSession({ ...offlineSession, ownerId: "account-two" }); expect(useShellStore.getState().bossCards.counts).toEqual({}); shell.beginSession(offlineSession); expect(useShellStore.getState().bossCards.counts["wow:lady anacondra"]?.normal).toBe(8); }); it("uploads the account collection without characters and restores it on another device", async () => { const firstDevice = storage(); const secondDevice = storage(); vi.stubGlobal("window", { localStorage: firstDevice }); let cloudData: unknown = { version: 1, characters: [] }; const fetchMock = vi.fn(async (_url: string, init: RequestInit) => { if (init.method === "PUT") cloudData = JSON.parse(String(init.body)).data; return new Response(JSON.stringify({ revision: 1, updatedAt: 1, data: cloudData }), { status: 200 }); }); vi.stubGlobal("fetch", fetchMock); const repository = await import("./accountRepository"); const client = await import("./onlineAccountClient"); client.activateOnlineSession(session); repository.saveAccountBossCards(session.ownerId, cards); await vi.advanceTimersByTimeAsync(501); expect(cloudData).toMatchObject({ version: 2, characters: [], bossCards: cards }); expect(client.useCloudSaveStatus.getState().state).toBe("synced"); vi.stubGlobal("window", { localStorage: secondDevice }); await client.hydrateOnlineSession(session); expect(repository.loadAccountBossCards(session.ownerId)).toEqual(cards); expect(repository.loadAccountBossCards("different-account").counts).toEqual({}); const { collectBossCardDrop } = await import("../game/bossCards"); const restored = repository.loadAccountBossCards(session.ownerId); expect(collectBossCardDrop(restored, { bossKey: "wow:lady anacondra", spawnId: "spawn", claimId: "200:1:spawn", rarities: ["normal", "rare"] })).toBe(restored); await client.logoutOnlineSession(session); }); it("publishes legacy cloud migration once and preserves it through repeated downloads", async () => { vi.stubGlobal("window", { localStorage: storage() }); const repository = await import("./accountRepository"); const first = repository.createCharacter(draft).character!; const second = repository.createCharacter({ ...draft, name: "Other" }).character!; let cloudData: unknown = { version: 1, characters: [ { ...first, bossCards: cards }, { ...second, bossCards: { counts: { "wow:lady anacondra": { normal: 3 } }, lastClaims: {} } }, ] }; vi.stubGlobal("fetch", vi.fn(async (_url: string, init: RequestInit) => { if (init.method === "PUT") cloudData = JSON.parse(String(init.body)).data; return new Response(JSON.stringify({ revision: 1, updatedAt: 1, data: cloudData }), { status: 200 }); })); const client = await import("./onlineAccountClient"); await client.hydrateOnlineSession(session); expect(repository.loadAccountBossCards(session.ownerId).counts["wow:lady anacondra"]?.normal).toBe(10); await vi.advanceTimersByTimeAsync(501); expect(cloudData).toMatchObject({ version: 2, bossCards: { counts: { "wow:lady anacondra": { normal: 10, rare: 1 } } } }); expect((cloudData as { characters: { bossCards?: unknown }[] }).characters.every((character) => character.bossCards === undefined)).toBe(true); await client.hydrateOnlineSession(session); expect(repository.loadAccountBossCards(session.ownerId).counts["wow:lady anacondra"]?.normal).toBe(10); await client.logoutOnlineSession(session); }); });