// Vitest coverage for the F8-11 battles table active view. The // component renders against a synthetic `RenderedReportSource` (no // live `GameStateStore`); no draft / selection plumbing is needed // because the row click only mutates `activeView` (no overlay or // selection state). The module-level rune `battlesTableState` is // reset in `beforeEach` so cases stay independent. import "@testing-library/jest-dom/vitest"; import { fireEvent, render } from "@testing-library/svelte"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { i18n } from "../src/lib/i18n/index.svelte"; import type { GameReport, ReportBattle, ReportPlanet, } from "../src/api/game-state"; import { RENDERED_REPORT_CONTEXT_KEY } from "../src/lib/rendered-report.svelte"; import { activeView } from "../src/lib/app-nav.svelte"; import { resetBattlesTableState } from "../src/lib/active-view/table-battles-state.svelte"; import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups"; const pageMock = vi.hoisted(() => ({ url: new URL("http://localhost/games/g1/table/battles"), params: { id: "g1" } as Record, })); const gotoMock = vi.hoisted(() => vi.fn()); vi.mock("$app/state", () => ({ page: pageMock, })); vi.mock("$app/navigation", () => ({ goto: gotoMock, pushState: vi.fn(), replaceState: vi.fn(), })); import TableBattles from "../src/lib/active-view/table-battles.svelte"; beforeEach(() => { i18n.resetForTests("en"); pageMock.params = { id: "g1" }; gotoMock.mockClear(); activeView.reset(); resetBattlesTableState(); }); afterEach(() => { activeView.reset(); }); function planet(num: number, name = `P${num}`): ReportPlanet { return { number: num, name, x: 0, y: 0, kind: "local", owner: null, size: null, resources: null, industryStockpile: null, materialsStockpile: null, industry: null, population: null, colonists: null, production: null, freeIndustry: null, }; } function battle( overrides: Partial & Pick, ): ReportBattle { return { planet: 0, shots: 0, ...overrides, }; } function makeReport(opts: { turn?: number; planets?: ReportPlanet[]; battles?: ReportBattle[]; }): GameReport { const battles = opts.battles ?? []; return { turn: opts.turn ?? 1, mapWidth: 1000, mapHeight: 1000, planetCount: opts.planets?.length ?? 0, planets: opts.planets ?? [], race: "Me", localShipClass: [], routes: [], localPlayerDrive: 0, localPlayerWeapons: 0, localPlayerShields: 0, localPlayerCargo: 0, ...EMPTY_SHIP_GROUPS, battles, battleIds: battles.map((b) => b.id), }; } function mount(report: GameReport | null) { const renderedReport = { get report() { return report; }, }; const context = new Map([ [RENDERED_REPORT_CONTEXT_KEY, renderedReport], ]); return render(TableBattles, { context }); } describe("battles table", () => { test("renders a loading placeholder before the report lands", () => { const ui = mount(null); expect(ui.getByTestId("battles-loading")).toBeInTheDocument(); }); test("renders an empty placeholder when no battles exist", () => { const ui = mount(makeReport({ planets: [planet(1)] })); expect(ui.getByTestId("battles-empty")).toBeInTheDocument(); }); test("renders one row per battle with planet label and shots", () => { const ui = mount( makeReport({ planets: [planet(7, "Avalon"), planet(11, "")], battles: [ battle({ id: "b1", planet: 7, shots: 3 }), battle({ id: "b2", planet: 11, shots: 25 }), battle({ id: "b3", planet: 99, shots: 1 }), ], }), ); const rows = ui.getAllByTestId("battles-row"); expect(rows).toHaveLength(3); const labelById = new Map(); const shotsById = new Map(); for (const row of rows) { const id = row.getAttribute("data-id")!; labelById.set( id, row.querySelector("[data-testid='battles-cell-planet']")! .textContent!.trim(), ); shotsById.set( id, row.querySelector("[data-testid='battles-cell-shots']")! .textContent!.trim(), ); } // Known planet with a name → "#N (name)"; known planet with // empty name and unknown planet → "#N". expect(labelById.get("b1")).toBe("#7 (Avalon)"); expect(labelById.get("b2")).toBe("#11"); expect(labelById.get("b3")).toBe("#99"); expect(shotsById.get("b1")).toBe("3"); expect(shotsById.get("b2")).toBe("25"); expect(shotsById.get("b3")).toBe("1"); }); test("default sort is shots descending", () => { const ui = mount( makeReport({ planets: [planet(1), planet(2), planet(3)], battles: [ battle({ id: "b1", planet: 1, shots: 5 }), battle({ id: "b2", planet: 2, shots: 99 }), battle({ id: "b3", planet: 3, shots: 12 }), ], }), ); const ids = ui .getAllByTestId("battles-row") .map((r) => r.getAttribute("data-id")); expect(ids).toEqual(["b2", "b3", "b1"]); }); test("clicking the active shots header flips the direction", async () => { const ui = mount( makeReport({ planets: [planet(1), planet(2)], battles: [ battle({ id: "b1", planet: 1, shots: 5 }), battle({ id: "b2", planet: 2, shots: 99 }), ], }), ); await fireEvent.click(ui.getByTestId("battles-column-shots")); const ids = ui .getAllByTestId("battles-row") .map((r) => r.getAttribute("data-id")); expect(ids).toEqual(["b1", "b2"]); }); test("clicking the planet header sorts ascending by planet number", async () => { const ui = mount( makeReport({ planets: [planet(1), planet(2), planet(3)], battles: [ battle({ id: "b1", planet: 3, shots: 5 }), battle({ id: "b2", planet: 1, shots: 99 }), battle({ id: "b3", planet: 2, shots: 12 }), ], }), ); await fireEvent.click(ui.getByTestId("battles-column-planet")); const ids = ui .getAllByTestId("battles-row") .map((r) => r.getAttribute("data-id")); expect(ids).toEqual(["b2", "b3", "b1"]); }); test("clicking a row opens the battle viewer with the matching id and turn", async () => { const ui = mount( makeReport({ turn: 42, planets: [planet(1)], battles: [battle({ id: "battle-uuid", planet: 1, shots: 4 })], }), ); await fireEvent.click(ui.getByTestId("battles-row")); expect(activeView.state).toMatchObject({ view: "battle", battleId: "battle-uuid", turn: 42, }); }); });