Files
galaxy-game/ui/frontend/tests/game-shell-stubs.test.ts
T
Ilia Denisov 80ed11e3b6
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Successful in 2m45s
feat(ui): F8-10 — tables planets / ship-groups / fleets, ship-classes delete guard (#53)
Lights up three previously-stubbed table active views and tightens the
existing one:

  - table-planets: 4 kind checkboxes (own / foreign / uninhabited /
    unknown) + race dropdown that filters the foreign slice; row click
    selects + centres the planet on the map.
  - table-ship-groups: local + foreign groups in one grid, owner
    checkboxes, planet dropdown (destination OR origin), class
    dropdown; on-planet click focuses the destination planet, in-space
    click focuses the ship group itself (camera follows interpolated
    position).
  - table-fleets: own fleets only with the shared planet dropdown;
    on-planet click focuses the planet, in-space click centres the
    camera on the interpolated fleet position without altering the
    selection (no fleet variant in Selected).
  - table-ship-classes: per-row Delete is disabled with a count tooltip
    while at least one local ship group references the class. The
    engine refuses the removal anyway; the UI pre-empts the surface.

Wires the click → map flow through a transient `SelectionStore.focus`
/ `focusPoint` channel that `map.svelte` consumes once on mount —
in-memory only, so an F5 does not re-centre.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:35:38 +02:00

165 lines
6.7 KiB
TypeScript

// 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 unknown entities", () => {
// Every menu-known slug is wired to a real component by F8-10;
// the fallback branch still exists for defensive routing (e.g.
// a restored snapshot referencing a removed entity).
const ui = render(TableView, { props: { entity: "unknown-slug" } });
const node = ui.getByTestId("active-view-table");
expect(node).toHaveAttribute("data-entity", "unknown-slug");
expect(node).toHaveTextContent("coming soon");
});
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();
});
});