Step 29.4: four tabs, More and Fleet's views

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-11-04 18:10:40 -05:00
parent 06bf2f17b1
commit 38b636014f
29 changed files with 185 additions and 69 deletions

View File

@ -130,7 +130,7 @@ export async function playToDeal(page: Page) {
await page.goto("/");
await dismissLogbook(page);
await page.getByRole("button", { name: "Shake on it" }).click();
await expect(page.getByRole("heading", { level: 1, name: "Dashboard" })).toBeVisible();
await expect(page.getByRole("heading", { level: 1, name: "Bridge" })).toBeVisible();
// Both pilots have skill points at the deal (CR-017); the pop-up says so.
const skills = page.getByRole("dialog", { name: "Your pilots are ready to train" });
await expect(skills.getByLabel("Unspent skill points")).toContainText(/You: \d+ points?/);

View File

@ -19,6 +19,7 @@ import { HQs } from "./screens/HQs";
import { Locations } from "./screens/Locations";
import { MapScreen } from "./screens/MapScreen";
import { Market } from "./screens/Market";
import { More } from "./screens/More";
import { Pilots } from "./screens/Pilots";
import { Logbook } from "./screens/Logbook";
import { Settings } from "./screens/Settings";
@ -30,13 +31,9 @@ import { Button } from "./ui";
const SCREEN_COMPONENTS: Record<(typeof SCREENS)[number]["path"], ReactNode> = {
"/": <Dashboard />,
"/map": <MapScreen />,
"/contracts": <Contracts />,
"/fleet": <Fleet />,
"/pilots": <Pilots />,
"/hqs": <HQs />,
"/market": <Market />,
"/locations": <Locations />,
"/more": <More />,
};
/** Reads the game clock (the engine's device clock, CR-014), then ticks it every second. */
@ -106,7 +103,13 @@ export function App() {
element={SCREEN_COMPONENTS[screen.path]}
/>
))}
{/* Settings opens from the header gear, not a tab (CR-011). */}
{/* Screens without a tab of their own (decision 0.5.167). */}
<Route path="/map" element={<MapScreen />} />
<Route path="/pilots" element={<Pilots />} />
<Route path="/hqs" element={<HQs />} />
<Route path="/market" element={<Market />} />
<Route path="/locations" element={<Locations />} />
{/* Settings opens from the header gear and More, not a tab (CR-011). */}
<Route path="/settings" element={<Settings />} />
{/* The Logbook opens from "While you were away" and the Bridge (CR-025). */}
<Route path="/logbook" element={<Logbook />} />

View File

@ -0,0 +1,31 @@
import { NavLink } from "react-router";
import { useLoadedPlayer } from "../state/actions";
/** Fleet's two views, Ships and Crew (decision 0.5.167). */
export function FleetViews() {
const state = useLoadedPlayer();
const views = [
{ to: "/fleet", label: `Ships ${state.ships.length}` },
{ to: "/pilots", label: `Crew ${state.pilots.length}` },
];
return (
<nav
aria-label="Fleet views"
className="flex gap-0.5 rounded-xl border border-glass-line bg-black/25 p-0.5"
>
{views.map((view) => (
<NavLink
key={view.to}
to={view.to}
className={({ isActive }) =>
`flex min-h-11 flex-1 items-center justify-center rounded-[0.6rem] text-sm font-medium ${
isActive ? "bg-white/14 text-ink-50" : "text-ink-300 hover:text-ink-50"
}`
}
>
{view.label}
</NavLink>
))}
</nav>
);
}

View File

