import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { localDungeonLeaderboard, localDungeonRunPlacement, saveLocalDungeonRun, submitDungeonRun } from "./dungeonLeaderboardClient"; import type { DungeonRunRecord } from "../game/dungeonRunTypes"; import catalog from "../game/generated/dungeonLeaderboardCatalog.generated.json"; const dungeon = catalog.dungeons.find((entry) => entry.id === "deadmines")!; const record = (id: string, durationMs: number, fullClear = true): DungeonRunRecord => ({ id, dungeonId: dungeon.id, difficultyId: dungeon.defaultDifficultyId, durationMs, finishedAt: 100000, party: [{ name: "Tester", classId: "priest", level: 20, role: "healer", npc: false }], killedBossIds: fullClear ? dungeon.bosses.map((boss) => boss.id) : [dungeon.finalBossId], fullClear, }); beforeEach(() => { const values = new Map(); vi.stubGlobal("localStorage", { getItem: (key: string) => values.get(key) ?? null, setItem: (key: string, value: string) => values.set(key, value) }); }); afterEach(() => vi.unstubAllGlobals()); describe("offline dungeon leaderboard", () => { it("isolates all ten difficulty levels, ranks, pages, and legacy records", () => { saveLocalDungeonRun(record("legacy-fastest", 1)); for (let difficulty = 1; difficulty <= 10; difficulty += 1) { const run = { ...record(`slider-run-${difficulty}`, 10000 + difficulty), bossDifficulty: difficulty }; saveLocalDungeonRun(run); expect(localDungeonLeaderboard(dungeon.id, dungeon.defaultDifficultyId, "final", 0, difficulty).entries .map((entry) => entry.id)).toEqual([run.id]); expect(localDungeonRunPlacement(run)).toEqual({ finalRank: 1, finalTotal: 1, fullRank: 1, fullTotal: 1 }); } expect(localDungeonLeaderboard(dungeon.id, dungeon.defaultDifficultyId, "final").total).toBe(1); }); it("persists full/final filters, ties, pages, and the placement of a run outside the first page", () => { for (let index = 0; index < 25; index += 1) saveLocalDungeonRun(record(`offline-${index}`, 1000 + index * 100)); const fastest = record("offline-fastest", 500, false); saveLocalDungeonRun(fastest); saveLocalDungeonRun(fastest); saveLocalDungeonRun(record("offline-tie", 1000)); const all = localDungeonLeaderboard(dungeon.id, dungeon.defaultDifficultyId, "final"); expect(all.total).toBe(27); expect(all.entries[0].id).toBe(fastest.id); expect(all.entries.slice(0, 3).map((entry) => entry.rank)).toEqual([1, 2, 2]); expect(localDungeonLeaderboard(dungeon.id, dungeon.defaultDifficultyId, "full").total).toBe(26); const page = localDungeonLeaderboard(dungeon.id, dungeon.defaultDifficultyId, "final", 20); expect(page.entries).toHaveLength(7); expect(localDungeonRunPlacement(record("offline-24", 3400))).toEqual({ finalRank: 27, finalTotal: 27, fullRank: 26, fullTotal: 26 }); }); it("retains the local time after a failed upload and uses the same run id when retried", async () => { const run = record("offline-retry-test", 4321); saveLocalDungeonRun(run); const fetch = vi.fn().mockRejectedValueOnce(new Error("offline")) .mockResolvedValueOnce(new Response(JSON.stringify({ placement: { finalRank: 2, finalTotal: 3, fullRank: 1, fullTotal: 2 } }), { status: 201 })); vi.stubGlobal("fetch", fetch); await expect(submitDungeonRun(run, null)).rejects.toThrow(); expect(localDungeonLeaderboard(dungeon.id, dungeon.defaultDifficultyId, "final").entries[0].durationMs).toBe(4321); await expect(submitDungeonRun(run, null)).resolves.toMatchObject({ finalRank: 2 }); expect(fetch.mock.calls.map((call) => JSON.parse(call[1].body).id)).toEqual([run.id, run.id]); }); });