deadhead-space-freight-tycoon/apps/web/test/dispatch-duty.test.tsx

138 lines
4.1 KiB
TypeScript

import { content } from "@deadhead/content";
import type { Contract, Pilot, Ship } from "@deadhead/schema";
import { realToGame, routeDistance, travelTimeGameHours } from "@deadhead/sim";
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 { formatGameHours } from "../src/format";
import { fakeApi, renderApp, samplePlayerState } from "./render";
const NOW = Date.parse("2100-01-01T00:00:00Z");
const ship = (id: string, name: string, pilotId: string): Ship => ({
id,
name,
modelId: "courier",
status: "idle",
locationId: "earth",
dispatchId: null,
lastArrivalAt: 0,
pilotId,
});
const pilot = (id: string, name: string, shipId: string, extra: Partial<Pilot>): Pilot => ({
id,
name,
isAvatar: false,
xp: 0,
level: 0,
skills: {},
skillPointsAvailable: 0,
assignedShipId: shipId,
status: "idle",
locationId: "earth",
dutyHours: 0,
dutyLimit: 12,
resting: false,
restEndsAt: null,
...extra,
});
const moonJob: Contract = {
id: "c-moon",
originId: "earth",
destinationId: "moon",
cargoTypeId: "standard",
goodId: "consumer-goods",
shipper: "Pacific Orbital Logistics",
priority: false,
deliveryWindowGameHours: null,
tier: 0,
massTonnes: 10,
payout: 900,
requiredSkill: null,
requiredLongHaulLevel: 0,
offeredAt: NOW,
expiresAt: NOW + 3_600_000,
firstContact: false,
};
const state = samplePlayerState({
tutorial: null,
company: { id: "c", name: "D", credits: 100_000, loanBalance: 0 },
bays: [{ id: "b1", locationId: "earth" }],
ships: [
ship("s-rest", "Sleepy Gull", "p-rest"),
ship("s-tired", "Tired Heron", "p-tired"),
ship("s-fresh", "Fresh Lark", "p-fresh"),
],
pilots: [
pilot("p-rest", "Ana Resting", "s-rest", {
resting: true,
restEndsAt: NOW + 12 * 60_000 + 5_000,
}),
pilot("p-tired", "Bo Tired", "s-tired", { dutyHours: 11 }),
pilot("p-fresh", "Cy Fresh", "s-fresh", { dutyHours: 1 }),
],
});
/** The Courier's game hours for the Moon job, straight from the sim. */
const tripHours = () =>
travelTimeGameHours(
content.shipModel("courier")!,
routeDistance(
content.location("earth")!,
content.location("moon")!,
realToGame(NOW, content.config),
),
moonJob.massTonnes,
content.config,
);
async function openSheet() {
renderApp(
fakeApi({
getPlayerState: () => state,
getContractBoard: () => ({ contracts: [moonJob], periodEndsAt: NOW + 3_600_000 }),
}),
"/contracts?location=earth",
);
await userEvent.click(await screen.findByRole("button", { name: "Dispatch" }));
return screen.getByRole("dialog");
}
const option = (dialog: HTMLElement, shipName: string) =>
within(dialog).getByText(shipName).closest("li")!;
describe("duty in the dispatch sheet (Step 10D.7)", () => {
beforeEach(() => {
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
vi.setSystemTime(NOW);
useClock.setState({ offsetMs: 0, synced: true, now: NOW });
});
afterEach(() => vi.useRealTimers());
it("disables a resting pilot's pair with the time left", async () => {
const resting = option(await openSheet(), "Sleepy Gull");
expect(within(resting).getByRole("radio")).toBeDisabled();
expect(resting).toHaveTextContent("Resting — 12:05");
});
it("warns when the trip takes the pilot to the limit, with the sim's duty numbers", async () => {
const dialog = await openSheet();
const tired = option(dialog, "Tired Heron");
expect(tired).toHaveTextContent(
`Duty after${formatGameHours(11 + tripHours())} of ${formatGameHours(12)}`,
);
expect(tired).toHaveTextContent("Will need rest on arrival");
});
it("shows no warning when the pilot stays under the limit", async () => {
const fresh = option(await openSheet(), "Fresh Lark");
expect(fresh).toHaveTextContent(
`Duty after${formatGameHours(1 + tripHours())} of ${formatGameHours(12)}`,
);
expect(fresh).not.toHaveTextContent("Will need rest");
});
});