import { content } from "@deadhead/content"; import type { Contract, Pilot, Ship } from "@deadhead/schema"; import { boardPeriodIndex, boardPeriodMs } from "@deadhead/sim"; import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useClock } from "../src/clock/clock"; import { formatClock } from "../src/format"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const period = boardPeriodMs(content.config); // A quarter of the way into a board period (derived from config, whatever its length). const NOW = Math.ceil(Date.parse("2100-01-01T00:00:00Z") / period) * period + period / 4; const periodEndsAt = (boardPeriodIndex(NOW, content.config) + 1) * period; const courier: Ship = { id: "s1", modelId: "courier", name: "Dusty Mule", status: "idle", locationId: "earth", dispatchId: null, lastArrivalAt: 0, ...shipExtras("courier"), pilotId: "p1", }; const rae: Pilot = { id: "p1", name: "Rae Okafor", isAvatar: false, isVeteran: false, xp: 0, level: 0, skills: {}, skillPointsAvailable: 0, assignedShipId: "s1", status: "idle", locationId: "earth", dutyHours: 3, dutyLimit: 12, resting: false, restEndsAt: null, }; /** Too heavy for a Courier. */ const heavyJob: Contract = { id: "c-heavy", originId: "earth", destinationId: "moon", cargoTypeId: "heavy", goodId: "machine-parts", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: 0, massTonnes: 300, payout: 9000, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: NOW, expiresAt: periodEndsAt, firstContact: false, }; const api = (contracts: Contract[]) => fakeApi({ getPlayerState: () => samplePlayerState({ tutorial: null, company: { id: "c", name: "D", credits: 10_000, loanBalance: 0 }, bays: [{ id: "b1", locationId: "earth" }], ships: [courier], pilots: [rae], }), getContractBoard: () => ({ contracts, periodEndsAt }), restPilot: () => ({ pilot: { ...rae, resting: true, restEndsAt: NOW + period } }), }); describe("board refresh and the nothing-here case (Step 10D.8)", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("counts down to the next board period", async () => { renderApp(api([heavyJob]), "/contracts?location=earth"); expect(await screen.findByLabelText("Board refresh")).toHaveTextContent( `New contracts in ${formatClock(periodEndsAt - NOW)}`, ); }); it("offers rest or a deadhead when no contract fits the idle ship, and each one acts", async () => { const client = api([heavyJob]); renderApp(client, "/contracts?location=earth"); const prompt = await screen.findByRole("group", { name: "Nothing for Dusty Mule" }); await userEvent.click( within(prompt).getByRole("button", { name: "Rest Rae Okafor — wakes to a new board" }), ); await waitFor(() => expect(client.call).toHaveBeenCalledWith("restPilot", { pilotId: "p1" })); await userEvent.click(within(prompt).getByRole("button", { name: "Deadhead (fly empty)" })); expect(screen.getByRole("dialog", { name: "Deadhead Dusty Mule" })).toBeInTheDocument(); }); it("does not prompt when the ship has a job it can take", async () => { renderApp(api([{ ...heavyJob, id: "c-light", massTonnes: 10 }]), "/contracts?location=earth"); await screen.findByLabelText("Board refresh"); expect(screen.queryByRole("group", { name: /Nothing for/ })).not.toBeInTheDocument(); }); });