131 lines
5.3 KiB
TypeScript
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();
|
|
});
|
|
});
|