@ -1,7 +1,7 @@
import type { CSSProperties, ReactNode } from "react";
import { Link, NavLink, Outlet, useLocation } from "react-router";
import { Link, Outlet, useLocation } from "react-router";
import { GameClock } from "../clock/GameClock";
import { SCREENS, type ScreenPath } from "../routes";
import { SCREENS, tabOf, type ScreenPath } from "../routes";
import { Icon } from "../ui";
import { useTrackScreen } from "./lastScreen";
import { MapBackdrop } from "./MapBackdrop";
@ -82,6 +82,7 @@ export function Layout({
label={screen.label}
icon={screen.icon}
badge={attention[screen.path] ?? 0}
active={tabOf(pathname) === screen.path}
/>
))}
</nav>
@ -94,21 +95,21 @@ function NavTab({
label,
icon,
badge,
active,
}: {
path: ScreenPath;
label: string;
icon: (typeof SCREENS)[number]["icon"];
badge: number;
active: boolean;
}) {
return (
<NavLink
<Link
to={path}
end={path === "/"}
className={({ isActive }) =>
`relative flex min-w-0 flex-1 flex-col items-center justify-center gap-0.5 text-xs font-medium transition-colors md:flex-none md:py-2 ${
isActive ? "text-action" : "text-ink-300 hover:text-ink-50"
}`
}
aria-current={active ? "page" : undefined}
className={`relative flex min-w-0 flex-1 flex-col items-center justify-center gap-0.5 text-xs font-medium transition-colors md:flex-none md:py-2 ${
active ? "text-action" : "text-ink-300 hover:text-ink-50"
}`}
>
<span className="relative grid size-6 place-items-center">
<Icon name={icon} className="size-5.5" />
@ -123,7 +124,7 @@ function NavTab({
)}
</span>
{label}
</NavLink>
</Link>
);
}

View File

@ -13,6 +13,6 @@ export function tabAttention(state: PlayerState): Partial<Record<ScreenPath, num
// Goals to claim and radio calls to answer (CR-026).
"/": state.goals.filter((g) => g.status === "ready").length + state.radioCalls.length,
"/contracts": ready,
"/pilots": state.pilots.reduce((sum, p) => sum + p.skillPointsAvailable, 0),
"/fleet": state.pilots.reduce((sum, p) => sum + p.skillPointsAvailable, 0),
};
}

View File

@ -1,16 +1,19 @@
import type { IconName } from "./ui/icons";
/** The app's top-level screens, in navigation order. Labels are short to fit eight tabs. */
/** The main tabs, in navigation order (decision 0.5.167). */
export const SCREENS = [
// The Dashboard, "Bridge" in the navigation (CR-020, decision 0.5.118).
{ path: "/", title: "Dashboard", label: "Bridge", icon: "helm" },
{ path: "/map", title: "Map", label: "Map", icon: "map" },
{ path: "/contracts", title: "Contracts", label: "Jobs", icon: "jobs" },
{ path: "/", title: "Bridge", label: "Bridge", icon: "helm" },
{ path: "/contracts", title: "Jobs", label: "Jobs", icon: "jobs" },
{ path: "/fleet", title: "Fleet", label: "Fleet", icon: "fleet" },
{ path: "/pilots", title: "Pilots", label: "Pilots", icon: "pilots" },
{ path: "/hqs", title: "HQs", label: "HQs", icon: "hq" },
{ path: "/market", title: "Market", label: "Shop", icon: "market" },
{ path: "/locations", title: "Locations", label: "Places", icon: "places" },
{ path: "/more", title: "More", label: "More", icon: "more" },
] as const satisfies readonly { path: string; title: string; label: string; icon: IconName }[];
export type ScreenPath = (typeof SCREENS)[number]["path"];
/** The tab a screen belongs to: Crew sits under Fleet, the map under the Bridge. */
export function tabOf(pathname: string): ScreenPath {
if (pathname === "/" || pathname === "/map") return "/";
if (pathname === "/contracts") return "/contracts";
if (pathname === "/fleet" || pathname === "/pilots") return "/fleet";
return "/more";
}

View File

