import { content } from "@deadhead/content"; import type { PlayerState, Ship } from "@deadhead/schema"; import { tripFuel } from "@deadhead/sim"; import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const [earth, mars] = [content.location("earth")!, content.location("mars")!]; const clipper = content.shipModel("clipper")!; const marsLegFu = tripFuel(clipper, earth, mars, content.config); const ship = (fuelFu: number): Ship => ({ id: "s1", name: "Swift Heron", modelId: "clipper", status: "idle", locationId: "earth", dispatchId: null, lastArrivalAt: 0, ...shipExtras("clipper"), fuelFu, fuelAvgPrice: 8, refuelPolicy: "manual", pilotId: null, }); /** A Clipper at Earth with Mars discovered, `fuelFu` aboard; refuels fill the tank. */ function company(fuelFu: number, discovered = ["earth", "moon", "mars"]) { let state: PlayerState = samplePlayerState({ tutorial: null, company: { id: "c", name: "D", credits: 100_000, loanBalance: 0 }, bays: [{ id: "b1", locationId: "earth" }], ships: [ship(fuelFu)], locations: content.locations.map((l) => ({ id: l.id, discovered: discovered.includes(l.id) })), }); const refuelShip = vi.fn(() => { const refuelled = { ...state.ships[0]!, fuelFu: clipper.tankCapacityFu }; state = { ...state, ships: [refuelled] }; return { ship: refuelled }; }); return fakeApi({ getPlayerState: () => state, refuelShip }); } describe("low-fuel warning (Step 18.4, decision 0.5.113)", () => { it("warns on a Clipper at Earth that can't fly to Mars, and clears once refuelled", async () => { renderApp(company(marsLegFu - 1), "/fleet"); const warning = await screen.findByLabelText("Swift Heron low fuel"); expect(warning).toHaveTextContent("Low fuel"); // Earth's longest leg for a Clipper among the places known here is Mars. expect(warning).toHaveTextContent( `Mars needs ${Math.round(marsLegFu).toLocaleString("en-US")} fu`, ); await userEvent.click(screen.getByRole("button", { name: "Fuel for Swift Heron" })); const sheet = screen.getByRole("dialog", { name: "Swift Heron" }); await userEvent.click(within(sheet).getByRole("button", { name: /^Fill/ })); await waitFor(() => expect(screen.queryByLabelText("Swift Heron low fuel")).not.toBeInTheDocument(), ); }); it("doesn't warn with enough for the longest leg", async () => { renderApp(company(marsLegFu + 1), "/fleet"); await screen.findByRole("progressbar", { name: "Swift Heron fuel" }); expect(screen.queryByLabelText("Swift Heron low fuel")).not.toBeInTheDocument(); }); it("measures only legs to discovered places", async () => { // Mars undiscovered: the Moon is the longest leg, and a little fuel covers it. renderApp(company(marsLegFu - 1, ["earth", "moon"]), "/fleet"); await screen.findByRole("progressbar", { name: "Swift Heron fuel" }); expect(screen.queryByLabelText("Swift Heron low fuel")).not.toBeInTheDocument(); }); it("shows Manual selected, as the default, in a new ship's fuel sheet", async () => { renderApp(company(clipper.tankCapacityFu), "/fleet"); await userEvent.click(await screen.findByRole("button", { name: "Fuel for Swift Heron" })); const policy = within(screen.getByRole("dialog")).getByRole("group", { name: "Refuel policy" }); const radios = within(policy).getAllByRole("radio"); // Manual comes first and is checked. expect(radios.map((r) => r.parentElement!.textContent)).toEqual([ "Manual refuel", "Auto top-up", ]); expect(within(policy).getByRole("radio", { name: "Manual refuel" })).toBeChecked(); expect(policy).toHaveTextContent("The default"); }); });