deadhead-space-freight-tycoon/apps/web/test/fleet-upgrades.test.tsx

131 lines
5.3 KiB
TypeScript

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> = {}): 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();
});
});