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:
parent
06bf2f17b1
commit
38b636014f
|
|
@ -130,7 +130,7 @@ export async function playToDeal(page: Page) {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await dismissLogbook(page);
|
await dismissLogbook(page);
|
||||||
await page.getByRole("button", { name: "Shake on it" }).click();
|
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.
|
// 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" });
|
const skills = page.getByRole("dialog", { name: "Your pilots are ready to train" });
|
||||||
await expect(skills.getByLabel("Unspent skill points")).toContainText(/You: \d+ points?/);
|
await expect(skills.getByLabel("Unspent skill points")).toContainText(/You: \d+ points?/);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { HQs } from "./screens/HQs";
|
||||||
import { Locations } from "./screens/Locations";
|
import { Locations } from "./screens/Locations";
|
||||||
import { MapScreen } from "./screens/MapScreen";
|
import { MapScreen } from "./screens/MapScreen";
|
||||||
import { Market } from "./screens/Market";
|
import { Market } from "./screens/Market";
|
||||||
|
import { More } from "./screens/More";
|
||||||
import { Pilots } from "./screens/Pilots";
|
import { Pilots } from "./screens/Pilots";
|
||||||
import { Logbook } from "./screens/Logbook";
|
import { Logbook } from "./screens/Logbook";
|
||||||
import { Settings } from "./screens/Settings";
|
import { Settings } from "./screens/Settings";
|
||||||
|
|
@ -30,13 +31,9 @@ import { Button } from "./ui";
|
||||||
|
|
||||||
const SCREEN_COMPONENTS: Record<(typeof SCREENS)[number]["path"], ReactNode> = {
|
const SCREEN_COMPONENTS: Record<(typeof SCREENS)[number]["path"], ReactNode> = {
|
||||||
"/": <Dashboard />,
|
"/": <Dashboard />,
|
||||||
"/map": <MapScreen />,
|
|
||||||
"/contracts": <Contracts />,
|
"/contracts": <Contracts />,
|
||||||
"/fleet": <Fleet />,
|
"/fleet": <Fleet />,
|
||||||
"/pilots": <Pilots />,
|
"/more": <More />,
|
||||||
"/hqs": <HQs />,
|
|
||||||
"/market": <Market />,
|
|
||||||
"/locations": <Locations />,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Reads the game clock (the engine's device clock, CR-014), then ticks it every second. */
|
/** 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]}
|
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 />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
{/* The Logbook opens from "While you were away" and the Bridge (CR-025). */}
|
{/* The Logbook opens from "While you were away" and the Bridge (CR-025). */}
|
||||||
<Route path="/logbook" element={<Logbook />} />
|
<Route path="/logbook" element={<Logbook />} />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { CSSProperties, ReactNode } from "react";
|
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 { GameClock } from "../clock/GameClock";
|
||||||
import { SCREENS, type ScreenPath } from "../routes";
|
import { SCREENS, tabOf, type ScreenPath } from "../routes";
|
||||||
import { Icon } from "../ui";
|
import { Icon } from "../ui";
|
||||||
import { useTrackScreen } from "./lastScreen";
|
import { useTrackScreen } from "./lastScreen";
|
||||||
import { MapBackdrop } from "./MapBackdrop";
|
import { MapBackdrop } from "./MapBackdrop";
|
||||||
|
|
@ -82,6 +82,7 @@ export function Layout({
|
||||||
label={screen.label}
|
label={screen.label}
|
||||||
icon={screen.icon}
|
icon={screen.icon}
|
||||||
badge={attention[screen.path] ?? 0}
|
badge={attention[screen.path] ?? 0}
|
||||||
|
active={tabOf(pathname) === screen.path}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -94,21 +95,21 @@ function NavTab({
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
badge,
|
badge,
|
||||||
|
active,
|
||||||
}: {
|
}: {
|
||||||
path: ScreenPath;
|
path: ScreenPath;
|
||||||
label: string;
|
label: string;
|
||||||
icon: (typeof SCREENS)[number]["icon"];
|
icon: (typeof SCREENS)[number]["icon"];
|
||||||
badge: number;
|
badge: number;
|
||||||
|
active: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<Link
|
||||||
to={path}
|
to={path}
|
||||||
end={path === "/"}
|
aria-current={active ? "page" : undefined}
|
||||||
className={({ isActive }) =>
|
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 ${
|
||||||
`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"
|
||||||
isActive ? "text-action" : "text-ink-300 hover:text-ink-50"
|
}`}
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className="relative grid size-6 place-items-center">
|
<span className="relative grid size-6 place-items-center">
|
||||||
<Icon name={icon} className="size-5.5" />
|
<Icon name={icon} className="size-5.5" />
|
||||||
|
|
@ -123,7 +124,7 @@ function NavTab({
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{label}
|
{label}
|
||||||
</NavLink>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,6 @@ export function tabAttention(state: PlayerState): Partial<Record<ScreenPath, num
|
||||||
// Goals to claim and radio calls to answer (CR-026).
|
// Goals to claim and radio calls to answer (CR-026).
|
||||||
"/": state.goals.filter((g) => g.status === "ready").length + state.radioCalls.length,
|
"/": state.goals.filter((g) => g.status === "ready").length + state.radioCalls.length,
|
||||||
"/contracts": ready,
|
"/contracts": ready,
|
||||||
"/pilots": state.pilots.reduce((sum, p) => sum + p.skillPointsAvailable, 0),
|
"/fleet": state.pilots.reduce((sum, p) => sum + p.skillPointsAvailable, 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
import type { IconName } from "./ui/icons";
|
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 = [
|
export const SCREENS = [
|
||||||
// The Dashboard, "Bridge" in the navigation (CR-020, decision 0.5.118).
|
{ path: "/", title: "Bridge", label: "Bridge", icon: "helm" },
|
||||||
{ path: "/", title: "Dashboard", label: "Bridge", icon: "helm" },
|
{ path: "/contracts", title: "Jobs", label: "Jobs", icon: "jobs" },
|
||||||
{ path: "/map", title: "Map", label: "Map", icon: "map" },
|
|
||||||
{ path: "/contracts", title: "Contracts", label: "Jobs", icon: "jobs" },
|
|
||||||
{ path: "/fleet", title: "Fleet", label: "Fleet", icon: "fleet" },
|
{ path: "/fleet", title: "Fleet", label: "Fleet", icon: "fleet" },
|
||||||
{ path: "/pilots", title: "Pilots", label: "Pilots", icon: "pilots" },
|
{ path: "/more", title: "More", label: "More", icon: "more" },
|
||||||
{ path: "/hqs", title: "HQs", label: "HQs", icon: "hq" },
|
|
||||||
{ path: "/market", title: "Market", label: "Shop", icon: "market" },
|
|
||||||
{ path: "/locations", title: "Locations", label: "Places", icon: "places" },
|
|
||||||
] as const satisfies readonly { path: string; title: string; label: string; icon: IconName }[];
|
] as const satisfies readonly { path: string; title: string; label: string; icon: IconName }[];
|
||||||
|
|
||||||
export type ScreenPath = (typeof SCREENS)[number]["path"];
|
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";
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ export function Contracts() {
|
||||||
const state = useLoadedPlayer();
|
const state = useLoadedPlayer();
|
||||||
const [selected, setSelected] = useState<Contract | null>(null);
|
const [selected, setSelected] = useState<Contract | null>(null);
|
||||||
return (
|
return (
|
||||||
<Screen className="max-w-4xl">
|
<Screen>
|
||||||
<ScreenHeader
|
<ScreenHeader
|
||||||
title="Contracts"
|
title="Jobs"
|
||||||
eyebrow={state.tutorial ? content.tutorialCompanyName : "Freight market"}
|
eyebrow={state.tutorial ? content.tutorialCompanyName : "Freight market"}
|
||||||
icon="jobs"
|
icon="jobs"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export function Dashboard() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Screen>
|
<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). */}
|
{/* While the fleet sleeps, first thing on the Bridge (CR-024). */}
|
||||||
<FleetAsleep />
|
<FleetAsleep />
|
||||||
{/* Calls waiting for an answer, each reopening its card (CR-026). */}
|
{/* Calls waiting for an answer, each reopening its card (CR-026). */}
|
||||||
|
|
@ -216,7 +216,7 @@ export function Dashboard() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!state.tutorial && state.ships.length === 0 && (
|
{!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>
|
</div>
|
||||||
|
|
||||||
|
|
@ -260,7 +260,7 @@ function YourShip({ className = "" }: { className?: string }) {
|
||||||
<p className="mt-2 text-sm text-ink-200">You're not flying a ship right now.</p>
|
<p className="mt-2 text-sm text-ink-200">You're not flying a ship right now.</p>
|
||||||
<Link to="/pilots" className={`mt-3 ${buttonClass("secondary", "sm")}`}>
|
<Link to="/pilots" className={`mt-3 ${buttonClass("secondary", "sm")}`}>
|
||||||
<Icon name="pilots" className="size-4" />
|
<Icon name="pilots" className="size-4" />
|
||||||
Assign yourself on Pilots
|
Assign yourself in Crew
|
||||||
</Link>
|
</Link>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
validateReposition,
|
validateReposition,
|
||||||
type RepositionFailure,
|
type RepositionFailure,
|
||||||
} from "@deadhead/sim";
|
} from "@deadhead/sim";
|
||||||
|
import { FleetViews } from "../components/FleetViews";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { useClock } from "../clock/clock";
|
import { useClock } from "../clock/clock";
|
||||||
|
|
@ -52,13 +53,14 @@ export function Fleet() {
|
||||||
}
|
}
|
||||||
icon="fleet"
|
icon="fleet"
|
||||||
/>
|
/>
|
||||||
|
<FleetViews />
|
||||||
{state.ships.length === 0 ? (
|
{state.ships.length === 0 ? (
|
||||||
<Panel tone="muted" className="flex flex-col items-center gap-3 py-8 text-center">
|
<Panel tone="muted" className="flex flex-col items-center gap-3 py-8 text-center">
|
||||||
<Art sprite="ship-courier" className="size-20 opacity-60" />
|
<Art sprite="ship-courier" className="size-20 opacity-60" />
|
||||||
<p className="text-sm text-ink-300">
|
<p className="text-sm text-ink-300">
|
||||||
No ships yet —{" "}
|
No ships yet —{" "}
|
||||||
<Link to="/market" className="font-semibold text-amber-300 underline">
|
<Link to="/market" className="font-semibold text-amber-300 underline">
|
||||||
visit the Market
|
visit the Shipyard
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function Locations() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Screen>
|
<Screen>
|
||||||
<ScreenHeader title="Locations" eyebrow="Known space" icon="places" />
|
<ScreenHeader title="Places" eyebrow="Known space" icon="places" />
|
||||||
{lease.isError && (
|
{lease.isError && (
|
||||||
<p role="alert" className="text-sm text-rose-300">
|
<p role="alert" className="text-sm text-rose-300">
|
||||||
{errorMessage(lease.error)}
|
{errorMessage(lease.error)}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export function Market() {
|
||||||
return (
|
return (
|
||||||
<Screen>
|
<Screen>
|
||||||
<ScreenHeader
|
<ScreenHeader
|
||||||
title="Market"
|
title="Shipyard"
|
||||||
eyebrow="Shipyard"
|
eyebrow="Shipyard"
|
||||||
icon="market"
|
icon="market"
|
||||||
subtitle="New ships are delivered with no pilot to one of your HQs."
|
subtitle="New ships are delivered with no pilot to one of your HQs."
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { FleetViews } from "../components/FleetViews";
|
||||||
import { content } from "@deadhead/content";
|
import { content } from "@deadhead/content";
|
||||||
import { PilotIndependence } from "../components/IndependentPilots";
|
import { PilotIndependence } from "../components/IndependentPilots";
|
||||||
import type { Pilot } from "@deadhead/schema";
|
import type { Pilot } from "@deadhead/schema";
|
||||||
|
|
@ -29,7 +30,8 @@ export function Pilots() {
|
||||||
const state = useLoadedPlayer();
|
const state = useLoadedPlayer();
|
||||||
return (
|
return (
|
||||||
<Screen>
|
<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 />
|
<HirePilot />
|
||||||
{!state.pilots.some((p) => !p.isAvatar) && (
|
{!state.pilots.some((p) => !p.isAvatar) && (
|
||||||
<p className="text-sm text-ink-400">
|
<p className="text-sm text-ink-400">
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
<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).
|
// A cog (outline after Lucide "settings", ISC licence).
|
||||||
gear: (
|
gear: (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ describe("navigation attention badges (Step 10E.3)", () => {
|
||||||
],
|
],
|
||||||
} as Partial<PlayerState>);
|
} as Partial<PlayerState>);
|
||||||
// Skill points: 2 + 0 + 1 across the three pilots (decision 0.5.109).
|
// 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", () => {
|
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 nav = await screen.findByRole("navigation", { name: "Main" });
|
||||||
const jobs = within(nav).getByRole("link", { name: "Jobs" });
|
const jobs = within(nav).getByRole("link", { name: "Jobs" });
|
||||||
expect(within(jobs).getByTestId("badge-Jobs")).toHaveTextContent("1");
|
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");
|
renderApp(api, "/pilots");
|
||||||
const nav = await screen.findByRole("navigation", { name: "Main" });
|
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) =>
|
const raise = async (pilot: string) =>
|
||||||
userEvent.click(
|
userEvent.click(
|
||||||
(await screen.findAllByRole("button", { name: new RegExp(`for ${pilot}$`) }))[0]!,
|
(await screen.findAllByRole("button", { name: new RegExp(`for ${pilot}$`) }))[0]!,
|
||||||
);
|
);
|
||||||
await raise(avatar.name);
|
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(avatar.name);
|
||||||
await raise("Rosa Vance");
|
await raise("Rosa Vance");
|
||||||
await waitFor(() => expect(within(nav).queryByTestId("badge-Pilots")).not.toBeInTheDocument());
|
await waitFor(() => expect(within(nav).queryByTestId("badge-Fleet")).not.toBeInTheDocument());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
it("does not show the dialog when there is nothing to report", async () => {
|
||||||
renderApp(fakeApi({ getPlayerState: () => samplePlayerState() }));
|
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();
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ describe("company settings (Step 10K.7)", () => {
|
||||||
|
|
||||||
await userEvent.click(confirm);
|
await userEvent.click(confirm);
|
||||||
await waitFor(() => expect(api.call).toHaveBeenCalledWith("newCompany", {}));
|
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();
|
expect(await screen.findByLabelText("Intro")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ describe("Dashboard (CR-020)", () => {
|
||||||
activeDispatches: [dispatch("b", NOW + 125_000), dispatch("c", NOW + 65_000)],
|
activeDispatches: [dispatch("b", NOW + 125_000), dispatch("c", NOW + 65_000)],
|
||||||
});
|
});
|
||||||
renderApp(fakeApi({ getPlayerState: () => state }));
|
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;
|
const statValue = (label: string) => screen.getByText(label).nextElementSibling;
|
||||||
expect(statValue("Ships idle")).toHaveTextContent("1");
|
expect(statValue("Ships idle")).toHaveTextContent("1");
|
||||||
|
|
@ -135,7 +135,7 @@ describe("loan on HQ", () => {
|
||||||
|
|
||||||
it("is hidden once the loan is paid off", async () => {
|
it("is hidden once the loan is paid off", async () => {
|
||||||
renderApp(fakeApi({ getPlayerState: () => owner(2000, 0) }));
|
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();
|
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: [] }) }));
|
renderApp(fakeApi({ getPlayerState: () => owner({ ships: [] }) }));
|
||||||
const p = await panel();
|
const p = await panel();
|
||||||
expect(p).toHaveTextContent("You're not flying a ship right now.");
|
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",
|
"href",
|
||||||
"/pilots",
|
"/pilots",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ describe("the deal", () => {
|
||||||
}),
|
}),
|
||||||
"/contracts",
|
"/contracts",
|
||||||
);
|
);
|
||||||
await screen.findByRole("heading", { level: 1, name: "Contracts" });
|
await screen.findByRole("heading", { level: 1, name: "Jobs" });
|
||||||
expect(
|
expect(
|
||||||
screen.queryByRole("dialog", { name: "An offer from Halvorsen" }),
|
screen.queryByRole("dialog", { name: "An offer from Halvorsen" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
|
|
@ -154,7 +154,7 @@ describe("the deal", () => {
|
||||||
);
|
);
|
||||||
await userEvent.click(await screen.findByRole("button", { name: "Shake on it" }));
|
await userEvent.click(await screen.findByRole("button", { name: "Shake on it" }));
|
||||||
await waitFor(() => expect(api.call).toHaveBeenCalledWith("acceptDeal", {}));
|
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(
|
expect(
|
||||||
screen.queryByRole("dialog", { name: "An offer from Halvorsen" }),
|
screen.queryByRole("dialog", { name: "An offer from Halvorsen" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
it("shows nothing on the Bridge while awake", async () => {
|
||||||
renderApp(fakeApi({ getPlayerState: () => owner(false) }), "/");
|
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();
|
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" }],
|
pilots: [samplePlayerState().pilots[0]!, { ...rosa, mode: "manual" }],
|
||||||
});
|
});
|
||||||
renderApp(fakeApi({ getPlayerState: () => 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();
|
expect(screen.queryByLabelText("Fleet asleep")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ describe("goals on HQ", () => {
|
||||||
|
|
||||||
it("is not shown during the tutorial", async () => {
|
it("is not shown during the tutorial", async () => {
|
||||||
renderApp(fakeApi({ getPlayerState: () => samplePlayerState() }));
|
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();
|
expect(screen.queryByRole("region", { name: "Goals" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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());
|
renderApp(signedIn());
|
||||||
const nav = await screen.findByRole("navigation", { name: "Main" });
|
const nav = await screen.findByRole("navigation", { name: "Main" });
|
||||||
const links = within(nav).getAllByRole("link");
|
const links = within(nav).getAllByRole("link");
|
||||||
expect(links).toHaveLength(8);
|
expect(links.map((l) => l.textContent)).toEqual(["Bridge", "Jobs", "Fleet", "More"]);
|
||||||
expect(links[0]).toHaveAccessibleName("Bridge");
|
|
||||||
expect(links[0]).toHaveAttribute("aria-current", "page");
|
expect(links[0]).toHaveAttribute("aria-current", "page");
|
||||||
expect(screen.getByRole("heading", { level: 1, name: "Dashboard" })).toBeInTheDocument();
|
expect(screen.getByRole("heading", { level: 1, name: "Bridge" })).toBeInTheDocument();
|
||||||
expect(within(nav).getByRole("link", { name: "HQs" })).toHaveAttribute("href", "/hqs");
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ describe("guest onboarding", () => {
|
||||||
expect(api.call).toHaveBeenCalledWith("createGuest", {});
|
expect(api.call).toHaveBeenCalledWith("createGuest", {});
|
||||||
|
|
||||||
// Lands on Contracts with the intro and the light contract suggested.
|
// 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(
|
expect(await screen.findByLabelText("Intro")).toHaveTextContent(
|
||||||
"Halvorsen Lunar Freight needs a pilot.",
|
"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 () => {
|
it("skips the start screen when a stored session is still valid", async () => {
|
||||||
const api = fakeApi({ getPlayerState: () => samplePlayerState() });
|
const api = fakeApi({ getPlayerState: () => samplePlayerState() });
|
||||||
renderApp(api);
|
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(screen.queryByRole("button", { name: "Start your company" })).not.toBeInTheDocument();
|
||||||
expect(api.call).not.toHaveBeenCalledWith("createGuest", expect.anything());
|
expect(api.call).not.toHaveBeenCalledWith("createGuest", expect.anything());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ describe("player state store", () => {
|
||||||
getPlayerState: () => samplePlayerState({ activeDispatches: [inFlight(NOW + 30_000)] }),
|
getPlayerState: () => samplePlayerState({ activeDispatches: [inFlight(NOW + 30_000)] }),
|
||||||
});
|
});
|
||||||
renderApp(api);
|
renderApp(api);
|
||||||
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
|
await screen.findByRole("heading", { level: 1, name: "Bridge" });
|
||||||
expect(stateCalls(api)).toBe(1);
|
expect(stateCalls(api)).toBe(1);
|
||||||
|
|
||||||
act(() => vi.advanceTimersByTime(29_000));
|
act(() => vi.advanceTimersByTime(29_000));
|
||||||
|
|
@ -67,7 +67,7 @@ describe("player state store on focus", () => {
|
||||||
it("refetches when the app regains focus", async () => {
|
it("refetches when the app regains focus", async () => {
|
||||||
const api = fakeApi({ getPlayerState: () => samplePlayerState() });
|
const api = fakeApi({ getPlayerState: () => samplePlayerState() });
|
||||||
renderApp(api);
|
renderApp(api);
|
||||||
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
|
await screen.findByRole("heading", { level: 1, name: "Bridge" });
|
||||||
const before = stateCalls(api);
|
const before = stateCalls(api);
|
||||||
|
|
||||||
// Leaving and returning to the app, as the browser reports it to TanStack Query.
|
// Leaving and returning to the app, as the browser reports it to TanStack Query.
|
||||||
|
|
|
||||||
|
|
@ -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();
|
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument();
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Shake on it" }));
|
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.
|
// The deal is done, but the permission prompt is still open.
|
||||||
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument();
|
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 () => {
|
it("Go to Pilots opens the Pilots tab and the pop-up doesn't come back", async () => {
|
||||||
renderApp(fakeApi({ getPlayerState: () => afterDeal() }), "/");
|
renderApp(fakeApi({ getPlayerState: () => afterDeal() }), "/");
|
||||||
await userEvent.click(await screen.findByRole("button", { name: "Go to Pilots" }));
|
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();
|
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(() =>
|
await waitFor(() =>
|
||||||
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument(),
|
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();
|
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument();
|
||||||
|
|
||||||
act(() => usePreferences.getState().showTipsAgain());
|
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 () => {
|
it("doesn't appear with no unspent points", async () => {
|
||||||
renderApp(fakeApi({ getPlayerState: () => afterDeal(0, 0) }), "/");
|
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.
|
// Give the prompt check a turn to settle.
|
||||||
await act(async () => {});
|
await act(async () => {});
|
||||||
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument();
|
expect(screen.queryByRole("dialog", { name: POP_UP })).not.toBeInTheDocument();
|
||||||
|
|
|
||||||
|
|
@ -86,13 +86,13 @@ describe("empty states", () => {
|
||||||
|
|
||||||
it("HQ: no ships points to the Market", async () => {
|
it("HQ: no ships points to the Market", async () => {
|
||||||
show("/");
|
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();
|
expect(screen.getByText("Nothing in flight.")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Fleet: no ships links to the Market", async () => {
|
it("Fleet: no ships links to the Market", async () => {
|
||||||
show("/fleet");
|
show("/fleet");
|
||||||
expect(await screen.findByRole("link", { name: "visit the Market" })).toHaveAttribute(
|
expect(await screen.findByRole("link", { name: "visit the Shipyard" })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"/market",
|
"/market",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ describe("welcome cards (Step 22.2, decision 0.5.131)", () => {
|
||||||
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
||||||
expect(api.call).toHaveBeenCalledWith("markWelcomeSeen", {});
|
expect(api.call).toHaveBeenCalledWith("markWelcomeSeen", {});
|
||||||
expect(state().welcomeSeen).toBe(true);
|
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");
|
expect(await screen.findByLabelText("Intro")).toHaveTextContent("needs a pilot");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export const onboardingGoals: OnboardingGoal[] = [
|
||||||
{
|
{
|
||||||
// Was "Buy a second ship"; the deal now brings two (CR-013).
|
// Was "Buy a second ship"; the deal now brings two (CR-013).
|
||||||
id: "market-ship",
|
id: "market-ship",
|
||||||
title: "Buy a ship from the Market",
|
title: "Buy a ship from the Shipyard",
|
||||||
condition: { kind: "market-ships-bought", count: 1 },
|
condition: { kind: "market-ships-bought", count: 1 },
|
||||||
reward: 1000,
|
reward: 1000,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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)", () => {
|
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(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.
|
// After buying a Lighter: three ships, one of them from the Market.
|
||||||
expect(isComplete("market-ship", { ...fresh, shipsOwned: 3, marketShipsBought: 1 })).toBe(true);
|
expect(isComplete("market-ship", { ...fresh, shipsOwned: 3, marketShipsBought: 1 })).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue