import type { Pilot, PlayerState, 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 { independentStatus } from "../src/components/IndependentPilots"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const SHOP_ENDS = NOW + 12 * 60_000 + 34_000; const ship: Ship = { id: "s1", modelId: "courier", name: "Wren", status: "idle", locationId: "earth", dispatchId: null, pilotId: "p1", lastArrivalAt: NOW - 60_000, ...shipExtras("courier"), inShopUntil: SHOP_ENDS, }; const base = samplePlayerState(); const ada = (overrides: Partial): Pilot => ({ ...base.pilots[0]!, id: "p1", name: "Ada", isAvatar: false, dutyHours: 11, ...overrides, }); /** Ada waiting with the ship in the shop, or unassigned and left at the HQ. */ const inShop = ada({ assignedShipId: "s1", locationId: "earth", restingInShopUntil: SHOP_ENDS }); const unassigned = ada({ assignedShipId: null, locationId: "earth", restingInShopUntil: null }); const owner = (pilot: Pilot): PlayerState => samplePlayerState({ tutorial: null, bays: [{ id: "b1", locationId: "earth" }], ships: [{ ...ship, pilotId: pilot.assignedShipId ? "p1" : null }], pilots: [base.pilots[0]!, pilot], }); describe("resting while in the shop (Step 23.3, decision 0.5.135)", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("shows the shop countdown as rest, and drops it once the pilot is unassigned", async () => { let state = owner(inShop); const { queryClient } = renderApp(fakeApi({ getPlayerState: () => state }), "/pilots"); const card = (await screen.findByText("Ada")).closest("li")!; expect(within(card).getByLabelText("Ada rest")).toHaveTextContent( "Resting while in the shop — 12:34", ); // No duty bar while the shop counts as rest. expect(within(card).queryByRole("progressbar", { name: "Ada duty" })).not.toBeInTheDocument(); state = owner(unassigned); await act(() => queryClient.invalidateQueries()); const after = (await screen.findByText("Ada")).closest("li")!; expect(after).not.toHaveTextContent(/Resting/); expect(within(after).getByRole("progressbar", { name: "Ada duty" })).toBeInTheDocument(); }); it("counts down with the clock", async () => { renderApp(fakeApi({ getPlayerState: () => owner(inShop) }), "/pilots"); const card = (await screen.findByText("Ada")).closest("li")!; act(() => useClock.setState({ now: NOW + 4 * 60_000 })); expect(within(card).getByLabelText("Ada rest")).toHaveTextContent("08:34"); }); it("says so in an Independent pilot's status line", () => { const state = owner({ ...inShop, mode: "independent" }); expect(independentStatus(state, state.pilots[1]!)).toMatch(/^Resting while in the shop until /); }); });