import type { Contract, Ship } from "@deadhead/schema"; import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it } from "vitest"; import { usePreferences } from "../src/state/preferences"; import { pairOptions } from "../src/screens/DispatchSheet"; import { jobFigures } from "../src/screens/jobFigures"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const NOW = Date.now(); const job = (overrides: Partial): Contract => ({ id: "j1", locationId: "earth", originId: "earth", destinationId: "moon", goodId: "consumer-goods", cargoTypeId: "standard", massTonnes: 10, payout: 2000, shipper: "Pacific Orbital Logistics", offeredAt: NOW, expiresAt: NOW + 3_600_000, priority: false, deliveryWindowGameHours: null, tier: null, requiredSkill: null, requiredLongHaulLevel: 0, firstContact: false, ...overrides, }) as Contract; const ship = (id: string, overrides: Partial = {}): Ship => ({ id, modelId: "courier", name: id, status: "idle", locationId: "earth", dispatchId: null, lastArrivalAt: 0, ...shipExtras("courier"), pilotId: "avatar", ...overrides, }); function owner(ships: Ship[]) { const base = samplePlayerState(); return samplePlayerState({ tutorial: null, bays: [{ id: "b", locationId: "earth" }], ships, pilots: [{ ...base.pilots[0]!, assignedShipId: ships[0]?.id ?? null }], }); } function show(contracts: Contract[], ships: Ship[]) { renderApp( fakeApi({ getPlayerState: () => owner(ships), getContractBoard: () => ({ contracts, periodEndsAt: NOW + 3_600_000 }), }), "/contracts?location=earth", ); } const rowNames = () => screen .getAllByRole("row") .map((r) => r.getAttribute("aria-label")) .filter(Boolean); describe("jobs table (decision 0.5.169)", () => { beforeEach(() => usePreferences.setState({ jobSort: "net" })); it("shows the same net as the dispatch sheet", () => { const state = owner([ship("s1")]); const contract = job({}); const figures = jobFigures(state, contract, NOW); expect(figures.ready).toBe(true); expect(figures.estimate).toBe(false); expect(figures.net).toBe(pairOptions(state, contract, NOW)[0]!.netProfit); expect(figures.perHour).toBe(Math.round(figures.net! / figures.hours!)); }); it("sorts by net, then by credits per hour, and remembers the choice", async () => { const big = job({ id: "big", goodId: "packaged-food", payout: 3000, destinationId: "moon", massTonnes: 18, }); const small = job({ id: "small", goodId: "mail", payout: 2000, massTonnes: 1 }); show([small, big], [ship("s1")]); await screen.findByRole("table"); expect(rowNames()).toEqual(["Packaged Food", "Mail & Parcels"]); const net = screen.getByRole("columnheader", { name: /Net/ }); expect(net).toHaveAttribute("aria-sort", "descending"); await userEvent.click(within(screen.getByRole("table")).getByRole("button", { name: /cr\/h/ })); expect(screen.getByRole("columnheader", { name: /cr\/h/ })).toHaveAttribute( "aria-sort", "descending", ); expect(net).toHaveAttribute("aria-sort", "none"); expect(usePreferences.getState().jobSort).toBe("rate"); }); it("keeps jobs it can't take below, with the reason and an estimate", async () => { // The only ship is away: the figures are an estimate, and the row says why. const away = ship("s1", { status: "in-flight", locationId: null, dispatchId: "d" }); show([job({})], [away]); await screen.findByRole("table"); expect(screen.getByText("Can't take yet")).toBeInTheDocument(); const row = screen.getByRole("row", { name: "Consumer Goods" }); expect(row).toHaveTextContent("No idle ship at Earth"); expect(within(row).getByLabelText("Net, estimated")).toHaveTextContent(/^≈/); }); it("opens the job from its row", async () => { show([job({})], [ship("s1")]); await userEvent.click(await screen.findByRole("button", { name: "Dispatch" })); await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument()); }); });