import { content } from "@deadhead/content"; import type { Contract } from "@deadhead/schema"; import { MS_PER_HOUR, realToGame } from "@deadhead/sim"; import { act, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useClock } from "../src/clock/clock"; import { formatGameHoursSpan, formatGameSpan, formatGameTime, formatSpan, formatTimeOfDay, } from "../src/clock/timeFormat"; import { formatClock } from "../src/format"; import { mapMotionStyle } from "../src/map/layout"; import { mapStyle } from "../src/map/style"; import { DEFAULT_PREFERENCES, usePreferences } from "../src/state/preferences"; import { fakeApi, renderApp, samplePlayerState } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); /** 4m 16s of real time is 4.27 game hours at 60× compression. */ const SPAN = 4 * 60_000 + 16_000; describe("time and span helpers (Step 10K.3)", () => { it("formats game time in 24-hour and 12-hour clocks", () => { const t = Date.parse("2100-03-14T13:05:00Z"); expect(formatGameTime(t, "24h")).toBe("2100-03-14 13:05"); expect(formatGameTime(t, "12h")).toMatch(/^2100-03-14 1:05\sPM$/); }); it("formats a real time of day in each clock mode", () => { const t = new Date(2100, 0, 1, 15, 7).getTime(); expect(formatTimeOfDay(t, "24h")).toBe("15:07"); expect(formatTimeOfDay(t, "12h")).toMatch(/^0?3:07\sPM$/); expect(formatTimeOfDay(t, "locale")).toBe( new Date(t).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), ); }); it("formats spans as real time, game time or both", () => { expect(formatSpan(SPAN, "real")).toBe("4m 16s"); expect(formatSpan(SPAN, "game")).toBe("4.3 game h"); expect(formatSpan(SPAN, "both")).toBe("4m 16s · 4.3 game h"); expect(formatSpan(SPAN, "both", formatClock)).toBe("04:16 · 4.3 game h"); expect(formatGameSpan(62.4)).toBe("62 game h"); expect(formatGameHoursSpan(5, "game")).toBe("5.0 game h"); expect(formatGameHoursSpan(5, "real")).toBe( formatSpan((5 * MS_PER_HOUR) / content.config.timeCompression, "real"), ); }); it("stills the map background with reduced motion", () => { expect(mapMotionStyle(true).parallax).toBe(0); expect(mapMotionStyle(true).backgroundZoomFollow).toBe(0); expect(mapMotionStyle(false).parallax).toBe(mapStyle.parallax); }); }); describe("display options in the app (Step 10K.3)", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => { vi.useRealTimers(); act(() => usePreferences.getState().set(DEFAULT_PREFERENCES)); }); it("switches the HQ clock to 24-hour", async () => { act(() => usePreferences.getState().set({ clock: "12h" })); renderApp(fakeApi({ getPlayerState: () => samplePlayerState({ tutorial: null }) }), "/"); const game = realToGame(NOW, content.config); expect(await screen.findByLabelText("Game clock")).toHaveTextContent( formatGameTime(game, "12h"), ); act(() => usePreferences.getState().set({ clock: "24h" })); expect(screen.getByLabelText("Game clock")).toHaveTextContent(formatGameTime(game, "24h")); }); it("shows contract expiry in both real and game time when asked", async () => { const job: Contract = { id: "c1", originId: "earth", destinationId: "moon", cargoTypeId: "standard", goodId: "consumer-goods", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: 0, massTonnes: 10, payout: 1234, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: NOW, expiresAt: NOW + SPAN, firstContact: false, }; renderApp( fakeApi({ getPlayerState: () => samplePlayerState({ tutorial: null }), getContractBoard: () => ({ contracts: [job], periodEndsAt: NOW + 600_000 }), }), "/contracts?location=earth", ); const card = (await screen.findByText("1,234 cr")).closest("tr")!; expect(card).toHaveTextContent("expires in 4m 16s"); act(() => usePreferences.getState().set({ countdown: "both" })); expect(card).toHaveTextContent("expires in 4m 16s · 4.3 game h"); }); });