ui/phase-10: in-game shell with view-replacement skeleton
Wraps every in-game route under `/games/:id/*` in a responsive shell
with a header (race / turn placeholders, view-menu dropdown or mobile
hamburger, account menu), a three-tab sidebar (Calculator, Inspector,
Order), an active-view slot, and a mobile-only bottom-tabs row
`[Map, Calc, Order, More]`. Every view in the IA section
(`map`, `table/:entity`, `report`, `battle/:battleId?`, `mail`,
`designer/{ship-class,science}/:id?`) ships as a thin SvelteKit route
that mounts a `lib/active-view/<name>.svelte` stub rendering a
localised `coming soon` body. The lobby's `gotoGame` path now actually
lands on a rendered shell instead of a 404.
The "view router" mentioned in the plan is implemented as the file
system plus two-line route wrappers — no separate dispatch component.
Sidebar tab state lives as a `$state` rune inside `sidebar.svelte`,
which sits in the layout that SvelteKit keeps mounted across child
route swaps, so tab choice survives every active-view navigation for
free. A `?sidebar=calc|inspector|order` URL param seeds the initial
tab on first mount; the mobile bottom-tabs use a layout-owned
`mobileTool` rune with a URL-gated `effectiveTool` derivation so the
Calc / Order tool overlay only applies on `/map` and naturally drops
when the user navigates elsewhere.
Tablet ships with a click-toggle drawer for the sidebar rather than
the IA section's swipe-from-right gesture; the structural breakpoint
satisfies Phase 10's acceptance criterion and Phase 35 polish lands
the swipe. The mobile More drawer mirrors the header view-menu
content; the IA's narrower More list (Mail, Battle, Tables, History,
Settings, Logout) is also a Phase 35 polish target once History
exists.
Topic doc `ui/docs/navigation.md` captures the active-view model, the
sidebar state-preservation rule, the `?sidebar=` and `mobileTool`
conventions, and the transient map-overlay back-stack concept (with
the implementation deferred to Phase 34 alongside its first user).
i18n catalogues for `en` and `ru` add the full `game.shell.*`,
`game.view.*`, `game.sidebar.*`, `game.bottom_tabs.*` namespaces.
Tests: Vitest covers the header view-menu (every IA destination
including the Tables sub-list), the account-menu Logout / Language
wiring, the sidebar default tab / switching / `?sidebar=` seed /
close button, and every active-view stub. Playwright e2e boots an
authenticated session via `__galaxyDebug.setDeviceSessionId` (no
gateway calls — the shell makes none in Phase 10), exercises every
view through both the desktop dropdown and the mobile More drawer,
verifies sidebar tab survival across navigation, and uses
`setViewportSize` to validate the breakpoint switches at 768 px and
1024 px.
Phase 10 status stays `pending` in `ui/PLAN.md` until the local-ci
run lands green; flipping to `done` follows in the next commit per
the per-stage CI gate in `CLAUDE.md`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// Component tests for the Phase 10 in-game shell sidebar. Validates
|
||||
// the default selected tab, the Calculator / Inspector / Order
|
||||
// switching, the empty-state copy that matches the IA section, and
|
||||
// the `?sidebar=` URL seed convention used by the mobile bottom-tabs.
|
||||
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { fireEvent, render } from "@testing-library/svelte";
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
test,
|
||||
vi,
|
||||
} from "vitest";
|
||||
|
||||
import { i18n } from "../src/lib/i18n/index.svelte";
|
||||
|
||||
const pageMock = vi.hoisted(() => ({
|
||||
url: new URL("http://localhost/games/g1/map"),
|
||||
params: { id: "g1" } as Record<string, string>,
|
||||
}));
|
||||
|
||||
vi.mock("$app/state", () => ({
|
||||
page: pageMock,
|
||||
}));
|
||||
|
||||
import Sidebar from "../src/lib/sidebar/sidebar.svelte";
|
||||
|
||||
beforeEach(() => {
|
||||
i18n.resetForTests("en");
|
||||
pageMock.url = new URL("http://localhost/games/g1/map");
|
||||
});
|
||||
|
||||
describe("game-shell sidebar", () => {
|
||||
test("renders the inspector tab content by default", () => {
|
||||
const ui = render(Sidebar, {
|
||||
props: { open: false, onClose: () => {} },
|
||||
});
|
||||
expect(ui.getByTestId("sidebar-tool-inspector")).toBeInTheDocument();
|
||||
expect(ui.getByTestId("sidebar-tool-inspector")).toHaveTextContent(
|
||||
"select an object on the map",
|
||||
);
|
||||
expect(ui.getByTestId("sidebar")).toHaveAttribute(
|
||||
"data-active-tab",
|
||||
"inspector",
|
||||
);
|
||||
});
|
||||
|
||||
test("switching tabs updates the rendered tool", async () => {
|
||||
const ui = render(Sidebar, {
|
||||
props: { open: false, onClose: () => {} },
|
||||
});
|
||||
await fireEvent.click(ui.getByTestId("sidebar-tab-calculator"));
|
||||
expect(ui.getByTestId("sidebar-tool-calculator")).toBeInTheDocument();
|
||||
expect(ui.queryByTestId("sidebar-tool-inspector")).toBeNull();
|
||||
expect(ui.queryByTestId("sidebar-tool-order")).toBeNull();
|
||||
|
||||
await fireEvent.click(ui.getByTestId("sidebar-tab-order"));
|
||||
expect(ui.getByTestId("sidebar-tool-order")).toBeInTheDocument();
|
||||
expect(ui.queryByTestId("sidebar-tool-calculator")).toBeNull();
|
||||
});
|
||||
|
||||
test("empty-state copy matches the IA section verbatim", () => {
|
||||
const ui = render(Sidebar, {
|
||||
props: { open: false, onClose: () => {} },
|
||||
});
|
||||
expect(ui.getByTestId("sidebar-tool-inspector")).toHaveTextContent(
|
||||
"select an object on the map",
|
||||
);
|
||||
});
|
||||
|
||||
test("?sidebar=calc seeds the calculator tab on first mount", () => {
|
||||
pageMock.url = new URL("http://localhost/games/g1/map?sidebar=calc");
|
||||
const ui = render(Sidebar, {
|
||||
props: { open: false, onClose: () => {} },
|
||||
});
|
||||
expect(ui.getByTestId("sidebar-tool-calculator")).toBeInTheDocument();
|
||||
expect(ui.getByTestId("sidebar")).toHaveAttribute(
|
||||
"data-active-tab",
|
||||
"calculator",
|
||||
);
|
||||
});
|
||||
|
||||
test("?sidebar=order seeds the order tab on first mount", () => {
|
||||
pageMock.url = new URL("http://localhost/games/g1/map?sidebar=order");
|
||||
const ui = render(Sidebar, {
|
||||
props: { open: false, onClose: () => {} },
|
||||
});
|
||||
expect(ui.getByTestId("sidebar-tool-order")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("close button calls the onClose prop", async () => {
|
||||
const onClose = vi.fn();
|
||||
const ui = render(Sidebar, { props: { open: true, onClose } });
|
||||
await fireEvent.click(ui.getByTestId("sidebar-close"));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user