import { noNotifications, type NotificationScheduler, type ScheduledNotification, } from "@deadhead/engine"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createEngineClient } from "../src/api/engine-client"; import { createGame } from "../src/platform/game"; import { platformNotifications } from "../src/platform/notifications"; import { DEFAULT_PREFERENCES, usePreferences } from "../src/state/preferences"; class FakeScheduler implements NotificationScheduler { scheduled: ScheduledNotification[] | null = null; permissionAsked = 0; async replaceAll(list: ScheduledNotification[]) { this.scheduled = list; } async requestPermission() { this.permissionAsked++; } } afterEach(() => usePreferences.setState(DEFAULT_PREFERENCES)); describe("local notifications on the device (Step 14.9)", () => { it("uses the no-op scheduler on the web, and playing still works", async () => { expect(platformNotifications()).toBe(noNotifications); const game = createGame(); await game.lock.acquire(); const { state } = await game.engine.call("createGuest", {}); expect(state.tutorial).not.toBeNull(); await expect(game.engine.requestNotificationPermission()).resolves.toBeUndefined(); }); it("reschedules when the Settings toggle changes", async () => { const scheduler = new FakeScheduler(); const game = createGame(scheduler); await game.lock.acquire(); await game.engine.call("createGuest", {}); const [contract] = (await game.engine.call("getTutorialBoard", {})).contracts; const { dispatch } = await game.engine.call("createDispatch", { kind: "tutorial", contractId: contract!.id, }); expect(scheduler.scheduled).toMatchObject([{ at: dispatch.arrivalAt }]); usePreferences.getState().set({ notifications: false }); await vi.waitFor(() => expect(scheduler.scheduled).toEqual([])); usePreferences.getState().set({ notifications: true }); await vi.waitFor(() => expect(scheduler.scheduled).toMatchObject([{ at: dispatch.arrivalAt }])); }); it("asks for permission after the deal, not before", async () => { const scheduler = new FakeScheduler(); const game = createGame(scheduler); await game.lock.acquire(); const api = createEngineClient(game.engine); await api.call("createGuest", {}); expect(scheduler.permissionAsked).toBe(0); // The deal itself is covered by the engine's tests; here it just succeeds. vi.spyOn(game.engine, "call").mockResolvedValueOnce({} as never); await api.call("acceptDeal", {}); await vi.waitFor(() => expect(scheduler.permissionAsked).toBe(1)); }); it("tells the skill point pop-up when the permission prompt is answered (Step 17.6)", async () => { let answer!: () => void; const scheduler = new FakeScheduler(); scheduler.requestPermission = () => new Promise((resolve) => (answer = resolve)); const game = createGame(scheduler); await game.lock.acquire(); const api = createEngineClient(game.engine); // Before the deal there is nothing to wait for. await expect(api.notificationPrompt!()).resolves.toBeUndefined(); vi.spyOn(game.engine, "call").mockResolvedValueOnce({} as never); await api.call("acceptDeal", {}); let answered = false; void api.notificationPrompt!().then(() => (answered = true)); await Promise.resolve(); expect(answered).toBe(false); answer(); await vi.waitFor(() => expect(answered).toBe(true)); }); });