import type { Contract, PlayerState, Ship } from "@deadhead/schema"; import { screen, within } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useClock } from "../src/clock/clock"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const PERIOD_ENDS = NOW + 4 * 60_000 + 5_000; const job = (overrides: Partial): Contract => ({ id: "j", originId: "earth", destinationId: "moon", cargoTypeId: "standard", goodId: "consumer-goods", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: 0, massTonnes: 6, payout: 1_000, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: NOW, expiresAt: PERIOD_ENDS, firstContact: false, ...overrides, }); const courier: Ship = { id: "s1", modelId: "courier", name: "Dusty Mule", status: "idle", locationId: "earth", dispatchId: null, pilotId: "avatar", lastArrivalAt: 0, ...shipExtras("courier"), }; function owner(): PlayerState { const base = samplePlayerState({ tutorial: null }); return { ...base, bays: [{ id: "b", locationId: "earth" }], ships: [courier], pilots: [{ ...base.pilots[0]!, assignedShipId: "s1", locationId: null }], }; } function show(contracts: Contract[]) { renderApp( fakeApi({ getPlayerState: owner, getContractBoard: () => ({ contracts, periodEndsAt: PERIOD_ENDS }), }), "/contracts?location=earth", ); } describe("board cards and the empty board (Step 19.4, decision 0.5.114)", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("shows no permanent-problem badge on any card", async () => { // Jobs a lone level-0 Courier couldn't take: the engine filters them out, // and even if one reached a card it carries no problem badge. show([ job({ id: "heavy", massTonnes: 300, payout: 9_001 }), job({ id: "rushed", priority: true, deliveryWindowGameHours: 0.01, payout: 9_002 }), job({ id: "skilled", cargoTypeId: "heavy", goodId: "machine-parts", tier: 3, requiredSkill: { skillId: "heavy-cargo", level: 3 }, payout: 9_003, }), ]); for (const payout of ["9,001 cr", "9,002 cr", "9,003 cr"]) { const card = (await screen.findByText(payout)).closest("li")!; expect(card).not.toHaveTextContent(/too heavy/i); expect(card).not.toHaveTextContent(/too slow/i); expect(card).not.toHaveTextContent(/skill too low/i); } }); it("says no job fits the fleet, with the refresh countdown, Rest and Deadhead", async () => { show([]); const empty = await screen.findByText(/No jobs here your fleet can take right now/); expect(empty).toHaveTextContent("New contracts in 04:05."); const stuck = screen.getByRole("group", { name: "Nothing for Dusty Mule" }); expect(within(stuck).getByRole("button", { name: /^Rest / })).toBeInTheDocument(); expect(within(stuck).getByRole("button", { name: "Deadhead (fly empty)" })).toBeInTheDocument(); }); });