Files
galaxy-game/ui/frontend/tests/game-shell-stubs.test.ts
T
Ilia Denisov e2aba856b5 ui/phase-27: viewer layout pass + static cluster + duel layout
Layout reshuffle so the scene captures the maximum viewer area:

- Header collapses three rows into one: `back to map` / `back to
  report` on the left, the centred title `Battle on planet <name>
  (#<number>)` (new i18n key `game.battle.header_title`), and the
  frame counter on the right. The wrapper `.active-view` no longer
  renders its own back-row; routes flow through props.
- Viewer drops the `max-width: 880px` cap so on a wide monitor the
  scene scales up across the full active-view-host.
- A drag-seek `<input type="range">` sits between the scene and the
  controls; dragging pauses playback and lands `frameIndex` on the
  chosen shot.
- Speed control is one cycling button: `1x → 2x → 4x → 6x → 1x`.
  The label shows the current speed; the new 6x adds a 67 ms frame
  interval for skimming a long timeline.
- The text protocol log is now collapsible behind a `Log ▲▼`
  toggle in the controls bar. The toggle is its own button; the
  default state stays expanded. Collapsing the log hands the
  remaining height to the scene.
- Numerical list markers (`1. 2. 3.`) are dropped from the log;
  `list-style: none` keeps each row visually clean.

Static cluster + visibility filter:

- `staticBucketsByRace` now locks bucket order, mass, radius and
  local Vogel-spiral positions for the lifetime of the viewer; it
  only re-derives when `report` or the wasm `core` change.
- `renderedByRace` overlays the per-frame `remaining` map and drops
  buckets whose `numLeft` hits zero. The surviving buckets keep
  their slots, so a class emptying never reshuffles the cluster —
  the empty bucket simply disappears.
- A shot whose attacker or defender bucket is no longer visible
  draws no line (phantom shots into already-empty buckets are
  silently skipped, matching the user expectation that pup at 0
  should stop attracting fire visually).
- Race label clamps to a minimum y inside the SVG viewport so
  three-or-more-race layouts with a north anchor never clip the
  top race name off-canvas.

Duel layout (user suggestion):

- `layoutRaces` rotates the radial start angle by 90° when only
  two participants remain, so race 0 lands at 9 o'clock and race 1
  at 3 o'clock. The pair faces off horizontally; neither label
  pushes against the SVG top edge. The existing test for two-race
  positions is updated accordingly.

Tests: the existing `layoutRaces` two-race case is rewritten for
the horizontal duel; the `game-shell-stubs` battle case checks the
loading placeholder (back buttons now live in the loaded viewer,
not the wrapper). 644 Vitest cases stay green; 4 Playwright
battle-viewer cases stay green.

Docs: `ui/docs/battle-viewer-ux.md` documents the static cluster /
visibility filter, the duel layout, the scrubber, the cycling
speed button and the collapsible log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:38:46 +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 shows the loading placeholder", () => {
// 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.
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-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();
});
});