deadhead-space-freight-tycoon/apps/web/test/attention.test.tsx

115 lines
4.2 KiB
TypeScript

import type { 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 { tabAttention } from "../src/layout/attention";
import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render";
const ship = (overrides: Partial<Ship>): Ship => ({
id: "s",
modelId: "courier",
name: "Ship",
status: "idle",
locationId: "earth",
dispatchId: null,
lastArrivalAt: 0,
...shipExtras("courier"),
pilotId: null,
...overrides,
});
const base = samplePlayerState();
const avatar = base.pilots[0]!;
const owner = (overrides: Partial<PlayerState>) =>
samplePlayerState({ tutorial: null, bays: [{ id: "b", locationId: "earth" }], ...overrides });
describe("navigation attention badges (Step 10E.3)", () => {
it("counts ready goals, ships ready for a job and unspent skill points", () => {
const state = owner({
goals: [
{ id: "a", status: "ready" },
{ id: "b", status: "incomplete" },
{ id: "c", status: "ready" },
],
ships: [
ship({ id: "s1", pilotId: "avatar" }), // ready
ship({ id: "s2", pilotId: "p2" }), // pilot resting
ship({ id: "s3", pilotId: null }), // uncrewed
ship({ id: "s4", pilotId: "p3", status: "in-flight", locationId: null }),
],
pilots: [
{ ...avatar, assignedShipId: "s1", skillPointsAvailable: 2 },
{
...avatar,
id: "p2",
isAvatar: false,
isVeteran: false,
assignedShipId: "s2",
resting: true,
restEndsAt: 1,
},
{ ...avatar, id: "p3", isAvatar: false, assignedShipId: "s4", skillPointsAvailable: 1 },
],
} as Partial<PlayerState>);
// Skill points: 2 + 0 + 1 across the three pilots (decision 0.5.109).
expect(tabAttention(state)).toEqual({ "/": 2, "/contracts": 1, "/pilots": 3 });
});
it("nudges toward Jobs during the tutorial until a run is in flight", () => {
expect(tabAttention(base)["/contracts"]).toBe(1);
const flying = samplePlayerState({ tutorial: { ...base.tutorial!, runInFlight: true } });
expect(tabAttention(flying)["/contracts"]).toBe(0);
});
it("shows a badge without changing the tab's accessible name", async () => {
renderApp(fakeApi({ getPlayerState: () => base }));
const nav = await screen.findByRole("navigation", { name: "Main" });
const jobs = within(nav).getByRole("link", { name: "Jobs" });
expect(within(jobs).getByTestId("badge-Jobs")).toHaveTextContent("1");
expect(within(nav).queryByTestId("badge-Pilots")).not.toBeInTheDocument();
});
});
describe("Pilots tab badge (Step 17.5, decision 0.5.109)", () => {
it("shows the total unspent skill points and disappears once all are spent", async () => {
// You with 2 points, Rosa with 1; each raise spends one.
const points: Record<string, number> = { avatar: 2, rosa: 1 };
const state = () =>
owner({
pilots: [
{ ...avatar, skillPointsAvailable: points.avatar! },
{
...avatar,
id: "rosa",
name: "Rosa Vance",
isAvatar: false,
isVeteran: true,
skillPointsAvailable: points.rosa!,
},
],
});
const api = fakeApi({
getPlayerState: state,
spendSkillPoint: ({ pilotId, skillId }) => {
points[pilotId]! -= 1;
const pilot = state().pilots.find((p) => p.id === pilotId)!;
return { pilot: { ...pilot, skills: { ...pilot.skills, [skillId]: 1 } } };
},
});
renderApp(api, "/pilots");
const nav = await screen.findByRole("navigation", { name: "Main" });
expect(await within(nav).findByTestId("badge-Pilots")).toHaveTextContent("3");
const raise = async (pilot: string) =>
userEvent.click(
(await screen.findAllByRole("button", { name: new RegExp(`for ${pilot}$`) }))[0]!,
);
await raise(avatar.name);
await waitFor(() => expect(within(nav).getByTestId("badge-Pilots")).toHaveTextContent("2"));
await raise(avatar.name);
await raise("Rosa Vance");
await waitFor(() => expect(within(nav).queryByTestId("badge-Pilots")).not.toBeInTheDocument());
});
});