import type { Pilot, 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 { tabAttention } from "../src/layout/attention"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const ship = (id: string, pilotId: string): Ship => ({ id, modelId: "courier", name: id, status: "idle", locationId: "earth", dispatchId: null, lastArrivalAt: NOW - 60_000, ...shipExtras("courier"), pilotId, }); const base = samplePlayerState(); const pilot = (id: string, shipId: string, overrides: Partial = {}): Pilot => ({ ...base.pilots[0]!, id, name: id, isAvatar: id === "avatar", assignedShipId: shipId, locationId: null, ...overrides, }); const resting = { resting: true, restEndsAt: NOW + 2 * 60 * 60_000 }; /** Two ships at Earth: the avatar's and Rosa's, with the pilots as given. */ const owner = (avatar: Partial, rosa: Partial): PlayerState => samplePlayerState({ tutorial: null, bays: [{ id: "b1", locationId: "earth" }], ships: [ship("s1", "avatar"), ship("s2", "rosa")], pilots: [pilot("avatar", "s1", avatar), pilot("Rosa", "s2", { id: "rosa", ...rosa })], }); const show = (state: PlayerState) => renderApp( fakeApi({ getPlayerState: () => state, getContractBoard: () => ({ contracts: [], periodEndsAt: NOW + 30 * 60_000 }), }), "/contracts?location=earth", ); describe("ready ships on the Contracts screen (decision 0.5.134)", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("doesn't count a ship whose pilot is resting", async () => { show(owner({}, resting)); const group = await screen.findByRole("group", { name: "Location" }); const earth = within(group).getByRole("button", { name: /Earth/ }); expect(within(earth).getByLabelText("1 ship ready")).toHaveTextContent("1"); }); it("doesn't count an Independent pilot's ship", async () => { show(owner({}, { mode: "independent" })); const group = await screen.findByRole("group", { name: "Location" }); expect(within(group).getByLabelText("1 ship ready")).toBeInTheDocument(); }); it("says who is resting and when they're ready when no pilot here can fly", async () => { show(owner(resting, { ...resting, restEndsAt: NOW + 30 * 60_000 })); const group = await screen.findByRole("group", { name: "Location" }); expect(within(group).queryByLabelText(/ready/)).not.toBeInTheDocument(); const status = await screen.findByText(/No pilot here is ready to fly/); // The first to wake. expect(status).toHaveTextContent("Rosa is resting, ready in 30:00"); expect(screen.queryByText(/No idle ships here/)).not.toBeInTheDocument(); }); it("counts a pilot once their rest is over, without waiting for a new state", async () => { show(owner({ resting: true, restEndsAt: NOW - 1 }, resting)); const group = await screen.findByRole("group", { name: "Location" }); expect(within(group).getByLabelText("1 ship ready")).toBeInTheDocument(); }); it("leaves resting and Independent pilots' ships out of the Jobs badge", () => { expect(tabAttention(owner({}, {}))["/contracts"]).toBe(2); expect(tabAttention(owner({}, resting))["/contracts"]).toBe(1); expect(tabAttention(owner({}, { mode: "independent" }))["/contracts"]).toBe(1); expect(tabAttention(owner(resting, resting))["/contracts"]).toBe(0); }); });