@ -12,9 +12,9 @@ export function Contracts() {
const state = useLoadedPlayer();
const [selected, setSelected] = useState<Contract | null>(null);
return (
<Screen className="max-w-4xl">
<Screen>
<ScreenHeader
title="Contracts"
title="Jobs"
eyebrow={state.tutorial ? content.tutorialCompanyName : "Freight market"}
icon="jobs"
/>

View File

@ -94,7 +94,7 @@ export function Dashboard() {
return (
<Screen>
<ScreenHeader title="Dashboard" eyebrow={state.company.name} icon="helm" />
<ScreenHeader title="Bridge" eyebrow={state.company.name} icon="helm" />
{/* While the fleet sleeps, first thing on the Bridge (CR-024). */}
<FleetAsleep />
{/* Calls waiting for an answer, each reopening its card (CR-026). */}
@ -216,7 +216,7 @@ export function Dashboard() {
)}
{!state.tutorial && state.ships.length === 0 && (
<p className="text-sm text-ink-400">No ships yet — buy one in the Market.</p>
<p className="text-sm text-ink-400">No ships yet — buy one in the Shipyard.</p>
)}
</div>
@ -260,7 +260,7 @@ function YourShip({ className = "" }: { className?: string }) {
<p className="mt-2 text-sm text-ink-200">You&apos;re not flying a ship right now.</p>
<Link to="/pilots" className={`mt-3 ${buttonClass("secondary", "sm")}`}>
<Icon name="pilots" className="size-4" />
Assign yourself on Pilots
Assign yourself in Crew
</Link>
</Panel>
);

View File

@ -7,6 +7,7 @@ import {
validateReposition,
type RepositionFailure,
} from "@deadhead/sim";
import { FleetViews } from "../components/FleetViews";
import { useState } from "react";
import { Link } from "react-router";
import { useClock } from "../clock/clock";
@ -52,13 +53,14 @@ export function Fleet() {
}
icon="fleet"
/>
<FleetViews />
{state.ships.length === 0 ? (
<Panel tone="muted" className="flex flex-col items-center gap-3 py-8 text-center">
<Art sprite="ship-courier" className="size-20 opacity-60" />
<p className="text-sm text-ink-300">
No ships yet —{" "}
<Link to="/market" className="font-semibold text-amber-300 underline">
visit the Market
visit the Shipyard
</Link>
.
</p>

View File

@ -27,7 +27,7 @@ export function Locations() {
return (
<Screen>
<ScreenHeader title="Locations" eyebrow="Known space" icon="places" />
<ScreenHeader title="Places" eyebrow="Known space" icon="places" />
{lease.isError && (
<p role="alert" className="text-sm text-rose-300">
{errorMessage(lease.error)}

View File

@ -29,7 +29,7 @@ export function Market() {
return (
<Screen>
<ScreenHeader
title="Market"
title="Shipyard"
eyebrow="Shipyard"
icon="market"
subtitle="New ships are delivered with no pilot to one of your HQs."

View File

@ -0,0 +1,33 @@
import { Link } from "react-router";
import { Icon, Screen, ScreenHeader, type IconName } from "../ui";
const ROWS: { to: string; title: string; detail: string; icon: IconName }[] = [
{ to: "/market", title: "Shipyard", detail: "Compare and buy ships", icon: "market" },
{ to: "/locations", title: "Places", detail: "Everywhere you can fly", icon: "places" },
{ to: "/hqs", title: "HQs", detail: "Your bases and who's there", icon: "hq" },
{ to: "/logbook", title: "Logbook", detail: "Every job and refuel, 7 days", icon: "route" },
{ to: "/settings", title: "Settings", detail: "Company, display and save", icon: "gear" },
];
/** Everything that isn't one of the main tabs (decision 0.5.167). */
export function More() {
return (
<Screen>
<ScreenHeader title="More" />
<ul className="divide-y divide-glass-line">
{ROWS.map((row) => (
<li key={row.to}>
<Link to={row.to} className="flex min-h-15 items-center gap-3 py-2">
<Icon name={row.icon} className="size-5 shrink-0 text-ink-300" />
<span className="min-w-0 flex-1">
<span className="block font-medium">{row.title}</span>
<span className="block text-[13px] text-ink-300">{row.detail}</span>
</span>
<Icon name="arrow" className="size-4 shrink-0 text-ink-400" />
</Link>
</li>
))}
</ul>
</Screen>
);
}

View File

@ -1,3 +1,4 @@
import { FleetViews } from "../components/FleetViews";
import { content } from "@deadhead/content";
import { PilotIndependence } from "../components/IndependentPilots";
import type { Pilot } from "@deadhead/schema";
@ -29,7 +30,8 @@ export function Pilots() {
const state = useLoadedPlayer();
return (
<Screen>
<ScreenHeader title="Pilots" eyebrow={`${state.pilots.length} on the roster`} icon="pilots" />
<ScreenHeader title="Crew" eyebrow={`${state.pilots.length} on the roster`} icon="pilots" />
<FleetViews />
<HirePilot />
{!state.pilots.some((p) => !p.isAvatar) && (
<p className="text-sm text-ink-400">

View File

@ -90,6 +90,14 @@ const PATHS = {
<path d="M12 2v3.5M12 18.5V22M2 12h3.5M18.5 12H22M4.9 4.9l2.5 2.5M16.6 16.6l2.5 2.5M4.9 19.1l2.5-2.5M16.6 7.4l2.5-2.5" />
</>
),
// Three dots.
more: (
<>
<circle cx="5" cy="12" r="1.2" />
<circle cx="12" cy="12" r="1.2" />
<circle cx="19" cy="12" r="1.2" />
</>
),
// A cog (outline after Lucide "settings", ISC licence).
gear: (
<>

View File

@ -53,7 +53,7 @@ describe("navigation attention badges (Step 10E.3)", () => {
],
} 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 });
expect(tabAttention(state)).toEqual({ "/": 2, "/contracts": 1, "/fleet": 3 });
});
it("nudges toward Jobs during the tutorial until a run is in flight", () => {
@ -67,7 +67,7 @@ describe("navigation attention badges (Step 10E.3)", () => {
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();
expect(within(nav).queryByTestId("badge-Fleet")).not.toBeInTheDocument();
});
});
@ -99,16 +99,16 @@ describe("Pilots tab badge (Step 17.5, decision 0.5.109)", () => {
});
renderApp(api, "/pilots");
const nav = await screen.findByRole("navigation", { name: "Main" });
expect(await within(nav).findByTestId("badge-Pilots")).toHaveTextContent("3");
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-Pilots")).toHaveTextContent("2"));
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-Pilots")).not.toBeInTheDocument());
await waitFor(() => expect(within(nav).queryByTestId("badge-Fleet")).not.toBeInTheDocument());
});
});

View File

@ -250,7 +250,7 @@ describe("the highlights pop-up (Step 25.3, decision 0.5.140)", () => {
it("does not show the dialog when there is nothing to report", async () => {
renderApp(fakeApi({ getPlayerState: () => samplePlayerState() }));
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
await screen.findByRole("heading", { level: 1, name: "Bridge" });
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});

View File

@ -61,7 +61,7 @@ describe("company settings (Step 10K.7)", () => {
await userEvent.click(confirm);
await waitFor(() => expect(api.call).toHaveBeenCalledWith("newCompany", {}));
expect(await screen.findByRole("heading", { level: 1, name: "Contracts" })).toBeInTheDocument();
expect(await screen.findByRole("heading", { level: 1, name: "Jobs" })).toBeInTheDocument();
expect(await screen.findByLabelText("Intro")).toBeInTheDocument();
});
});

View File

@ -54,7 +54,7 @@ describe("Dashboard (CR-020)", () => {
activeDispatches: [dispatch("b", NOW + 125_000), dispatch("c", NOW + 65_000)],
});
renderApp(fakeApi({ getPlayerState: () => state }));
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
await screen.findByRole("heading", { level: 1, name: "Bridge" });
const statValue = (label: string) => screen.getByText(label).nextElementSibling;
expect(statValue("Ships idle")).toHaveTextContent("1");
@ -135,7 +135,7 @@ describe("loan on HQ", () => {
it("is hidden once the loan is paid off", async () => {
renderApp(fakeApi({ getPlayerState: () => owner(2000, 0) }));
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
await screen.findByRole("heading", { level: 1, name: "Bridge" });
expect(screen.queryByText("Loan from Halvorsen")).not.toBeInTheDocument();
});
@ -283,7 +283,7 @@ describe("your ship on the Dashboard (CR-020)", () => {
renderApp(fakeApi({ getPlayerState: () => owner({ ships: [] }) }));
const p = await panel();
expect(p).toHaveTextContent("You're not flying a ship right now.");
expect(within(p).getByRole("link", { name: "Assign yourself on Pilots" })).toHaveAttribute(
expect(within(p).getByRole("link", { name: "Assign yourself in Crew" })).toHaveAttribute(
"href",
"/pilots",
);

View File

@ -67,7 +67,7 @@ describe("the deal", () => {
}),
"/contracts",
);
await screen.findByRole("heading", { level: 1, name: "Contracts" });
await screen.findByRole("heading", { level: 1, name: "Jobs" });
expect(
screen.queryByRole("dialog", { name: "An offer from Halvorsen" }),
).not.toBeInTheDocument();
@ -154,7 +154,7 @@ describe("the deal", () => {
);
await userEvent.click(await screen.findByRole("button", { name: "Shake on it" }));
await waitFor(() => expect(api.call).toHaveBeenCalledWith("acceptDeal", {}));
expect(await screen.findByRole("heading", { level: 1, name: "Dashboard" })).toBeInTheDocument();
expect(await screen.findByRole("heading", { level: 1, name: "Bridge" })).toBeInTheDocument();
expect(
screen.queryByRole("dialog", { name: "An offer from Halvorsen" }),
).not.toBeInTheDocument();

View File

@ -106,7 +106,7 @@ describe("fleet sleep on screen (Step 24.5, decisions 0.5.137, 0.5.138)", () =>
it("shows nothing on the Bridge while awake", async () => {
renderApp(fakeApi({ getPlayerState: () => owner(false) }), "/");
expect(await screen.findByRole("heading", { name: "Dashboard" })).toBeInTheDocument();
expect(await screen.findByRole("heading", { name: "Bridge" })).toBeInTheDocument();
expect(screen.queryByLabelText("Fleet asleep")).not.toBeInTheDocument();
});
@ -115,7 +115,7 @@ describe("fleet sleep on screen (Step 24.5, decisions 0.5.137, 0.5.138)", () =>
pilots: [samplePlayerState().pilots[0]!, { ...rosa, mode: "manual" }],
});
renderApp(fakeApi({ getPlayerState: () => manual }), "/");
expect(await screen.findByRole("heading", { name: "Dashboard" })).toBeInTheDocument();
expect(await screen.findByRole("heading", { name: "Bridge" })).toBeInTheDocument();
expect(screen.queryByLabelText("Fleet asleep")).not.toBeInTheDocument();
});

View File

@ -66,7 +66,7 @@ describe("goals on HQ", () => {
it("is not shown during the tutorial", async () => {
renderApp(fakeApi({ getPlayerState: () => samplePlayerState() }));
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
await screen.findByRole("heading", { level: 1, name: "Bridge" });
expect(screen.queryByRole("region", { name: "Goals" })).not.toBeInTheDocument();
});
});

View File

@ -39,14 +39,47 @@ describe("app layout", () => {
);
});
it("opens on the Bridge tab, the Dashboard, with HQs among eight tabs (CR-020)", async () => {
it("opens on the Bridge among four tabs (CR-029)", async () => {
renderApp(signedIn());
const nav = await screen.findByRole("navigation", { name: "Main" });
const links = within(nav).getAllByRole("link");
expect(links).toHaveLength(8);
expect(links[0]).toHaveAccessibleName("Bridge");
expect(links.map((l) => l.textContent)).toEqual(["Bridge", "Jobs", "Fleet", "More"]);
expect(links[0]).toHaveAttribute("aria-current", "page");
expect(screen.getByRole("heading", { level: 1, name: "Dashboard" })).toBeInTheDocument();
expect(within(nav).getByRole("link", { name: "HQs" })).toHaveAttribute("href", "/hqs");
expect(screen.getByRole("heading", { level: 1, name: "Bridge" })).toBeInTheDocument();
});
it.each([
["/map", "Map", "Bridge"],
["/pilots", "Crew", "Fleet"],
["/market", "Shipyard", "More"],
["/locations", "Places", "More"],
["/hqs", "HQs", "More"],
["/logbook", "Logbook", "More"],
["/settings", "Settings", "More"],
])("still opens %s, as %s under %s", async (path, title, tab) => {
renderApp(signedIn(), path);
expect(await screen.findByRole("heading", { level: 1, name: title })).toBeInTheDocument();
const nav = screen.getByRole("navigation", { name: "Main" });
expect(within(nav).getByRole("link", { name: tab })).toHaveAttribute("aria-current", "page");
});
it("switches between Ships and Crew inside Fleet", async () => {
renderApp(signedIn(), "/fleet");
const views = await screen.findByRole("navigation", { name: "Fleet views" });
await userEvent.click(within(views).getByRole("link", { name: /^Crew/ }));
expect(await screen.findByRole("heading", { level: 1, name: "Crew" })).toBeInTheDocument();
});
it("More reaches every screen without a tab", async () => {
renderApp(signedIn(), "/more");
await screen.findByRole("heading", { level: 1, name: "More" });
for (const [name, href] of [
[/^Shipyard/, "/market"],
[/^Places/, "/locations"],
[/^HQs/, "/hqs"],
[/^Logbook/, "/logbook"],
] as const) {
expect(screen.getByRole("link", { name })).toHaveAttribute("href", href);
}
});
});

View File

@ -24,7 +24,7 @@ describe("guest onboarding", () => {
expect(api.call).toHaveBeenCalledWith("createGuest", {});
// Lands on Contracts with the intro and the light contract suggested.
expect(await screen.findByRole("heading", { level: 1, name: "Contracts" })).toBeInTheDocument();
expect(await screen.findByRole("heading", { level: 1, name: "Jobs" })).toBeInTheDocument();
expect(await screen.findByLabelText("Intro")).toHaveTextContent(
"Halvorsen Lunar Freight needs a pilot.",
);
@ -44,7 +44,7 @@ describe("guest onboarding", () => {
it("skips the start screen when a stored session is still valid", async () => {
const api = fakeApi({ getPlayerState: () => samplePlayerState() });
renderApp(api);
expect(await screen.findByRole("heading", { level: 1, name: "Dashboard" })).toBeInTheDocument();
expect(await screen.findByRole("heading", { level: 1, name: "Bridge" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Start your company" })).not.toBeInTheDocument();
expect(api.call).not.toHaveBeenCalledWith("createGuest", expect.anything());
});

View File

@ -46,7 +46,7 @@ describe("player state store", () => {
getPlayerState: () => samplePlayerState({ activeDispatches: [inFlight(NOW + 30_000)] }),
});
renderApp(api);
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
await screen.findByRole("heading", { level: 1, name: "Bridge" });
expect(stateCalls(api)).toBe(1);
act(() => vi.advanceTimersByTime(29_000));
@ -67,7 +67,7 @@ describe("player state store on focus", () => {
it("refetches when the app regains focus", async () => {
const api = fakeApi({ getPlayerState: () => samplePlayerState() });
renderApp(api);
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
await screen.findByRole("heading", { level: 1, name: "Bridge" });
const before = stateCalls(api);
// Leaving and returning to the app, as the browser reports it to TanStack Query.

View File

@ -70,7 +70,7 @@ describe("skill point pop-up (Step 17.6, decision 0.5.109)", () => {
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Shake on it" }));
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
await screen.findByRole("heading", { level: 1, name: "Bridge" });
// The deal is done, but the permission prompt is still open.
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument();
@ -85,7 +85,7 @@ describe("skill point pop-up (Step 17.6, decision 0.5.109)", () => {
it("Go to Pilots opens the Pilots tab and the pop-up doesn't come back", async () => {
renderApp(fakeApi({ getPlayerState: () => afterDeal() }), "/");
await userEvent.click(await screen.findByRole("button", { name: "Go to Pilots" }));
await screen.findByRole("heading", { level: 1, name: "Pilots" });
await screen.findByRole("heading", { level: 1, name: "Crew" });
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument();
});
@ -100,7 +100,7 @@ describe("skill point pop-up (Step 17.6, decision 0.5.109)", () => {
await waitFor(() =>
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument(),
);
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
await screen.findByRole("heading", { level: 1, name: "Bridge" });
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument();
act(() => usePreferences.getState().showTipsAgain());
@ -109,7 +109,7 @@ describe("skill point pop-up (Step 17.6, decision 0.5.109)", () => {
it("doesn't appear with no unspent points", async () => {
renderApp(fakeApi({ getPlayerState: () => afterDeal(0, 0) }), "/");
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
await screen.findByRole("heading", { level: 1, name: "Bridge" });
// Give the prompt check a turn to settle.
await act(async () => {});
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument();

View File

@ -86,13 +86,13 @@ describe("empty states", () => {
it("HQ: no ships points to the Market", async () => {
show("/");
expect(await screen.findByText("No ships yet — buy one in the Market.")).toBeInTheDocument();
expect(await screen.findByText("No ships yet — buy one in the Shipyard.")).toBeInTheDocument();
expect(screen.getByText("Nothing in flight.")).toBeInTheDocument();
});
it("Fleet: no ships links to the Market", async () => {
show("/fleet");
expect(await screen.findByRole("link", { name: "visit the Market" })).toHaveAttribute(
expect(await screen.findByRole("link", { name: "visit the Shipyard" })).toHaveAttribute(
"href",
"/market",
);

View File

@ -52,7 +52,7 @@ describe("welcome cards (Step 22.2, decision 0.5.131)", () => {
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
expect(api.call).toHaveBeenCalledWith("markWelcomeSeen", {});
expect(state().welcomeSeen).toBe(true);
expect(await screen.findByRole("heading", { level: 1, name: "Contracts" })).toBeInTheDocument();
expect(await screen.findByRole("heading", { level: 1, name: "Jobs" })).toBeInTheDocument();
expect(await screen.findByLabelText("Intro")).toHaveTextContent("needs a pilot");
});

View File

@ -111,7 +111,7 @@ export const onboardingGoals: OnboardingGoal[] = [
{
// Was "Buy a second ship"; the deal now brings two (CR-013).
id: "market-ship",
title: "Buy a ship from the Market",
title: "Buy a ship from the Shipyard",
condition: { kind: "market-ships-bought", count: 1 },
reward: 1000,
},

View File

@ -45,7 +45,7 @@ describe("onboarding goal evaluation", () => {
it("doesn't count the deal's two ships as a Market purchase; a bought Lighter does (CR-013)", () => {
expect(isComplete("market-ship", fresh)).toBe(false);
expect(goals.find((g) => g.id === "market-ship")?.title).toBe("Buy a ship from the Market");
expect(goals.find((g) => g.id === "market-ship")?.title).toBe("Buy a ship from the Shipyard");
// After buying a Lighter: three ships, one of them from the Market.
expect(isComplete("market-ship", { ...fresh, shipsOwned: 3, marketShipsBought: 1 })).toBe(true);
});