import { afterEach, describe, expect, it, vi } from "vitest"; import { DUNGEON_REPORT_STATUSES, fetchDungeonReports, filterDungeonReports, sessionCanManageDungeonReports, submitDungeonReport, updateDungeonReport, type DungeonReport, } from "./dungeonReports"; import { useGameStore } from "../game/store"; function report(id: string, dungeonId: string, status: DungeonReport["status"]): DungeonReport { return { id, dungeonId, status, comment: "Door does not open", submittedBy: "Player", createdAt: 1, updatedAt: 1, updatedBy: null, revision: 0 }; } afterEach(() => { vi.unstubAllGlobals(); useGameStore.getState().closeOverlay(); }); describe("dungeon reports", () => { it("shows all statuses by default, including Closed, and combines status and dungeon filters", () => { const reports = DUNGEON_REPORT_STATUSES.flatMap((status, index) => [ report(`stock-${index}`, "stormwind-stockade", status), report(`cave-${index}`, "wailing-caverns", status), ]); expect(filterDungeonReports(reports, "all", "all")).toEqual(reports); expect(filterDungeonReports(reports, "Closed", "all")).toHaveLength(2); expect(filterDungeonReports(reports, "all", "wailing-caverns")).toHaveLength(4); expect(filterDungeonReports(reports, "Testing Fixes", "stormwind-stockade").map((item) => item.id)).toEqual(["stock-2"]); expect(filterDungeonReports(reports, "Submitted", "missing")).toEqual([]); }); it("allows GM report management in the ordinary release and rejects offline or local-only authority", () => { const gm = { kind: "account" as const, ownerId: "gm", displayName: "Phenom", accessToken: "token", roles: ["gm" as const] }; expect(sessionCanManageDungeonReports(gm)).toBe(true); expect(sessionCanManageDungeonReports({ ...gm, roles: [] })).toBe(false); expect(sessionCanManageDungeonReports({ ...gm, kind: "offline" })).toBe(false); expect(sessionCanManageDungeonReports({ ...gm, accessToken: undefined })).toBe(false); expect(sessionCanManageDungeonReports(null)).toBe(false); }); it("sends the active dungeon and preserves the expected report revision for GM updates", async () => { const item = report("report-id", "stormwind-stockade", "Submitted"); const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ report: item }) }); vi.stubGlobal("fetch", fetchMock); const gm = { kind: "account" as const, ownerId: "gm", displayName: "Phenom", accessToken: "token", roles: ["gm" as const] }; await submitDungeonReport(gm, item.dungeonId, "Door does not open"); expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ dungeonId: item.dungeonId, comment: "Door does not open" }); expect(fetchMock.mock.calls[0][1].headers.get("Authorization")).toBe("Bearer token"); await updateDungeonReport(gm, { ...item, revision: 3 }, "Closed"); expect(fetchMock.mock.calls[1][1].method).toBe("PATCH"); expect(JSON.parse(fetchMock.mock.calls[1][1].body)).toEqual({ status: "Closed", expectedRevision: 3 }); await fetchDungeonReports(); expect(fetchMock.mock.calls[2][0]).toBe("/api/dungeon-reports"); }); it("keeps the game paused and camera look stopped while reporting and returning to pause", () => { const game = useGameStore.getState(); game.setCameraLookActive(true); game.openOverlay("report-issue"); expect(useGameStore.getState()).toMatchObject({ paused: true, overlay: "report-issue", cameraLookActive: false }); game.openOverlay("pause"); expect(useGameStore.getState()).toMatchObject({ paused: true, overlay: "pause", cameraLookActive: false }); }); });