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 => ({ 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) => 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); // Skill points: 2 + 0 + 1 across the three pilots (decision 0.5.109). expect(tabAttention(state)).toEqual({ "/": 2, "/contracts": 1, "/fleet": 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-Fleet")).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 = { 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-Fleet")).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-Fleet")).toHaveTextContent("2")); await raise(avatar.name); await raise("Rosa Vance"); await waitFor(() => expect(within(nav).queryByTestId("badge-Fleet")).not.toBeInTheDocument()); }); });