import type { Contract, PlayerState } from "@deadhead/schema"; 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 { tutorialHint } from "../src/screens/HQ"; import { fakeApi, renderApp, samplePlayerState } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const job = ( id: string, massTonnes: number, skill: { skillId: string; level: number } | null, ): Contract => ({ id, originId: "earth", destinationId: "moon", cargoTypeId: skill ? "high-value" : "standard", goodId: skill ? "medical-supplies" : "consumer-goods", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: skill?.level ?? 0, massTonnes, payout: 1000, requiredSkill: skill, requiredLongHaulLevel: 0, offeredAt: NOW, expiresAt: NOW + 3_600_000, firstContact: false, }); /** Just after the deal: one crewed Courier at Earth, as in the user's report. */ function afterDeal(): PlayerState { const base = samplePlayerState({ tutorial: null }); return { ...base, company: { ...base.company, credits: 1652, loanBalance: 5000 }, bays: [{ id: "b1", locationId: "earth" }], ships: [ { id: "s1", modelId: "courier", name: "Copper Wren", status: "idle", locationId: "earth", dispatchId: null, lastArrivalAt: 0, pilotId: "avatar", }, ], pilots: [{ ...base.pilots[0]!, assignedShipId: "s1" }], }; } describe("dispatch sheet explains each job", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("shows each job's own details and every problem, so different jobs look different", async () => { const board = [ job("skill", 228, { skillId: "expensive-cargo", level: 2 }), job("heavy", 69, null), ]; renderApp( fakeApi({ getPlayerState: afterDeal, getContractBoard: () => ({ contracts: board, periodEndsAt: NOW + 3_600_000 }), }), "/contracts", ); const [skillJob, heavyJob] = await screen.findAllByRole("button", { name: "Dispatch" }); await userEvent.click(skillJob!); let sheet = screen.getByRole("dialog"); expect(within(sheet).getByLabelText("Job details")).toHaveTextContent( "High-Value (tier 2) · 228 t", ); expect(within(sheet).getByLabelText("Job details")).toHaveTextContent( "needs Expensive Cargo 2", ); const problems = within(sheet).getByRole("list", { name: "Problems" }); expect(problems).toHaveTextContent("Too heavy: 228 t (holds 20 t)"); expect(problems).toHaveTextContent("Needs Expensive Cargo 2 (pilot has 0)"); await userEvent.click(within(sheet).getByRole("button", { name: "Cancel" })); await userEvent.click(heavyJob!); sheet = screen.getByRole("dialog"); expect(within(sheet).getByLabelText("Job details")).toHaveTextContent("Standard · 69 t"); expect(within(sheet).getByLabelText("Job details")).toHaveTextContent("no skills needed"); const heavyProblems = within(sheet).getByRole("list", { name: "Problems" }); expect(heavyProblems).toHaveTextContent("Too heavy: 69 t (holds 20 t)"); expect(heavyProblems).not.toHaveTextContent("Expensive Cargo"); }); it("lets a job the Courier can carry be confirmed", async () => { renderApp( fakeApi({ getPlayerState: afterDeal, getContractBoard: () => ({ contracts: [job("ok", 15, null)], periodEndsAt: NOW + 3_600_000, }), }), "/contracts", ); await userEvent.click(await screen.findByRole("button", { name: "Dispatch" })); const sheet = screen.getByRole("dialog"); expect(within(sheet).queryByRole("list", { name: "Problems" })).not.toBeInTheDocument(); expect(within(sheet).getByRole("button", { name: "Confirm" })).toBeEnabled(); }); }); describe("tutorial hint on HQ", () => { const tutorial = (runsCompleted: number, loaner: "earth" | "moon", extra = {}) => ({ loanerLocationId: loaner, runInFlight: false, runsCompleted, dealAvailable: false, ...extra, }); it("asks for one more run back to Earth when enough runs are done at the Moon", () => { expect(tutorialHint(tutorial(4, "moon"), 4)).toMatch( /One more run: bring the ship back to Earth/, ); expect(tutorialHint(tutorial(4, "moon", { runInFlight: true }), 4)).toMatch(/land at Earth/); }); it("says keep flying before enough runs, and points at the offer when it is ready", () => { expect(tutorialHint(tutorial(2, "earth"), 4)).toMatch(/Keep flying/); expect(tutorialHint(tutorial(4, "earth", { dealAvailable: true }), 4)).toMatch(/offer/); }); });