167 lines
6.1 KiB
TypeScript
167 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, shipExtras } 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,
|
|
...shipExtras("courier"),
|
|
...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();
|
|
});
|
|
});
|