// Vitest coverage for the F8-10 planets table active view. // The component is mounted against a synthetic `RenderedReportSource` // (kind discriminants set per case) and a real `SelectionStore`, so // the click → focus contract is exercised end-to-end without needing // the live map renderer. 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, ReportPlanet } from "../src/api/game-state"; import { RENDERED_REPORT_CONTEXT_KEY } from "../src/lib/rendered-report.svelte"; import { SELECTION_CONTEXT_KEY, SelectionStore, } from "../src/lib/selection.svelte"; import { activeView } from "../src/lib/app-nav.svelte"; import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups"; const pageMock = vi.hoisted(() => ({ url: new URL("http://localhost/games/g1/table/planets"), 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 TablePlanets from "../src/lib/active-view/table-planets.svelte"; let selection: SelectionStore; beforeEach(() => { selection = new SelectionStore(); i18n.resetForTests("en"); pageMock.params = { id: "g1" }; gotoMock.mockClear(); activeView.reset(); }); afterEach(() => { selection.dispose(); }); function planet(overrides: Partial & { number: number }): ReportPlanet { return { name: `P${overrides.number}`, x: 0, y: 0, kind: "uninhabited", owner: null, size: null, resources: null, industryStockpile: null, materialsStockpile: null, industry: null, population: null, colonists: null, production: null, freeIndustry: null, ...overrides, }; } function makeReport(planets: ReportPlanet[]): GameReport { return { turn: 1, mapWidth: 1000, mapHeight: 1000, planetCount: planets.length, planets, race: "Me", localShipClass: [], routes: [], localPlayerDrive: 0, localPlayerWeapons: 0, localPlayerShields: 0, localPlayerCargo: 0, ...EMPTY_SHIP_GROUPS, }; } function mount(report: GameReport | null) { const renderedReport = { get report() { return report; }, }; const context = new Map([ [RENDERED_REPORT_CONTEXT_KEY, renderedReport], [SELECTION_CONTEXT_KEY, selection], ]); return render(TablePlanets, { context }); } describe("planets table", () => { test("renders a loading placeholder before the report lands", () => { const ui = mount(null); expect(ui.getByTestId("planets-loading")).toBeInTheDocument(); }); test("renders an empty placeholder when the report has no planets", () => { const ui = mount(makeReport([])); expect(ui.getByTestId("planets-empty")).toBeInTheDocument(); }); test("renders one row per planet with kind classification", () => { const ui = mount( makeReport([ planet({ number: 1, name: "Earth", kind: "local" }), planet({ number: 2, name: "Vega", kind: "other", owner: "Klingon", }), planet({ number: 3, name: "Rock", kind: "uninhabited" }), planet({ number: 4, name: "", kind: "unidentified" }), ]), ); const rows = ui.getAllByTestId("planets-row"); expect(rows).toHaveLength(4); expect(rows[0]).toHaveAttribute("data-kind", "local"); expect(rows[1]).toHaveAttribute("data-kind", "other"); }); test("kind checkboxes filter the visible rows independently", async () => { const ui = mount( makeReport([ planet({ number: 1, kind: "local" }), planet({ number: 2, kind: "other", owner: "A" }), planet({ number: 3, kind: "uninhabited" }), planet({ number: 4, kind: "unidentified" }), ]), ); await fireEvent.click(ui.getByTestId("planets-filter-own")); let kinds = ui .getAllByTestId("planets-row") .map((r) => r.getAttribute("data-kind")); expect(kinds).toEqual(["other", "uninhabited", "unidentified"]); await fireEvent.click(ui.getByTestId("planets-filter-uninhabited")); kinds = ui .getAllByTestId("planets-row") .map((r) => r.getAttribute("data-kind")); expect(kinds).toEqual(["other", "unidentified"]); }); test("owner dropdown narrows the foreign slice only", async () => { const ui = mount( makeReport([ planet({ number: 1, kind: "local" }), planet({ number: 2, kind: "other", owner: "Klingon" }), planet({ number: 3, kind: "other", owner: "Romulan" }), planet({ number: 4, kind: "uninhabited" }), ]), ); const select = ui.getByTestId( "planets-filter-owner", ) as HTMLSelectElement; await fireEvent.change(select, { target: { value: "Klingon" } }); const numbers = ui .getAllByTestId("planets-row") .map((r) => r.getAttribute("data-number")); // own (1) + foreign Klingon (2) + uninhabited (4); Romulan dropped expect(numbers).toEqual(["1", "2", "4"]); }); test("clicking a row focuses the planet and switches the active view", async () => { const ui = mount( makeReport([ planet({ number: 7, kind: "local", name: "Earth", x: 12, y: 34 }), ]), ); await fireEvent.click(ui.getByTestId("planets-row")); expect(selection.selected).toEqual({ kind: "planet", id: 7 }); expect(selection.consumePendingFocus()).toEqual({ kind: "planet", id: 7, }); expect(activeView.view).toBe("map"); }); test("number column sorts ascending then descending", async () => { const ui = mount( makeReport([ planet({ number: 3, kind: "local" }), planet({ number: 1, kind: "local" }), planet({ number: 2, kind: "local" }), ]), ); const numbers = ui .getAllByTestId("planets-row") .map((r) => r.getAttribute("data-number")); expect(numbers).toEqual(["1", "2", "3"]); await fireEvent.click(ui.getByTestId("planets-column-number")); const reversed = ui .getAllByTestId("planets-row") .map((r) => r.getAttribute("data-number")); expect(reversed).toEqual(["3", "2", "1"]); }); });