import type { Dispatch, Ship } from "@deadhead/schema"; import { act, screen, within } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useClock } from "../src/clock/clock"; import { tutorialGoalCost } from "../src/screens/HQ"; import { fakeApi, renderApp, samplePlayerState } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const ship = (id: string, status: Ship["status"]): Ship => ({ id, modelId: "courier", name: id, status, locationId: status === "idle" ? "earth" : null, dispatchId: status === "idle" ? null : `d-${id}`, pilotId: null, }); const dispatch = (shipId: string, arrivalAt: number): Dispatch => ({ id: `d-${shipId}`, shipId, pilotId: "avatar", contract: null, originId: "earth", destinationId: "moon", tutorial: false, departureAt: NOW - 60_000, arrivalAt, distanceMillionKm: 0.39, fuelCost: 50, expectedPayout: 0, seed: 1, status: "in-flight", resolvedAt: null, }); describe("HQ screen", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("shows idle and in-flight counts and counts down to the next arrival", async () => { const state = samplePlayerState({ tutorial: null, ships: [ship("a", "idle"), ship("b", "in-flight"), ship("c", "in-flight")], activeDispatches: [dispatch("b", NOW + 125_000), dispatch("c", NOW + 65_000)], }); renderApp(fakeApi({ getPlayerState: () => state })); await screen.findByRole("heading", { level: 1, name: "HQ" }); const statValue = (label: string) => screen.getByText(label).nextElementSibling; expect(statValue("Ships idle")).toHaveTextContent("1"); expect(statValue("Ships in flight")).toHaveTextContent("2"); const next = screen.getByRole("heading", { name: "Next arrival" }).parentElement!; expect(within(next).getByText("1m 05s")).toBeInTheDocument(); act(() => vi.advanceTimersByTime(10_000)); expect(within(next).getByText("55s")).toBeInTheDocument(); expect(screen.queryByText(/Save up for your first ship/)).not.toBeInTheDocument(); }); it("shows tutorial goal progress as a percentage of a ship plus an Earth bay", async () => { const goal = tutorialGoalCost(); const state = samplePlayerState({ company: { id: "c", name: "D", credits: Math.round(goal * 0.4), loanBalance: 0 }, }); renderApp(fakeApi({ getPlayerState: () => state })); expect( await screen.findByText("Save up for your first ship and an Earth bay"), ).toBeInTheDocument(); expect(screen.getByRole("progressbar", { name: "Tutorial goal" })).toHaveAttribute( "aria-valuenow", "40", ); expect(screen.getByText("40%")).toBeInTheDocument(); expect(screen.getByText("Nothing in flight.")).toBeInTheDocument(); }); it("caps tutorial progress at 100%", async () => { const state = samplePlayerState({ company: { id: "c", name: "D", credits: tutorialGoalCost() * 3, loanBalance: 0 }, }); renderApp(fakeApi({ getPlayerState: () => state })); expect(await screen.findByText("100%")).toBeInTheDocument(); }); });