import { content } from "@deadhead/content"; import type { Contract, PlayerState } from "@deadhead/schema"; import { checkRoute, gameToReal, realToGame, wouldStrand } from "@deadhead/sim"; import { screen, 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 { pairOptions } from "../src/screens/DispatchSheet"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const DAY = 24 * 3_600_000; const gameStart = realToGame(Date.parse(content.config.epoch.realStart), content.config); const clipper = content.shipModel("clipper")!; const [moon, mars] = [content.location("moon")!, content.location("mars")!]; const discovered = new Set(["earth", "moon", "mars"]); /** A date on which a Clipper can hop Moon→Mars but couldn't leave Mars (CR-012). */ const NOW = (() => { for (let d = 0; d < 16 * 365; d += 5) { const t = gameStart + d * DAY; if ( checkRoute(clipper, moon, mars, t).ok && wouldStrand(clipper, moon, mars, 10, t, discovered, content) ) { return gameToReal(t, content.config); } } throw new Error("no stranding date"); })(); const STRANDED = "Would be stranded — can't fly out of Mars from there"; /** A crewed Clipper at the Moon, with Mars discovered. */ function state(): PlayerState { const base = samplePlayerState({ tutorial: null }); return { ...base, company: { ...base.company, credits: 100_000 }, bays: [{ id: "b1", locationId: "moon" }], locations: base.locations.map((l) => ({ ...l, discovered: discovered.has(l.id) })), ships: [ { id: "s1", modelId: "clipper", name: "Swift Heron", status: "idle", locationId: "moon", dispatchId: null, lastArrivalAt: 0, ...shipExtras("clipper"), pilotId: "avatar", }, ], pilots: [{ ...base.pilots[0]!, assignedShipId: "s1", locationId: "moon" }], }; } const toMars: Contract = { id: "c1", originId: "moon", destinationId: "mars", cargoTypeId: "standard", goodId: "consumer-goods", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: 0, massTonnes: 10, payout: 50_000, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: NOW, expiresAt: NOW + 3_600_000, firstContact: false, }; describe("stranding reason (Step 10L.9)", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("marks a would-strand ship and pilot pair with the reason", () => { const [pair] = pairOptions(state(), toMars, NOW); expect(pair!.problems).toEqual([STRANDED]); }); it("disables a would-strand deadhead destination with the reason", async () => { renderApp(fakeApi({ getPlayerState: state }), "/fleet"); const card = (await screen.findByText("Swift Heron")).closest("li")!; await userEvent.click(within(card).getByRole("button", { name: /Deadhead/ })); const dialog = screen.getByRole("dialog", { name: "Deadhead Swift Heron" }); const option = within(dialog).getByRole("button", { name: "Mars" }); expect(option).toBeDisabled(); expect(option).toHaveTextContent(STRANDED); expect(within(dialog).getByRole("button", { name: "Earth" })).toBeEnabled(); }); });