deadhead-space-freight-tycoon/apps/web/test/independent-controls.test.tsx

201 lines
7.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 });
});
});