144 lines
5.0 KiB
TypeScript
144 lines
5.0 KiB
TypeScript
import { pilotExtras } from "./fixtures";
|
|
import { content } from "@deadhead/content";
|
|
import type { Pilot, PlayerState, Ship } from "@deadhead/schema";
|
|
import { screen, waitFor, within } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { describe, expect, it } from "vitest";
|
|
import { levelProgress } from "../src/screens/Pilots";
|
|
import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render";
|
|
|
|
const ship = (overrides: Partial<Ship>): Ship => ({
|
|
id: "s",
|
|
modelId: "courier",
|
|
name: "Ship",
|
|
status: "idle",
|
|
locationId: "moon",
|
|
dispatchId: null,
|
|
lastArrivalAt: 0,
|
|
...shipExtras("courier"),
|
|
pilotId: null,
|
|
...overrides,
|
|
});
|
|
|
|
const rae: Pilot = {
|
|
id: "p-rae",
|
|
name: "Rae Okafor",
|
|
isAvatar: false,
|
|
isVeteran: false,
|
|
xp: 260,
|
|
level: 2,
|
|
skills: { "heavy-cargo": 1 },
|
|
skillPointsAvailable: 3,
|
|
assignedShipId: null,
|
|
status: "idle",
|
|
locationId: "moon",
|
|
dutyHours: 0,
|
|
dutyLimit: 12,
|
|
resting: false,
|
|
restEndsAt: null,
|
|
...pilotExtras,
|
|
};
|
|
|
|
function pilotsState(overrides: Partial<PlayerState> = {}): PlayerState {
|
|
const base = samplePlayerState({ tutorial: null });
|
|
return {
|
|
...base,
|
|
company: { ...base.company, credits: 50_000 },
|
|
bays: [
|
|
{ id: "b1", locationId: "earth" },
|
|
{ id: "b2", locationId: "moon" },
|
|
],
|
|
pilots: [{ ...base.pilots[0]!, assignedShipId: "s-earth" }, rae],
|
|
ships: [
|
|
ship({ id: "s-earth", name: "Earth Crewed", locationId: "earth", pilotId: "avatar" }),
|
|
ship({ id: "s-moon-free", name: "Moon Free" }),
|
|
ship({ id: "s-moon-crewed", name: "Moon Crewed", pilotId: "someone" }),
|
|
ship({ id: "s-earth-free", name: "Earth Free", locationId: "earth" }),
|
|
ship({ id: "s-flying", name: "Flying", status: "in-flight", locationId: null }),
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("pilots screen", () => {
|
|
it("shows level, XP progress, skills and points", async () => {
|
|
renderApp(fakeApi({ getPlayerState: () => pilotsState() }), "/pilots");
|
|
const card = (await screen.findByText("Rae Okafor")).closest("li")!;
|
|
const { xp, needed } = levelProgress(260, 2)!;
|
|
expect(card).toHaveTextContent("Level 2");
|
|
expect(card).toHaveTextContent(`${xp} / ${needed} XP to next level`);
|
|
expect(card).toHaveTextContent("Heavy Cargo 1/5");
|
|
expect(within(card).getByLabelText("Rae Okafor skill points")).toHaveTextContent("3");
|
|
});
|
|
|
|
it("spends a skill point through the endpoint and shows the updated pilot", async () => {
|
|
let spent = false;
|
|
const api = fakeApi({
|
|
getPlayerState: () =>
|
|
pilotsState({
|
|
pilots: [
|
|
pilotsState().pilots[0]!,
|
|
spent ? { ...rae, skills: { "heavy-cargo": 2 }, skillPointsAvailable: 2 } : rae,
|
|
],
|
|
}),
|
|
spendSkillPoint: ({ pilotId, skillId }) => {
|
|
spent = true;
|
|
return {
|
|
pilot: { ...rae, id: pilotId, skills: { [skillId]: 2 }, skillPointsAvailable: 2 },
|
|
};
|
|
},
|
|
});
|
|
renderApp(api, "/pilots");
|
|
await userEvent.click(
|
|
await screen.findByRole("button", { name: "Raise Heavy Cargo for Rae Okafor" }),
|
|
);
|
|
|
|
expect(api.call).toHaveBeenCalledWith("spendSkillPoint", {
|
|
pilotId: "p-rae",
|
|
skillId: "heavy-cargo",
|
|
});
|
|
const card = screen.getByText("Rae Okafor").closest("li")!;
|
|
await waitFor(() => expect(card).toHaveTextContent("Heavy Cargo 2/5"));
|
|
expect(within(card).getByLabelText("Rae Okafor skill points")).toHaveTextContent("2");
|
|
});
|
|
|
|
it("disables hiring when the player cannot afford a pilot", async () => {
|
|
const cost = content.config.pilotHireCost;
|
|
renderApp(
|
|
fakeApi({
|
|
getPlayerState: () =>
|
|
pilotsState({ company: { id: "c", name: "D", credits: cost - 1, loanBalance: 0 } }),
|
|
}),
|
|
"/pilots",
|
|
);
|
|
const hire = await screen.findByRole("button", { name: /Hire pilot/ });
|
|
expect(hire).toBeDisabled();
|
|
expect(hire).toHaveTextContent(`${cost.toLocaleString("en-US")} cr`);
|
|
});
|
|
|
|
it("hires at the chosen bay", async () => {
|
|
const api = fakeApi({ getPlayerState: () => pilotsState() });
|
|
renderApp(api, "/pilots");
|
|
await userEvent.selectOptions(await screen.findByLabelText("Hire at"), "moon");
|
|
await userEvent.click(screen.getByRole("button", { name: /Hire pilot/ }));
|
|
expect(api.call).toHaveBeenCalledWith("hirePilot", { locationId: "moon" });
|
|
});
|
|
|
|
it("offers only idle, uncrewed ships at the pilot's location for assignment", async () => {
|
|
const api = fakeApi({ getPlayerState: () => pilotsState() });
|
|
renderApp(api, "/pilots");
|
|
const select = await screen.findByLabelText("Assign Rae Okafor to");
|
|
const options = within(select)
|
|
.getAllByRole("option")
|
|
.map((o) => o.textContent);
|
|
expect(options).toEqual(["Choose a ship…", "Moon Free (Courier)"]);
|
|
|
|
await userEvent.selectOptions(select, "s-moon-free");
|
|
await userEvent.click(screen.getByRole("button", { name: "Assign" }));
|
|
expect(api.call).toHaveBeenCalledWith("assignPilot", {
|
|
pilotId: "p-rae",
|
|
shipId: "s-moon-free",
|
|
});
|
|
});
|
|
});
|