222 lines
8.4 KiB
TypeScript
222 lines
8.4 KiB
TypeScript
import { content } from "@deadhead/content";
|
|
import type { Contract, Pilot, PlayerState, Ship } from "@deadhead/schema";
|
|
import { repairCost } from "@deadhead/sim";
|
|
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { describe, expect, it } from "vitest";
|
|
import { independentStatus } from "../src/components/IndependentPilots";
|
|
import { serviceStatus, serviceTag } from "../src/components/Service";
|
|
import { formatCredits } from "../src/format";
|
|
import { pairOptions } from "../src/screens/DispatchSheet";
|
|
import { pilotExtras } from "./fixtures";
|
|
import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render";
|
|
|
|
type Booking = NonNullable<Ship["booking"]>;
|
|
|
|
const ship = (over: Partial<Ship> = {}): Ship => ({
|
|
id: "s1",
|
|
name: "Dented Wren",
|
|
modelId: "courier",
|
|
status: "idle",
|
|
locationId: "moon",
|
|
dispatchId: null,
|
|
pilotId: "p1",
|
|
lastArrivalAt: 0,
|
|
...shipExtras("courier"),
|
|
hullConditionPct: 60,
|
|
...over,
|
|
});
|
|
const booking = (over: Partial<Booking> = {}): Booking => ({
|
|
repair: true,
|
|
upgradeId: null,
|
|
upgradeLevel: null,
|
|
hqId: "earth",
|
|
by: "player",
|
|
estimate: 4_000,
|
|
status: "heading",
|
|
...over,
|
|
});
|
|
const rosa: Pilot = {
|
|
...samplePlayerState().pilots[0]!,
|
|
id: "p1",
|
|
name: "Rosa",
|
|
isAvatar: false,
|
|
assignedShipId: "s1",
|
|
locationId: null,
|
|
...pilotExtras,
|
|
mode: "independent",
|
|
};
|
|
const state = (s: Ship, over: Partial<PlayerState> = {}) =>
|
|
samplePlayerState({
|
|
tutorial: null,
|
|
company: { id: "c", name: "D", credits: 1_000_000, loanBalance: 0 },
|
|
bays: [{ id: "b1", locationId: "earth" }],
|
|
ships: [s],
|
|
pilots: [samplePlayerState().pilots[0]!, rosa],
|
|
...over,
|
|
});
|
|
|
|
async function openService(api: ReturnType<typeof fakeApi>) {
|
|
renderApp(api, "/fleet");
|
|
const card = (await screen.findByText("Dented Wren")).closest("li")!;
|
|
await userEvent.click(within(card).getByRole("button", { name: "Upgrades for Dented Wren" }));
|
|
return within(screen.getByRole("dialog", { name: "Dented Wren" })).getByLabelText("Service");
|
|
}
|
|
|
|
describe("the Service section (Step 27.5, decision 0.5.153)", () => {
|
|
it("books service with the estimate and the nearest reachable HQ by default", async () => {
|
|
const api = fakeApi({
|
|
getPlayerState: () => state(ship()),
|
|
bookService: () => ({ ship: ship({ booking: booking() }) }),
|
|
});
|
|
const section = await openService(api);
|
|
await userEvent.click(within(section).getByRole("button", { name: "Book service" }));
|
|
const form = within(section).getByRole("group", { name: "Book service" });
|
|
expect(within(form).getByLabelText("HQ")).toHaveValue("earth");
|
|
expect(within(form).getByRole("option", { name: "Earth (nearest)" })).toBeInTheDocument();
|
|
const cost = repairCost(content.shipModel("courier")!, 60, content.config);
|
|
expect(within(form).getByLabelText("Estimate")).toHaveTextContent(formatCredits(cost));
|
|
await userEvent.selectOptions(within(form).getByLabelText("Upgrade"), "cargo-bay");
|
|
await userEvent.click(within(form).getByRole("button", { name: "Book service" }));
|
|
expect(api.call).toHaveBeenCalledWith("bookService", {
|
|
shipId: "s1",
|
|
repair: true,
|
|
upgradeId: "cargo-bay",
|
|
hqId: "earth",
|
|
});
|
|
});
|
|
|
|
it("shows a booking's items, estimate and status, and changes or cancels it", async () => {
|
|
const booked = ship({
|
|
booking: booking({ upgradeId: "cargo-bay", upgradeLevel: 1, estimate: 12_345 }),
|
|
});
|
|
const api = fakeApi({
|
|
getPlayerState: () => state(booked),
|
|
cancelBooking: () => ({ ship: ship() }),
|
|
bookService: () => ({ ship: booked }),
|
|
});
|
|
const section = await openService(api);
|
|
expect(section).toHaveTextContent(
|
|
`Repair + Cargo Bay Expansion 1 at Earth · ${formatCredits(12_345)}`,
|
|
);
|
|
expect(within(section).getByLabelText("Service status")).toHaveTextContent(
|
|
"Heading to Earth for service",
|
|
);
|
|
await userEvent.click(within(section).getByRole("button", { name: "Cancel booking" }));
|
|
expect(api.call).toHaveBeenCalledWith("cancelBooking", { shipId: "s1" });
|
|
|
|
await userEvent.click(within(section).getByRole("button", { name: "Change" }));
|
|
const form = within(section).getByRole("group", { name: "Book service" });
|
|
expect(within(form).getByLabelText("Upgrade")).toHaveValue("cargo-bay");
|
|
await userEvent.selectOptions(within(form).getByLabelText("Upgrade"), "");
|
|
await userEvent.click(within(form).getByRole("button", { name: "Save booking" }));
|
|
expect(api.call).toHaveBeenCalledWith("bookService", {
|
|
shipId: "s1",
|
|
repair: true,
|
|
upgradeId: null,
|
|
hqId: "earth",
|
|
});
|
|
});
|
|
|
|
it("can't change work that has started", async () => {
|
|
const api = fakeApi({
|
|
getPlayerState: () => state(ship({ booking: booking({ status: "in-shop" }) })),
|
|
});
|
|
const section = await openService(api);
|
|
expect(within(section).queryByRole("button", { name: "Change" })).not.toBeInTheDocument();
|
|
expect(
|
|
within(section).queryByRole("button", { name: "Cancel booking" }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("saves the auto-repair threshold in steps of 5, and turns it off", async () => {
|
|
const api = fakeApi({
|
|
getPlayerState: () => state(ship()),
|
|
setAutoRepair: () => ({ ship: ship() }),
|
|
});
|
|
const section = await openService(api);
|
|
const slider = within(section).getByLabelText("Auto-repair threshold");
|
|
expect(slider).toHaveAttribute("step", "5");
|
|
expect(slider).toHaveAttribute("min", "10");
|
|
expect(slider).toHaveAttribute("max", "90");
|
|
fireEvent.change(slider, { target: { value: "55" } });
|
|
await waitFor(() =>
|
|
expect(api.call).toHaveBeenCalledWith("setAutoRepair", {
|
|
shipId: "s1",
|
|
on: true,
|
|
thresholdPct: 55,
|
|
}),
|
|
);
|
|
await userEvent.click(within(section).getByRole("checkbox", { name: /Auto-repair below 40%/ }));
|
|
expect(api.call).toHaveBeenCalledWith("setAutoRepair", {
|
|
shipId: "s1",
|
|
on: false,
|
|
thresholdPct: 40,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("service tags and status lines (Step 27.5, CR-027 §4)", () => {
|
|
it("tags the Fleet card with the booking, or Repair recommended", async () => {
|
|
renderApp(
|
|
fakeApi({
|
|
getPlayerState: () =>
|
|
state(
|
|
ship({
|
|
booking: booking({ upgradeId: "engines", upgradeLevel: 2, repair: false }),
|
|
}),
|
|
),
|
|
}),
|
|
"/fleet",
|
|
);
|
|
expect(await screen.findByLabelText("Dented Wren service")).toHaveTextContent(
|
|
"Service booked: Engine Tuning 2 at Earth",
|
|
);
|
|
});
|
|
|
|
it("words each state", () => {
|
|
const at = (status: Booking["status"]) => ship({ booking: booking({ status }) });
|
|
expect(serviceStatus(at("heading"), rosa)).toBe("Heading to Earth for service");
|
|
expect(serviceStatus(at("in-shop"), { ...rosa, restingInShopUntil: 1 })).toBe(
|
|
"In the shop at Earth (resting)",
|
|
);
|
|
expect(serviceStatus(at("in-shop"), rosa)).toBe("In the shop at Earth");
|
|
expect(serviceStatus(at("waiting-funds"), rosa)).toBe("Waiting for funds: Repair at Earth");
|
|
expect(serviceStatus(at("cant-reach"), rosa)).toBe("Can't reach Earth for service");
|
|
expect(serviceStatus(ship(), rosa)).toBeNull();
|
|
expect(serviceTag(ship({ repairRecommended: true }))).toBe("Repair recommended");
|
|
expect(serviceTag(ship())).toBeNull();
|
|
});
|
|
|
|
it("puts the service first in an Independent pilot's status line", () => {
|
|
const heading = ship({ booking: booking() });
|
|
expect(independentStatus(state(heading), rosa)).toBe("Heading to Earth for service");
|
|
// Out of reach: the pilot works as usual, and the line says so.
|
|
const far = ship({ booking: booking({ status: "cant-reach" }) });
|
|
expect(independentStatus(state(far), { ...rosa, locationId: "moon" })).not.toMatch(/service/);
|
|
});
|
|
|
|
it("tags the ship in the dispatch sheet", () => {
|
|
const job: Contract = {
|
|
id: "c",
|
|
originId: "moon",
|
|
destinationId: "earth",
|
|
cargoTypeId: "standard",
|
|
goodId: "consumer-goods",
|
|
shipper: "Pacific Orbital Logistics",
|
|
priority: false,
|
|
deliveryWindowGameHours: null,
|
|
tier: 0,
|
|
massTonnes: 5,
|
|
payout: 1_000,
|
|
requiredSkill: null,
|
|
requiredLongHaulLevel: 0,
|
|
offeredAt: 0,
|
|
expiresAt: 600_000,
|
|
firstContact: false,
|
|
};
|
|
const [pair] = pairOptions(state(ship({ booking: booking({ status: "booked" }) })), job, 0);
|
|
expect(pair!.service).toBe("Service booked: Repair at Earth");
|
|
});
|
|
});
|