// Component tests for the remaining Phase 10 active-view stubs. Each // stub renders the localised view title plus the `coming soon` body // copy and exposes a stable `data-testid` so later phases can replace // the content without renaming the test hook. Phase 17 lit up the // ship-classes table (Phase 30 folded the designer into the sidebar // calculator); Phase 21 lit up the sciences table and the science // designer. Their assertions moved to dedicated suites // (`table-ship-classes.test.ts`, `calculator-tab.test.ts`, // `table-sciences.test.ts`, `designer-science.test.ts`); the // `table.svelte` router still falls // back to the stub for the remaining entities (planets, ship-groups, // fleets, races) and that fallback is exercised here. import "@testing-library/jest-dom/vitest"; import { render } from "@testing-library/svelte"; import { beforeEach, describe, expect, test } from "vitest"; import { i18n } from "../src/lib/i18n/index.svelte"; import { registerSyntheticBattle, resetSyntheticBattles, } from "../src/api/synthetic-battle"; import type { BattleReport } from "../src/api/battle-fetch"; import MapView from "../src/lib/active-view/map.svelte"; import TableView from "../src/lib/active-view/table.svelte"; import ReportView from "../src/lib/active-view/report.svelte"; import BattleView from "../src/lib/active-view/battle.svelte"; import MailView from "../src/lib/active-view/mail.svelte"; beforeEach(() => { i18n.resetForTests("en"); resetSyntheticBattles(); }); describe("active-view stubs", () => { test("map view renders loading overlay when no game-state context is provided", () => { // The live integration in `lib/active-view/map.svelte` (Phase 11) // reads its data from a `GameStateStore` provided through context // by `routes/games/[id]/+layout.svelte`. Without the context the // store reference is `undefined` and the view stays in the // `idle` branch, surfacing the localised loading overlay so the // shell never renders an empty active-view slot. const ui = render(MapView); const node = ui.getByTestId("active-view-map"); expect(node).toHaveAttribute("data-status", "idle"); expect(ui.getByTestId("map-loading")).toBeInTheDocument(); expect(ui.getByTestId("map-canvas-wrap")).toBeInTheDocument(); }); test("table stub falls back for not-yet-implemented entities", () => { const ui = render(TableView, { props: { entity: "planets" } }); const node = ui.getByTestId("active-view-table"); expect(node).toHaveAttribute("data-entity", "planets"); expect(node).toHaveTextContent("planets"); expect(node).toHaveTextContent("coming soon"); }); test("table stub also handles multi-word entities", () => { const ui = render(TableView, { props: { entity: "ship-groups" } }); const node = ui.getByTestId("active-view-table"); expect(node).toHaveAttribute("data-entity", "ship-groups"); expect(node).toHaveTextContent("ship groups"); }); test("report view mounts with the icon-popup TOC", () => { // Phase 23 replaces the Phase 10 stub with the full report // orchestrator. The orchestrator mounts the table of contents // regardless of report state; the inner sections render // loading copy until a `RenderedReportSource` lands via // context. This test only smokes the orchestrator scaffold — // per-section assertions live in `report-section-*.test.ts`. // F8-09 collapsed the TOC into a single sticky icon-popup // trigger; "Back to map" lives in the app-shell view menu. const r = render(ReportView); expect(r.getByTestId("active-view-report")).toBeInTheDocument(); expect(r.getByTestId("report-toc")).toBeInTheDocument(); expect(r.getByTestId("report-toc-trigger")).toBeInTheDocument(); }); test("mail stub renders its localised title", () => { const m = render(MailView); expect(m.getByTestId("active-view-mail")).toHaveTextContent( "diplomatic mail", ); }); test("battle view stamps the battleId and shows the loading placeholder for a live game", () => { // Phase 27 replaces the Phase 10 stub with the Battle Viewer // wrapper. The latest layout iteration moved the back- // navigation buttons inside `BattleViewer` so they only mount // once the BattleReport finishes loading. The wrapper itself // always renders the `active-view-battle` host with the // `data-battle-id` stamp and a localized loading copy until // the fetcher resolves. For a live game id the wrapper also // waits for the surrounding layout to publish a `GalaxyClient` // before issuing the fetch — without the context here the // effect stays in `loading` as designed. const ui = render(BattleView, { props: { gameId: "00000000-0000-0000-0000-000000000010", turn: 0, battleId: "b-42", }, }); const node = ui.getByTestId("active-view-battle"); expect(node).toHaveAttribute("data-battle-id", "b-42"); expect(ui.getByTestId("battle-loading")).toBeInTheDocument(); }); test("battle view surfaces the not-found state for an empty battleId", () => { const ui = render(BattleView, { props: { gameId: "synthetic-test", turn: 0, battleId: "" }, }); expect(ui.getByTestId("active-view-battle")).toHaveAttribute( "data-battle-id", "", ); expect(ui.getByTestId("battle-not-found")).toBeInTheDocument(); }); test("battle view surfaces not-found for a synthetic game when no fixture is registered", async () => { // Synthetic games never publish a GalaxyClient — the in-game // shell layout deliberately skips `galaxyClient.set(...)` on // that branch. The viewer must resolve the fixture (or its // absence) without waiting on the client handle; if it did // wait, the view would sit on `loading` indefinitely because // the handle never lands. `fetchBattle` itself is `async`, so // even the synchronous fixture-miss path resolves on a // microtask — `findByTestId` lets the BattleViewer wrapper // flush its rejection handler before the assertion. const ui = render(BattleView, { props: { gameId: "synthetic-test", turn: 0, battleId: "missing-fixture", }, }); await ui.findByTestId("battle-not-found"); }); test("battle view renders a synthetic fixture without a GalaxyClient context", async () => { // Regression for the dev-deploy bug where the viewer // short-circuited to `loading` for every cross click on a // synthetic-report game. The fixture below mirrors the shape // `fetchBattle` returns for the live path; once // `lookupSyntheticBattle` resolves it, the wrapper transitions // to `ready` and mounts the BattleViewer scene. const fixture: BattleReport = { id: "fixture-battle", planet: 1, planetName: "Earth", races: { "0": "00000000-0000-0000-0000-0000000000aa" }, ships: {}, protocol: [], }; registerSyntheticBattle(fixture); const ui = render(BattleView, { props: { gameId: "synthetic-test", turn: 0, battleId: "fixture-battle", }, }); await ui.findByTestId("battle-viewer"); expect(ui.queryByTestId("battle-loading")).not.toBeInTheDocument(); }); });