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 { pilotExtras } from "./fixtures"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const MINUTE = 60_000; const ship = (over: Partial = {}): Ship => ({ id: "s1", name: "Wren", modelId: "courier", status: "idle", locationId: "earth", dispatchId: null, pilotId: "p1", lastArrivalAt: 0, ...shipExtras("courier"), ...over, }); const pilot = (over: Partial = {}): Pilot => ({ ...samplePlayerState().pilots[0]!, id: "p1", name: "Rosa", isAvatar: false, assignedShipId: "s1", locationId: null, ...pilotExtras, ...over, }); const state = (s: Ship, pilots: Pilot[]): PlayerState => samplePlayerState({ tutorial: null, company: { id: "c", name: "D", credits: 50_000, loanBalance: 0 }, ships: [s], pilots: [samplePlayerState().pilots[0]!, ...pilots], }); async function card(s: Ship, pilots: Pilot[] = [pilot()]) { renderApp(fakeApi({ getPlayerState: () => state(s, pilots) }), "/fleet"); return (await screen.findByText("Wren")).closest("li")!; } const resting = (minutes: number) => pilot({ resting: true, restEndsAt: NOW + minutes * MINUTE }); describe('"Find work here" when the ship can\'t go (decision 0.5.164)', () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("links to the board when the ship can be sent", async () => { const li = await card(ship()); expect(within(li).getByRole("link", { name: "Find work here" })).toHaveAttribute( "href", "/contracts?location=earth", ); }); it("is disabled with the rest countdown while the pilot rests", async () => { const li = await card(ship(), [resting(4.2)]); const button = within(li).getByRole("button", { name: /^Find work here: Resting/ }); expect(button).toBeDisabled(); expect(button).toHaveTextContent("Resting · 04:12"); expect(within(li).queryByRole("link", { name: "Find work here" })).not.toBeInTheDocument(); }); it("is disabled with the shop countdown while the ship is in the shop", async () => { const li = await card(ship({ inShopUntil: NOW + 12.5 * MINUTE })); expect( within(li).getByRole("button", { name: /^Find work here: In the shop/ }), ).toHaveTextContent("In the shop · 12:30"); }); it("counts down to the later of the two when both apply", async () => { const shopFirst = await card(ship({ inShopUntil: NOW + 5 * MINUTE }), [resting(9)]); expect(within(shopFirst).getByRole("button", { name: /Find work here/ })).toHaveTextContent( "Resting · 09:00", ); }); it("uses the shop's end when it outlasts the rest, as with service overlapping rest", async () => { const li = await card(ship({ inShopUntil: NOW + 20 * MINUTE }), [resting(10)]); expect(within(li).getByRole("button", { name: /Find work here/ })).toHaveTextContent( "In the shop · 20:00", ); }); it("says No pilot assigned for an uncrewed ship", async () => { const li = await card(ship({ pilotId: null }), []); const button = within(li).getByRole("button", { name: /Find work here/ }); expect(button).toBeDisabled(); expect(button).toHaveTextContent("No pilot assigned"); }); it("covers an Independent pilot's ship too", async () => { const li = await card(ship(), [{ ...resting(3), mode: "independent" }]); expect(within(li).getByRole("button", { name: /Find work here/ })).toBeDisabled(); }); it("re-enables on its own when the countdown reaches zero", async () => { const li = await card(ship(), [resting(1)]); expect(within(li).getByRole("button", { name: /Find work here/ })).toBeDisabled(); act(() => useClock.setState({ now: NOW + MINUTE })); expect(within(li).getByRole("link", { name: "Find work here" })).toBeInTheDocument(); }); });