Release Healer Man 0.1.38
This commit is contained in:
1 parent
361734bc41
commit
b417b7c3c3
48 files changed
+1982
-90
No files matched your search
@@ -0,0 +1,58 @@
|
||||
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 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { PlayerSession } from "./types";
|
||||
import { requestOnlineJson } from "./onlineAccountClient";
|
||||
import policy from "../../shared/dungeon-report-policy.json";
|
||||
|
||||
export type DungeonReportStatus = "Submitted" | "In-Progress" | "Testing Fixes" | "Closed";
|
||||
export const DUNGEON_REPORT_STATUSES = policy.statuses as readonly DungeonReportStatus[];
|
||||
export const DUNGEON_REPORT_COMMENT_MAX_LENGTH = policy.commentMaxLength;
|
||||
|
||||
export interface DungeonReport {
|
||||
readonly id: string;
|
||||
readonly dungeonId: string;
|
||||
readonly comment: string;
|
||||
readonly status: DungeonReportStatus;
|
||||
readonly submittedBy: string;
|
||||
readonly createdAt: number;
|
||||
readonly updatedAt: number;
|
||||
readonly updatedBy: string | null;
|
||||
readonly revision: number;
|
||||
}
|
||||
|
||||
export interface DungeonReportSnapshot {
|
||||
readonly reports: readonly DungeonReport[];
|
||||
readonly openCounts: Readonly<Record<string, number>>;
|
||||
}
|
||||
|
||||
export function sessionCanManageDungeonReports(session: PlayerSession | null): boolean {
|
||||
return Boolean(session?.kind === "account" && session.accessToken && session.roles?.includes("gm"));
|
||||
}
|
||||
|
||||
export function filterDungeonReports(
|
||||
reports: readonly DungeonReport[],
|
||||
status: DungeonReportStatus | "all",
|
||||
dungeonId: string,
|
||||
): readonly DungeonReport[] {
|
||||
return reports.filter((report) => (status === "all" || report.status === status)
|
||||
&& (dungeonId === "all" || report.dungeonId === dungeonId));
|
||||
}
|
||||
|
||||
export function fetchDungeonReports(): Promise<DungeonReportSnapshot> {
|
||||
return requestOnlineJson<DungeonReportSnapshot>("/api/dungeon-reports");
|
||||
}
|
||||
|
||||
export async function submitDungeonReport(session: PlayerSession | null, dungeonId: string, comment: string): Promise<DungeonReport> {
|
||||
const response = await requestOnlineJson<{ report: DungeonReport }>("/api/dungeon-reports", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ dungeonId, comment }),
|
||||
}, session?.kind === "account" ? session.accessToken : undefined);
|
||||
return response.report;
|
||||
}
|
||||
|
||||
export async function updateDungeonReport(
|
||||
session: PlayerSession,
|
||||
report: DungeonReport,
|
||||
status: DungeonReportStatus,
|
||||
): Promise<DungeonReport> {
|
||||
const response = await requestOnlineJson<{ report: DungeonReport }>(`/api/dungeon-reports/${encodeURIComponent(report.id)}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status, expectedRevision: report.revision }),
|
||||
}, session.accessToken);
|
||||
return response.report;
|
||||
}
|
||||
Reference in new issue
Block a user