Files
galaxy-game/ui/frontend/tests/game-shell-stubs.test.ts
T
Ilia Denisov 969c0480ba ui/phase-27: battle viewer (radial scene, playback, map markers)
Engine wire change: Report.battle switched from []uuid.UUID to
[]BattleSummary{id, planet, shots} so the map can place battle
markers without N extra fetches. FBS schema + generated Go/TS
regenerated; transcoder + report controller updated; openapi
adds the BattleSummary schema with a freeze test.

Backend gateway forwards engine GET /api/v1/battle/:turn/:uuid as
/api/v1/user/games/{game_id}/battles/{turn}/{battle_id} (handler
plus engineclient.FetchBattle, contract test stub, openapi spec).

UI:
- BattleViewer (lib/battle-player/) is a logically isolated SVG
  radial scene that consumes a BattleReport prop. Planet at the
  centre, races on the outer ring at equal angular spacing, race
  clusters by (race, className) with <class>:<numLeft> labels;
  observer groups (inBattle: false) are not drawn; eliminated
  races drop out and survivors re-distribute on the next frame.
- Shot line per frame: red on destroyed, green otherwise; erased
  on the next frame. Playback controls: play/pause + step ± +
  rewind + 1x/2x/4x speed (400/200/100 ms per frame).
- Page wrapper (lib/active-view/battle.svelte) loads BattleReport
  via api/battle-fetch.ts; synthetic-gameId prefix routes to a
  fixture loader, otherwise REST through the gateway. Always-
  visible <ol> text protocol satisfies the accessibility ask.
- section-battles.svelte links every battle UUID into the viewer.
- map/battle-markers.ts: yellow X cross of 2 LinePrim through the
  corners of the planet's circumscribed square (stroke width
  clamps from 1 px at 1 shot to 5 px at 100+ shots); bombing
  marker is a stroke-only ring (yellow when damaged, red when
  wiped). Wired into state-binding.ts; click handler dispatches
  battle clicks to the viewer and bombing clicks to the matching
  Reports row.
- i18n keys for the viewer in en + ru.

Docs: ui/docs/battle-viewer-ux.md, FUNCTIONAL.md §6.5 + ru
mirror, ui/PLAN.md Phase 27 decisions + deferred TODOs (push
event, richer class visuals, animated re-distribution).

Tests: Vitest unit (radial layout + timeline frame builder +
marker stroke formula + marker primitives), Playwright e2e for
the viewer (Reports link → viewer, playback step, not-found),
backend engineclient FetchBattle (200 / 404 / bad input), engine
openapi freezes (BattleReport, BattleReportGroup,
BattleActionReport, BattleSummary, Report.battle items).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:24:20 +02:00

106 lines
4.6 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 and the ship-class designer; Phase 21 lit up
// the sciences table and the science designer. Their assertions
// moved to dedicated suites (`table-ship-classes.test.ts`,
// `designer-ship-class.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 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");
});
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 TOC and the back-to-map link", () => {
// 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`.
const r = render(ReportView);
expect(r.getByTestId("active-view-report")).toBeInTheDocument();
expect(r.getByTestId("report-toc")).toBeInTheDocument();
expect(r.getByTestId("report-back-to-map")).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 renders the back-to-map link", () => {
// Phase 27 replaces the Phase 10 stub with the Battle Viewer
// wrapper. The wrapper mounts the loading copy until the
// fetcher resolves (component test runs in jsdom without a
// network); the back buttons and the data-battle-id stamp are
// rendered unconditionally so the orchestrator scaffold is the
// stable hook the active-view shell relies on.
const ui = render(BattleView, {
props: { gameId: "synthetic-test", turn: 0, battleId: "b-42" },
});
const node = ui.getByTestId("active-view-battle");
expect(node).toHaveAttribute("data-battle-id", "b-42");
expect(ui.getByTestId("battle-back-to-map")).toBeInTheDocument();
expect(ui.getByTestId("battle-back-to-report")).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();
});
});