-
+
{lease.isError && (
{errorMessage(lease.error)}
diff --git a/apps/web/src/screens/Market.tsx b/apps/web/src/screens/Market.tsx
index 742f3fd..1a2be21 100644
--- a/apps/web/src/screens/Market.tsx
+++ b/apps/web/src/screens/Market.tsx
@@ -29,7 +29,7 @@ export function Market() {
return (
+
+
+ {ROWS.map((row) => (
+ -
+
+
+
+ {row.title}
+ {row.detail}
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/screens/Pilots.tsx b/apps/web/src/screens/Pilots.tsx
index 5ebd9ab..3d0be9c 100644
--- a/apps/web/src/screens/Pilots.tsx
+++ b/apps/web/src/screens/Pilots.tsx
@@ -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 (
-
+
+
{!state.pilots.some((p) => !p.isAvatar) && (
diff --git a/apps/web/src/ui/icons.tsx b/apps/web/src/ui/icons.tsx
index 8178d7c..1d8ba87 100644
--- a/apps/web/src/ui/icons.tsx
+++ b/apps/web/src/ui/icons.tsx
@@ -90,6 +90,14 @@ const PATHS = {
>
),
+ // Three dots.
+ more: (
+ <>
+
+
+
+ >
+ ),
// A cog (outline after Lucide "settings", ISC licence).
gear: (
<>
diff --git a/apps/web/test/attention.test.tsx b/apps/web/test/attention.test.tsx
index a471810..cefc339 100644
--- a/apps/web/test/attention.test.tsx
+++ b/apps/web/test/attention.test.tsx
@@ -53,7 +53,7 @@ describe("navigation attention badges (Step 10E.3)", () => {
],
} as Partial);
// 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());
});
});
diff --git a/apps/web/test/away-summary.test.tsx b/apps/web/test/away-summary.test.tsx
index ac1245b..0c735a4 100644
--- a/apps/web/test/away-summary.test.tsx
+++ b/apps/web/test/away-summary.test.tsx
@@ -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();
});
});
diff --git a/apps/web/test/company-settings.test.tsx b/apps/web/test/company-settings.test.tsx
index c153b8f..ba8cc1f 100644
--- a/apps/web/test/company-settings.test.tsx
+++ b/apps/web/test/company-settings.test.tsx
@@ -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();
});
});
diff --git a/apps/web/test/dashboard.test.tsx b/apps/web/test/dashboard.test.tsx
index 89d0669..6e52aa5 100644
--- a/apps/web/test/dashboard.test.tsx
+++ b/apps/web/test/dashboard.test.tsx
@@ -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",
);
diff --git a/apps/web/test/deal-modal.test.tsx b/apps/web/test/deal-modal.test.tsx
index e5ed042..6fdcc03 100644
--- a/apps/web/test/deal-modal.test.tsx
+++ b/apps/web/test/deal-modal.test.tsx
@@ -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();
diff --git a/apps/web/test/fleet-sleep.test.tsx b/apps/web/test/fleet-sleep.test.tsx
index e7a5d5b..c284157 100644
--- a/apps/web/test/fleet-sleep.test.tsx
+++ b/apps/web/test/fleet-sleep.test.tsx
@@ -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();
});
diff --git a/apps/web/test/goals.test.tsx b/apps/web/test/goals.test.tsx
index 8ccba43..c61ed58 100644
--- a/apps/web/test/goals.test.tsx
+++ b/apps/web/test/goals.test.tsx
@@ -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();
});
});
diff --git a/apps/web/test/layout.test.tsx b/apps/web/test/layout.test.tsx
index e84a5eb..686a108 100644
--- a/apps/web/test/layout.test.tsx
+++ b/apps/web/test/layout.test.tsx
@@ -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);
+ }
});
});
diff --git a/apps/web/test/onboarding.test.tsx b/apps/web/test/onboarding.test.tsx
index 3e0547c..5946754 100644
--- a/apps/web/test/onboarding.test.tsx
+++ b/apps/web/test/onboarding.test.tsx
@@ -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());
});
diff --git a/apps/web/test/player-state.test.tsx b/apps/web/test/player-state.test.tsx
index 3d9f03c..7153d2e 100644
--- a/apps/web/test/player-state.test.tsx
+++ b/apps/web/test/player-state.test.tsx
@@ -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.
diff --git a/apps/web/test/skill-point-prompt.test.tsx b/apps/web/test/skill-point-prompt.test.tsx
index b1e19fc..814ddea 100644
--- a/apps/web/test/skill-point-prompt.test.tsx
+++ b/apps/web/test/skill-point-prompt.test.tsx
@@ -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();
diff --git a/apps/web/test/states.test.tsx b/apps/web/test/states.test.tsx
index 25f5a39..e968ca3 100644
--- a/apps/web/test/states.test.tsx
+++ b/apps/web/test/states.test.tsx
@@ -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",
);
diff --git a/apps/web/test/welcome.test.tsx b/apps/web/test/welcome.test.tsx
index 415cb98..884b050 100644
--- a/apps/web/test/welcome.test.tsx
+++ b/apps/web/test/welcome.test.tsx
@@ -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");
});
diff --git a/packages/content/src/data/onboarding.ts b/packages/content/src/data/onboarding.ts
index 4e93bf5..90bc58a 100644
--- a/packages/content/src/data/onboarding.ts
+++ b/packages/content/src/data/onboarding.ts
@@ -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,
},
diff --git a/packages/sim/test/goals.test.ts b/packages/sim/test/goals.test.ts
index 25e703b..c65f4e0 100644
--- a/packages/sim/test/goals.test.ts
+++ b/packages/sim/test/goals.test.ts
@@ -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);
});