deadhead-space-freight-tycoon/apps/web/test/board-refresh.test.tsx

112 lines
3.7 KiB
TypeScript

import { content } from "@deadhead/content";
import type { Contract, Pilot, Ship } from "@deadhead/schema";
import { boardPeriodIndex, boardPeriodMs } 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 { formatClock } from "../src/format";
import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render";
const period = boardPeriodMs(content.config);
// A quarter of the way into a board period (derived from config, whatever its length).
const NOW = Math.ceil(Date.parse("2100-01-01T00:00:00Z") / period) * period + period / 4;
const periodEndsAt = (boardPeriodIndex(NOW, content.config) + 1) * period;
const courier: Ship = {
id: "s1",
modelId: "courier",
name: "Dusty Mule",
status: "idle",
locationId: "earth",
dispatchId: null,
lastArrivalAt: 0,
...shipExtras("courier"),
pilotId: "p1",
};
const rae: Pilot = {
id: "p1",
name: "Rae Okafor",
isAvatar: false,
xp: 0,
level: 0,
skills: {},
skillPointsAvailable: 0,
assignedShipId: "s1",
status: "idle",
locationId: "earth",
dutyHours: 3,
dutyLimit: 12,
resting: false,
restEndsAt: null,
};
/** Too heavy for a Courier. */
const heavyJob: Contract = {
id: "c-heavy",
originId: "earth",
destinationId: "moon",
cargoTypeId: "heavy",
goodId: "machine-parts",
shipper: "Pacific Orbital Logistics",
priority: false,
deliveryWindowGameHours: null,
tier: 0,
massTonnes: 300,
payout: 9000,
requiredSkill: null,
requiredLongHaulLevel: 0,
offeredAt: NOW,
expiresAt: periodEndsAt,
firstContact: false,
};
const api = (contracts: Contract[]) =>
fakeApi({
getPlayerState: () =>
samplePlayerState({
tutorial: null,
company: { id: "c", name: "D", credits: 10_000, loanBalance: 0 },
bays: [{ id: "b1", locationId: "earth" }],
ships: [courier],
pilots: [rae],
}),
getContractBoard: () => ({ contracts, periodEndsAt }),
restPilot: () => ({ pilot: { ...rae, resting: true, restEndsAt: NOW + period } }),
});
describe("board refresh and the nothing-here case (Step 10D.8)", () => {
beforeEach(() => {
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
vi.setSystemTime(NOW);
useClock.setState({ offsetMs: 0, synced: true, now: NOW });
});
afterEach(() => vi.useRealTimers());
it("counts down to the next board period", async () => {
renderApp(api([heavyJob]), "/contracts?location=earth");
expect(await screen.findByLabelText("Board refresh")).toHaveTextContent(
`New contracts in ${formatClock(periodEndsAt - NOW)}`,
);
});
it("offers rest or a deadhead when no contract fits the idle ship, and each one acts", async () => {
const client = api([heavyJob]);
renderApp(client, "/contracts?location=earth");
const prompt = await screen.findByRole("group", { name: "Nothing for Dusty Mule" });
await userEvent.click(
within(prompt).getByRole("button", { name: "Rest Rae Okafor — wakes to a new board" }),
);
await waitFor(() => expect(client.call).toHaveBeenCalledWith("restPilot", { pilotId: "p1" }));
await userEvent.click(within(prompt).getByRole("button", { name: "Deadhead (fly empty)" }));
expect(screen.getByRole("dialog", { name: "Deadhead Dusty Mule" })).toBeInTheDocument();
});
it("does not prompt when the ship has a job it can take", async () => {
renderApp(api([{ ...heavyJob, id: "c-light", massTonnes: 10 }]), "/contracts?location=earth");
await screen.findByLabelText("Board refresh");
expect(screen.queryByRole("group", { name: /Nothing for/ })).not.toBeInTheDocument();
});
});