deadhead-space-freight-tycoon/apps/web/test/location-boards.test.tsx

166 lines
6.1 KiB
TypeScript

import type { Contract, Ship } from "@deadhead/schema";
import { screen, 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 { fakeApi, renderApp, samplePlayerState } from "./render";
const NOW = Date.parse("2100-01-01T00:00:00Z");
const contract = (overrides: Partial<Contract>): Contract => ({
id: "c",
originId: "earth",
destinationId: "moon",
cargoTypeId: "standard",
goodId: "consumer-goods",
shipper: "Pacific Orbital Logistics",
priority: false,
deliveryWindowGameHours: null,
tier: 0,
massTonnes: 50,
payout: 2000,
requiredSkill: null,
requiredLongHaulLevel: 0,
offeredAt: NOW,
expiresAt: NOW + 30 * 60_000,
firstContact: false,
...overrides,
});
const boards: Record<string, Contract[]> = {
earth: [
contract({ id: "e1", payout: 1999 }),
contract({
id: "e2",
destinationId: "mars",
cargoTypeId: "heavy",
goodId: "machine-parts",
shipper: "Pacific Orbital Logistics",
priority: false,
deliveryWindowGameHours: null,
tier: 2,
requiredSkill: { skillId: "heavy-cargo", level: 2 },
requiredLongHaulLevel: 1,
firstContact: true,
payout: 88_000,
}),
],
moon: [contract({ id: "m1", originId: "moon", destinationId: "earth", payout: 1234 })],
};
const ship = (id: string, overrides: Partial<Ship>): Ship => ({
id,
modelId: "courier",
name: id,
status: "idle",
locationId: "earth",
dispatchId: null,
pilotId: null,
lastArrivalAt: NOW - 60_000,
...overrides,
});
/** Only an Earth bay; one idle ship at Earth, two at the Moon (the newest arrival). */
const owner = () =>
samplePlayerState({
tutorial: null,
bays: [{ id: "b1", locationId: "earth" }],
ships: [
ship("s1", { locationId: "earth", lastArrivalAt: NOW - 120_000 }),
ship("s2", { locationId: "moon", lastArrivalAt: NOW - 1000 }),
ship("s3", { locationId: "moon", lastArrivalAt: NOW - 90_000 }),
ship("s4", { status: "in-flight", locationId: null, dispatchId: "d", lastArrivalAt: NOW }),
],
});
const boardApi = () =>
fakeApi({
getPlayerState: owner,
getContractBoard: ({ locationId }) => ({
contracts: boards[locationId] ?? [],
periodEndsAt: NOW + 30 * 60_000,
}),
});
describe("location contract boards", () => {
beforeEach(() => {
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
vi.setSystemTime(NOW);
useClock.setState({ offsetMs: 0, synced: true, now: NOW });
});
afterEach(() => vi.useRealTimers());
it("lists every discovered location, none undiscovered, with idle-ship badges", async () => {
renderApp(boardApi(), "/contracts");
const group = await screen.findByRole("group", { name: "Location" });
const names = within(group)
.getAllByRole("button")
.map((b) => b.textContent?.replace(/[0-9]+$/, ""));
expect(names).toEqual(["Earth", "Moon"]);
expect(within(group).getByRole("button", { name: /Earth/ })).toContainElement(
within(group).getByLabelText("1 idle ship"),
);
expect(within(group).getByRole("button", { name: /Moon/ })).toContainElement(
within(group).getByLabelText("2 idle ships"),
);
});
it("opens on the most recently arrived idle ship's location, and switches on tap", async () => {
renderApp(boardApi(), "/contracts");
expect(await screen.findByText("1,234 cr")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Moon/, pressed: true })).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: /Earth/ }));
expect(await screen.findByText("1,999 cr")).toBeInTheDocument();
expect(screen.queryByText("1,234 cr")).not.toBeInTheDocument();
});
it("defaults to Earth when no ship is idle", async () => {
const api = fakeApi({
getPlayerState: () => samplePlayerState({ tutorial: null }),
getContractBoard: ({ locationId }) => ({
contracts: boards[locationId] ?? [],
periodEndsAt: NOW + 30 * 60_000,
}),
});
renderApp(api, "/contracts");
expect(await screen.findByText("1,999 cr")).toBeInTheDocument();
expect(screen.getByText(/No idle ships here/)).toBeInTheDocument();
});
it("opens the location named in the address", async () => {
renderApp(boardApi(), "/contracts?location=earth");
expect(await screen.findByText("1,999 cr")).toBeInTheDocument();
});
it("'Find work here' on the Fleet opens that ship's location board", async () => {
renderApp(boardApi(), "/fleet");
const row = (await screen.findByText("s1")).closest("li")!;
await userEvent.click(within(row).getByRole("link", { name: "Find work here" }));
expect(await screen.findByText("1,999 cr")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Earth/, pressed: true })).toBeInTheDocument();
});
it("shows the good, class and tier, shipper, mass, requirements, expiry and first contact", async () => {
renderApp(boardApi(), "/contracts?location=earth");
await screen.findByText("1,999 cr");
const items = screen.getAllByRole("listitem");
const [plain, special] = items;
// CR-007 card: good headline, class chip, shipper · mass · distance.
expect(within(plain!).getByRole("heading", { name: "Consumer Goods" })).toBeInTheDocument();
expect(plain).toHaveTextContent("Standard");
expect(plain).toHaveTextContent("Pacific Orbital Logistics · 50 t");
expect(plain).toHaveTextContent("No skills needed");
expect(plain).toHaveTextContent("expires in 30m 00s");
expect(within(plain!).queryByText("First contact")).not.toBeInTheDocument();
expect(special).toHaveTextContent("Earth → Mars");
expect(within(special!).getByRole("heading", { name: "Machine Parts" })).toBeInTheDocument();
expect(special).toHaveTextContent("Heavy · T2");
expect(special).toHaveTextContent("Needs Heavy Cargo 2, Long Haul 1");
expect(special).toHaveTextContent(/M km/);
expect(within(special!).getByText("First contact")).toBeInTheDocument();
});
});