import { content } from "@deadhead/content"; import { realToGame } from "@deadhead/sim"; import { act, render } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ApiClient } from "../src/api/client"; import { serverNow, startClockTicker, syncClock, useClock } from "../src/clock/clock"; import { formatGameTime, GameClock } from "../src/clock/GameClock"; const DEVICE_NOW = Date.parse("2100-01-01T00:00:00Z"); const TEN_MINUTES = 10 * 60_000; /** An API whose server clock runs `aheadMs` ahead of the device. */ function serverAhead(aheadMs: number): ApiClient { return { call: vi.fn(async () => ({ realTime: Date.now() + aheadMs, gameTime: 0 })), } as unknown as ApiClient; } describe("server-synced clock", () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(DEVICE_NOW); useClock.setState({ offsetMs: 0, synced: false, now: DEVICE_NOW }); }); afterEach(() => vi.useRealTimers()); it("returns a time 10 minutes ahead when the server is 10 minutes ahead", async () => { await syncClock(serverAhead(TEN_MINUTES)); expect(useClock.getState().synced).toBe(true); expect(serverNow()).toBe(DEVICE_NOW + TEN_MINUTES); vi.advanceTimersByTime(5000); expect(serverNow()).toBe(DEVICE_NOW + TEN_MINUTES + 5000); }); it("works when the server is behind the device", async () => { await syncClock(serverAhead(-30_000)); expect(serverNow()).toBe(DEVICE_NOW - 30_000); }); it("ticks the store every second", async () => { await syncClock(serverAhead(TEN_MINUTES)); const stop = startClockTicker(); vi.advanceTimersByTime(3000); expect(useClock.getState().now).toBe(DEVICE_NOW + TEN_MINUTES + 3000); stop(); vi.advanceTimersByTime(3000); expect(useClock.getState().now).toBe(DEVICE_NOW + TEN_MINUTES + 3000); }); it("shows the in-game time in the game clock and keeps it ticking", async () => { await syncClock(serverAhead(0)); const stop = startClockTicker(); const { container } = render(); // Date and time are separate spans (the date hides on phones). const clock = () => container.querySelector("time")!; // The default clock follows the device locale (CR-011). const at = (real: number) => formatGameTime(realToGame(real, content.config), "locale"); expect(clock()).toHaveTextContent(at(DEVICE_NOW)); // One real minute is one game hour. act(() => vi.advanceTimersByTime(60_000)); expect(clock()).toHaveTextContent(at(DEVICE_NOW + 60_000)); stop(); }); });