import { content } from "@deadhead/content"; import type { Ship } from "@deadhead/schema"; import { effectiveStats, upgradeCost } from "@deadhead/sim"; import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useClock } from "../src/clock/clock"; import { formatCredits, formatMillionKm } from "../src/format"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const freighter = content.shipModel("freighter")!; const ship = (id: string, name: string, overrides: Partial = {}): Ship => ({ id, name, modelId: "freighter", status: "idle", locationId: "earth", dispatchId: null, pilotId: null, lastArrivalAt: 0, ...shipExtras(overrides.modelId ?? "freighter", overrides.upgrades ?? {}), ...overrides, }); const state = (ships: Ship[], credits = 1_000_000) => samplePlayerState({ tutorial: null, company: { id: "c", name: "D", credits, loanBalance: 0 }, bays: [{ id: "b1", locationId: "earth" }], ships, pilots: [], }); async function openUpgrades(shipName: string) { const card = (await screen.findByText(shipName)).closest("li")!; await userEvent.click(within(card).getByRole("button", { name: `Upgrades for ${shipName}` })); return screen.getByRole("dialog", { name: shipName }); } const row = (dialog: HTMLElement, name: string) => within(dialog).getByRole("listitem", { name }); describe("upgrade shop on the Fleet screen (Step 10I.6)", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("shows each upgrade's next level as before → after, from the sim, with cost and time", async () => { renderApp( fakeApi({ getPlayerState: () => state([ship("s1", "Slow Ox", { upgrades: { engines: 1 } })]), }), "/fleet", ); const dialog = await openUpgrades("Slow Ox"); const before = effectiveStats(freighter, { engines: 1 }, content).cruiseSpeedKmPerGameHour; const after = effectiveStats(freighter, { engines: 2 }, content).cruiseSpeedKmPerGameHour; const engines = row(dialog, "Engine Tuning"); expect(engines).toHaveTextContent("Level 1/3"); expect(engines).toHaveTextContent( `Speed ${formatMillionKm(before / 1e6)}/h → ${formatMillionKm(after / 1e6)}/h`, ); expect(engines).toHaveTextContent(formatCredits(upgradeCost(freighter, 2, content.config))); expect(engines).toHaveTextContent("20m 00s in the shop"); expect(row(dialog, "Cargo Bay Expansion")).toHaveTextContent("Capacity 100 t → 115 t"); }); it("shows why an upgrade can't start", async () => { const ships = [ ship("s-moon", "Away Ox", { locationId: "moon" }), ship("s-fly", "Busy Ox", { status: "in-flight", locationId: null, dispatchId: "d" }), ship("s-shop", "Shop Ox", { inShopUntil: NOW + 60_000 }), ship("s-max", "Max Ox", { upgrades: { engines: 3 } }), ]; renderApp(fakeApi({ getPlayerState: () => state(ships, 100) }), "/fleet"); const reasons: [string, string, string][] = [ ["Away Ox", "Engine Tuning", "Not at an HQ"], ["Busy Ox", "Engine Tuning", "In flight"], ["Shop Ox", "Engine Tuning", "In the shop"], ["Max Ox", "Engine Tuning", "Maxed"], ["Max Ox", "Cargo Bay Expansion", "Can't afford"], ]; for (const [shipName, upgrade, reason] of reasons) { const dialog = await openUpgrades(shipName); expect(row(dialog, upgrade), `${shipName} ${upgrade}`).toHaveTextContent(reason); expect(within(row(dialog, upgrade)).queryByRole("button")).not.toBeInTheDocument(); await userEvent.click(within(dialog).getByRole("button", { name: "Close" })); } }); it("starts an upgrade after confirming", async () => { const api = fakeApi({ getPlayerState: () => state([ship("s1", "Slow Ox")]), startUpgrade: ({ shipId }) => ({ ship: ship(shipId, "Slow Ox", { upgrades: { "cargo-bay": 1 } }), }), }); renderApp(api, "/fleet"); const dialog = await openUpgrades("Slow Ox"); await userEvent.click( within(row(dialog, "Cargo Bay Expansion")).getByRole("button", { name: "Upgrade to level 1", }), ); const confirm = within(dialog).getByRole("group", { name: "Start Cargo Bay Expansion 1?" }); expect(confirm).toHaveTextContent("10m 00s"); await userEvent.click(within(confirm).getByRole("button", { name: "Start upgrade" })); await waitFor(() => expect(api.call).toHaveBeenCalledWith("startUpgrade", { shipId: "s1", upgradeId: "cargo-bay", }), ); }); it("shows a ship in the shop with a countdown instead of its actions", async () => { renderApp( fakeApi({ getPlayerState: () => state([ship("s1", "Slow Ox", { inShopUntil: NOW + 7.5 * 60_000 })]), }), "/fleet", ); const card = (await screen.findByText("Slow Ox")).closest("li")!; expect(within(card).getByLabelText("Slow Ox shop")).toHaveTextContent("In the shop — 07:30"); expect(within(card).queryByRole("button", { name: /Deadhead/ })).not.toBeInTheDocument(); }); });