201 lines
7.4 KiB
TypeScript
201 lines
7.4 KiB
TypeScript
import { content } from "@deadhead/content";
|
||
import type { Dispatch, Pilot, PlayerState, Ship } from "@deadhead/schema";
|
||
import { screen, 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 { formatLocalTime } from "../src/screens/DispatchSheet";
|
||
import { pilotExtras } from "./fixtures";
|
||
import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render";
|
||
|
||
const NOW = Date.now();
|
||
|
||
const ship = (id: string, pilotId: string | null): Ship => ({
|
||
id,
|
||
modelId: "courier",
|
||
name: `Ship ${id}`,
|
||
status: "idle",
|
||
locationId: "earth",
|
||
dispatchId: null,
|
||
lastArrivalAt: 0,
|
||
...shipExtras("courier"),
|
||
pilotId,
|
||
});
|
||
|
||
const hired = (over: Partial<Pilot> = {}): Pilot => ({
|
||
id: "p1",
|
||
name: "Ada Vance",
|
||
isAvatar: false,
|
||
isVeteran: false,
|
||
xp: 0,
|
||
level: 0,
|
||
skills: {},
|
||
skillPointsAvailable: 0,
|
||
assignedShipId: "s1",
|
||
status: "idle",
|
||
locationId: "earth",
|
||
dutyHours: 0,
|
||
dutyLimit: 12,
|
||
resting: false,
|
||
restEndsAt: null,
|
||
...pilotExtras,
|
||
...over,
|
||
});
|
||
|
||
function owner(over: Partial<PlayerState> = {}): PlayerState {
|
||
const base = samplePlayerState({ tutorial: null });
|
||
return {
|
||
...base,
|
||
company: { ...base.company, credits: 100_000 },
|
||
bays: [{ id: "b1", locationId: "earth" }],
|
||
ships: [ship("s0", "avatar"), ship("s1", "p1")],
|
||
pilots: [{ ...base.pilots[0]!, assignedShipId: "s0", locationId: "earth" }, hired()],
|
||
...over,
|
||
};
|
||
}
|
||
|
||
const ownedOffice = { dispatchOffice: { owned: true, spendingFloor: 500 } };
|
||
|
||
describe("Dispatch Office and pilot controls (Step 21.6, decisions 0.5.122–0.5.124)", () => {
|
||
it("disables Independent with a reason before the Dispatch Office, and hides it for you", async () => {
|
||
renderApp(fakeApi({ getPlayerState: () => owner() }), "/pilots");
|
||
const card = (await screen.findByText("Ada Vance")).closest("li")!;
|
||
expect(within(card).getByRole("radio", { name: "Independent" })).toBeDisabled();
|
||
expect(card).toHaveTextContent("Buy a Dispatch Office on the Bridge");
|
||
const yours = screen.getByText("Captain").closest("li")!;
|
||
expect(within(yours).queryByRole("radio", { name: "Independent" })).not.toBeInTheDocument();
|
||
});
|
||
|
||
it("sets a pilot Independent once the office is bought", async () => {
|
||
const api = fakeApi({
|
||
getPlayerState: () => owner(ownedOffice),
|
||
setPilotMode: ({ pilotId, mode }) => ({ pilot: hired({ id: pilotId, mode }) }),
|
||
});
|
||
renderApp(api, "/pilots");
|
||
const card = (await screen.findByText("Ada Vance")).closest("li")!;
|
||
await userEvent.click(within(card).getByRole("radio", { name: "Independent" }));
|
||
expect(api.call).toHaveBeenCalledWith("setPilotMode", { pilotId: "p1", mode: "independent" });
|
||
});
|
||
|
||
it("saves edited orders through the engine", async () => {
|
||
const api = fakeApi({
|
||
getPlayerState: () => owner(ownedOffice),
|
||
setPilotOrders: ({ pilotId, orders }) => ({ pilot: hired({ id: pilotId, orders }) }),
|
||
});
|
||
renderApp(api, "/pilots");
|
||
const card = (await screen.findByText("Ada Vance")).closest("li")!;
|
||
await userEvent.click(within(card).getByText("Orders"));
|
||
const form = within(card).getByRole("form", { name: "Orders for Ada Vance" });
|
||
await userEvent.click(within(form).getByRole("checkbox", { name: "Take Priority jobs" }));
|
||
await userEvent.click(within(form).getByRole("checkbox", { name: "Anywhere you know" }));
|
||
await userEvent.click(within(form).getByRole("checkbox", { name: "Earth" }));
|
||
await userEvent.clear(within(form).getByRole("spinbutton"));
|
||
await userEvent.type(within(form).getByRole("spinbutton"), "300");
|
||
await userEvent.click(within(form).getByRole("button", { name: "Save orders" }));
|
||
expect(api.call).toHaveBeenCalledWith("setPilotOrders", {
|
||
pilotId: "p1",
|
||
orders: {
|
||
...pilotExtras.orders,
|
||
destinationIds: ["moon"],
|
||
takePriority: false,
|
||
minProfit: 300,
|
||
},
|
||
});
|
||
});
|
||
|
||
it("describes what an Independent pilot is doing", () => {
|
||
const flight: Dispatch = {
|
||
id: "d1",
|
||
shipId: "s1",
|
||
pilotId: "p1",
|
||
contract: {
|
||
id: "c1",
|
||
originId: "earth",
|
||
destinationId: "moon",
|
||
cargoTypeId: "high-value",
|
||
goodId: "medical-supplies",
|
||
shipper: "Meridian Health",
|
||
priority: false,
|
||
deliveryWindowGameHours: null,
|
||
tier: 1,
|
||
massTonnes: 6,
|
||
payout: 3_000,
|
||
requiredSkill: { skillId: "expensive-cargo", level: 1 },
|
||
requiredLongHaulLevel: 0,
|
||
offeredAt: NOW,
|
||
expiresAt: NOW + 600_000,
|
||
firstContact: false,
|
||
},
|
||
originId: "earth",
|
||
destinationId: "moon",
|
||
tutorial: false,
|
||
departureAt: NOW,
|
||
arrivalAt: NOW + 120_000,
|
||
distanceMillionKm: 0.38,
|
||
fuelCost: 50,
|
||
fuelBurnedFu: 5,
|
||
expectedPayout: 3_000,
|
||
seed: 1,
|
||
status: "in-flight",
|
||
resolvedAt: null,
|
||
};
|
||
const independent = hired({ mode: "independent" });
|
||
expect(
|
||
independentStatus(owner({ activeDispatches: [flight] }), {
|
||
...independent,
|
||
status: "in-flight",
|
||
locationId: null,
|
||
}),
|
||
).toBe("Independent: flying Medical Supplies to Moon");
|
||
expect(independentStatus(owner(), { ...independent, locationId: "mars" })).toBe(
|
||
"Waiting for new contracts at Mars",
|
||
);
|
||
const wakes = NOW + 30 * 60_000;
|
||
expect(independentStatus(owner(), { ...independent, resting: true, restEndsAt: wakes })).toBe(
|
||
`Resting until ${formatLocalTime(wakes)}`,
|
||
);
|
||
expect(independentStatus(owner(), hired())).toBeNull();
|
||
});
|
||
|
||
it("shows the Independent badge only on ships crewed by Independent pilots", async () => {
|
||
const state = owner({
|
||
pilots: [{ ...owner().pilots[0]! }, hired({ mode: "independent" })],
|
||
});
|
||
renderApp(fakeApi({ getPlayerState: () => state }), "/fleet");
|
||
const theirs = (await screen.findByText("Ship s1")).closest("li")!;
|
||
const yours = screen.getByText("Ship s0").closest("li")!;
|
||
expect(within(theirs).getByText("Independent")).toBeInTheDocument();
|
||
expect(within(yours).queryByText("Independent")).not.toBeInTheDocument();
|
||
});
|
||
|
||
it("sells the Dispatch Office on the Bridge and sets the spending floor in Settings", async () => {
|
||
const api = fakeApi({
|
||
getPlayerState: () => owner(),
|
||
buyDispatchOffice: () => ({
|
||
company: owner().company,
|
||
dispatchOffice: { owned: true, spendingFloor: 500 },
|
||
}),
|
||
setSpendingFloor: ({ credits }) => ({
|
||
company: owner().company,
|
||
dispatchOffice: { owned: false, spendingFloor: credits },
|
||
}),
|
||
});
|
||
const { unmount } = renderApp(api, "/");
|
||
const office = await screen.findByLabelText("Dispatch Office");
|
||
expect(office).toHaveTextContent("Lets your hired pilots run jobs on their own");
|
||
const price = content.config.independentPilots.dispatchOfficePrice.toLocaleString("en-US");
|
||
await userEvent.click(
|
||
within(office).getByRole("button", { name: `Buy Dispatch Office (${price} cr)` }),
|
||
);
|
||
expect(api.call).toHaveBeenCalledWith("buyDispatchOffice", {});
|
||
unmount();
|
||
|
||
renderApp(api, "/settings");
|
||
const floor = await screen.findByRole("spinbutton");
|
||
await userEvent.clear(floor);
|
||
await userEvent.type(floor, "2000");
|
||
await userEvent.click(screen.getByRole("button", { name: "Save floor" }));
|
||
expect(api.call).toHaveBeenCalledWith("setSpendingFloor", { credits: 2000 });
|
||
});
|
||
});
|