+
Your ship
-
{name}
+
{name}
{dispatch ? (
In flight
@@ -331,7 +297,7 @@ function YourShip({ className = "" }: { className?: string }) {
? `${contract.massTonnes} t ${goodName(contract)} for ${contract.shipper}`
: "Flying empty"}
-
+
Arrives in
@@ -428,23 +394,6 @@ function PilotDuty({ pilot }: { pilot: Pilot }) {
);
}
-/** Where in-flight alerts and news will appear; empty for now (decision 0.5.120). */
-function Alerts() {
- return (
-
-
-
- Alerts
-
- No alerts.
- In-flight alerts and news will appear here.
-
- );
-}
-
/** One pip per company run toward the deal. */
function RunPips({ done, total }: { done: number; total: number }) {
return (
@@ -452,7 +401,7 @@ function RunPips({ done, total }: { done: number; total: number }) {
{Array.from({ length: total }, (_, i) => (
))}
diff --git a/apps/web/test/dashboard.test.tsx b/apps/web/test/dashboard.test.tsx
index 6e52aa5..5798fef 100644
--- a/apps/web/test/dashboard.test.tsx
+++ b/apps/web/test/dashboard.test.tsx
@@ -56,11 +56,9 @@ describe("Dashboard (CR-020)", () => {
renderApp(fakeApi({ getPlayerState: () => state }));
await screen.findByRole("heading", { level: 1, name: "Bridge" });
- const statValue = (label: string) => screen.getByText(label).nextElementSibling;
- expect(statValue("Ships idle")).toHaveTextContent("1");
- expect(statValue("Ships in flight")).toHaveTextContent("2");
+ expect(screen.getByLabelText("Ships in flight")).toHaveTextContent("2 flying · 1 idle");
- const jobs = screen.getByRole("region", { name: "Ships on jobs" });
+ const jobs = screen.getByRole("region", { name: "In flight" });
const rows = within(jobs).getAllByRole("listitem");
expect(rows.map((r) => r.getAttribute("aria-label"))).toEqual(["c", "b"]);
expect(rows[0]).toHaveTextContent("Earth → Moon1m 05s");
@@ -297,9 +295,9 @@ describe("your ship on the Dashboard (CR-020)", () => {
expect(within(p).getByLabelText("Your ship's status")).toHaveTextContent("At Earth");
});
- it("has an Alerts panel, empty for now", async () => {
- renderApp(fakeApi({ getPlayerState: () => owner({ ships: [mine()] }) }));
- const alerts = await screen.findByRole("region", { name: "Alerts" });
- expect(alerts).toHaveTextContent("No alerts.");
+ it("says so when nothing needs the player", async () => {
+ renderApp(fakeApi({ getPlayerState: () => owner({ ships: [] }) }));
+ const list = await screen.findByRole("region", { name: "Needs you" });
+ expect(list).toHaveTextContent("Nothing right now.");
});
});
diff --git a/apps/web/test/layout.test.tsx b/apps/web/test/layout.test.tsx
index 686a108..77d426f 100644
--- a/apps/web/test/layout.test.tsx
+++ b/apps/web/test/layout.test.tsx
@@ -43,7 +43,9 @@ describe("app layout", () => {
renderApp(signedIn());
const nav = await screen.findByRole("navigation", { name: "Main" });
const links = within(nav).getAllByRole("link");
- expect(links.map((l) => l.textContent)).toEqual(["Bridge", "Jobs", "Fleet", "More"]);
+ ["Bridge", "Jobs", "Fleet", "More"].forEach((name, i) =>
+ expect(links[i]).toHaveAccessibleName(name),
+ );
expect(links[0]).toHaveAttribute("aria-current", "page");
expect(screen.getByRole("heading", { level: 1, name: "Bridge" })).toBeInTheDocument();
});
diff --git a/apps/web/test/logbook-screen.test.tsx b/apps/web/test/logbook-screen.test.tsx
index 4f4b758..013ad2d 100644
--- a/apps/web/test/logbook-screen.test.tsx
+++ b/apps/web/test/logbook-screen.test.tsx
@@ -260,8 +260,7 @@ describe("the Logbook screen (Step 25.4, decision 0.5.141)", () => {
it("is reachable from the Bridge", async () => {
renderApp(app(), "/");
- const card = await screen.findByLabelText("Logbook");
- await userEvent.click(within(card).getByRole("link", { name: "Open the Logbook" }));
+ await userEvent.click(await screen.findByRole("link", { name: "Open the Logbook" }));
expect(await screen.findByRole("heading", { level: 1, name: "Logbook" })).toBeInTheDocument();
});
});
diff --git a/apps/web/test/needs-you.test.tsx b/apps/web/test/needs-you.test.tsx
new file mode 100644
index 0000000..41ff508
--- /dev/null
+++ b/apps/web/test/needs-you.test.tsx
@@ -0,0 +1,71 @@
+import type { PlayerState, RadioCall, 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 { needsYou } from "../src/components/NeedsYou";
+import { tabAttention } from "../src/layout/attention";
+import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render";
+
+const base = samplePlayerState();
+const avatar = base.pilots[0]!;
+
+const ship: Ship = {
+ id: "s1",
+ modelId: "courier",
+ name: "Restless Sparrow",
+ status: "idle",
+ locationId: "earth",
+ dispatchId: null,
+ lastArrivalAt: 0,
+ ...shipExtras("courier"),
+ pilotId: "avatar",
+};
+
+const owner = (overrides: Partial
= {}): PlayerState =>
+ samplePlayerState({
+ tutorial: null,
+ ships: [ship],
+ bays: [{ id: "b", locationId: "earth" }],
+ pilots: [{ ...avatar, assignedShipId: "s1", skillPointsAvailable: 2 }],
+ goals: [{ id: "buy-a-ship", status: "ready" }],
+ ...overrides,
+ } as Partial);
+
+const call = { dispatchId: "d1", eventId: "debris-field", pilotId: avatar.id } as RadioCall;
+
+describe("Needs you (decision 0.5.168)", () => {
+ it("orders radio calls, ready ships, skill points, then goals", () => {
+ const items = needsYou(owner({ radioCalls: [{ ...call, windowEndsAt: 1 }] }));
+ expect(items.map((i) => i.kind)).toEqual(["radio", "ready", "skills", "goal"]);
+ });
+
+ it("badges count what the list holds, tab by tab", () => {
+ const state = owner({ radioCalls: [{ ...call, windowEndsAt: 1 }] });
+ const items = needsYou(state);
+ const badges = tabAttention(state);
+ expect(badges).toEqual({ "/": 2, "/contracts": 1, "/fleet": 2 });
+ const total = items.reduce((sum, i) => sum + i.count, 0);
+ expect(Object.values(badges).reduce((sum, n) => sum + n, 0)).toBe(total);
+ });
+
+ it("names who has skill points and opens Crew", async () => {
+ renderApp(fakeApi({ getPlayerState: () => owner() }));
+ const list = await screen.findByRole("region", { name: "Needs you" });
+ await userEvent.click(within(list).getByRole("link", { name: /2 skill points to spend/ }));
+ expect(await screen.findByRole("heading", { level: 1, name: "Crew" })).toBeInTheDocument();
+ });
+
+ it("sends a ready ship to the jobs where it is", async () => {
+ renderApp(
+ fakeApi({
+ getPlayerState: () => owner(),
+ getContractBoard: () => ({ contracts: [], periodEndsAt: Date.now() + 60_000 }) as never,
+ }),
+ );
+ const list = await screen.findByRole("region", { name: "Needs you" });
+ expect(within(list).getByRole("link", { name: /Restless Sparrow is ready/ })).toHaveAttribute(
+ "href",
+ "/contracts?location=earth",
+ );
+ });
+});
diff --git a/apps/web/test/radio-calls.test.tsx b/apps/web/test/radio-calls.test.tsx
index 2ea32f4..b51ac20 100644
--- a/apps/web/test/radio-calls.test.tsx
+++ b/apps/web/test/radio-calls.test.tsx
@@ -132,7 +132,7 @@ describe("radio calls (Step 26.6, decision 0.5.144)", () => {
await userEvent.click(within(sheet).getByRole("button", { name: "Decide later" }));
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
const panel = screen.getByLabelText("Radio calls");
- expect(panel).toHaveTextContent("Radio calls (1)");
+ expect(panel).toHaveTextContent("Debris field ahead");
await userEvent.click(within(panel).getByRole("button", { name: "Answer" }));
expect(await screen.findByRole("dialog", { name: "Debris field ahead" })).toBeInTheDocument();
});