1
0
Fork 0
polaris-task-force/tests/int/mission-reminder-toasts.int.spec.ts

150 lines
4.8 KiB
TypeScript

import { createElement } from "react";
import { act, cleanup, render, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { toast } from "sonner";
import { GAME_TICK_EVENT } from "@/hooks/useGameTick";
import { MissionReminderToasts } from "@/components/frontend/intelligence/MissionReminderToasts";
const routerPush = vi.hoisted(() => vi.fn());
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: routerPush }),
}));
vi.mock("sonner", () => {
const mockedToast = Object.assign(vi.fn(), { dismiss: vi.fn() });
return { toast: mockedToast };
});
type Leader = {
id: number;
username: string;
displayName: string;
};
type Reminder = {
missionId: number;
missionName: string;
missionCodeName: string;
completedAt: string;
leaders: readonly Leader[];
};
const reminder: Reminder = {
missionId: 42,
missionName: "Operation Nightfall",
missionCodeName: "OP:NIGHTFALL",
completedAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
leaders: [
{ id: 7, username: "leader-one", displayName: "Leader One" },
{ id: 8, username: "leader-two", displayName: "Leader Two" },
],
};
const fetchMock = vi.fn<typeof fetch>();
const mockedToast = vi.mocked(toast);
function responseFor(reminders: readonly Reminder[]): Response {
return new Response(JSON.stringify({ reminders }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
function firstToastOptions() {
const options = mockedToast.mock.calls[0]?.[1];
if (!options) throw new Error("Expected the first reminder toast to have options.");
return options;
}
function invokeButton(button: unknown): void {
if (typeof button !== "object" || button === null || !("onClick" in button)) {
throw new Error("Expected a Sonner button contract.");
}
const onClick = button.onClick;
if (typeof onClick !== "function") throw new Error("Expected a Sonner button handler.");
onClick(new MouseEvent("click"));
}
describe("MissionReminderToasts", () => {
beforeEach(() => {
cleanup();
vi.clearAllMocks();
localStorage.clear();
vi.stubGlobal("fetch", fetchMock);
mockedToast.mockReturnValue("mission-reminder:42:7");
});
afterEach(() => {
cleanup();
});
it("shows eligible leaders one at a time and links the action to the leader profile", async () => {
fetchMock.mockResolvedValue(responseFor([reminder]));
render(createElement(MissionReminderToasts));
await waitFor(() => expect(mockedToast).toHaveBeenCalledTimes(1));
expect(firstToastOptions().action).toMatchObject({ label: "Rate leader" });
expect(firstToastOptions().cancel).toMatchObject({ label: "Dismiss reminder" });
invokeButton(firstToastOptions().action);
expect(routerPush).toHaveBeenCalledWith("/profile/leader-one");
await waitFor(() => expect(mockedToast).toHaveBeenCalledTimes(2));
expect(mockedToast.mock.calls[1]?.[1]?.description).toContain("Leader Two");
});
it("does not issue a second request while a realtime refresh overlaps the initial poll", async () => {
let resolveFetch: ((value: Response) => void) | undefined;
fetchMock.mockImplementation(
() =>
new Promise<Response>((resolve) => {
resolveFetch = resolve;
}),
);
render(createElement(MissionReminderToasts));
act(() => window.dispatchEvent(new Event(GAME_TICK_EVENT)));
expect(fetchMock).toHaveBeenCalledTimes(1);
resolveFetch?.(responseFor([reminder]));
await waitFor(() => expect(mockedToast).toHaveBeenCalledTimes(1));
});
it("persists an explicit dismissal for that mission and leader only", async () => {
fetchMock.mockResolvedValue(responseFor([reminder]));
render(createElement(MissionReminderToasts));
await waitFor(() => expect(mockedToast).toHaveBeenCalledTimes(1));
invokeButton(firstToastOptions().cancel);
const stored = localStorage.getItem("ptf:mission-reminder:dismissals");
expect(stored).toContain('"42:7"');
expect(stored).not.toContain('"42:8"');
cleanup();
mockedToast.mockClear();
fetchMock.mockResolvedValue(responseFor([reminder]));
render(createElement(MissionReminderToasts));
await waitFor(() => expect(mockedToast).toHaveBeenCalledTimes(1));
expect(mockedToast.mock.calls[0]?.[1]?.description).toContain("Leader Two");
});
it("does not show a reminder whose completion window has expired", async () => {
const expiredReminder = {
...reminder,
completedAt: new Date(Date.now() - 73 * 60 * 60 * 1000).toISOString(),
};
fetchMock.mockResolvedValue(responseFor([expiredReminder]));
render(createElement(MissionReminderToasts));
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
await act(async () => {
await Promise.resolve();
});
expect(mockedToast).not.toHaveBeenCalled();
});
});