// Vitest coverage for the F8-10 ship-groups table active view. // Mounts the component against a synthetic `RenderedReportSource` // and a real `SelectionStore`; the click → focus contract (planet // for on-orbit groups, ship-group ref for in-space) is exercised // directly through the store. 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, ReportLocalShipGroup, ReportOtherShipGroup, 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 { resetShipGroupsTableState } from "../src/lib/active-view/table-ship-groups-state.svelte"; import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups"; const pageMock = vi.hoisted(() => ({ url: new URL("http://localhost/games/g1/table/ship-groups"), 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 TableShipGroups from "../src/lib/active-view/table-ship-groups.svelte"; let selection: SelectionStore; beforeEach(() => { selection = new SelectionStore(); i18n.resetForTests("en"); pageMock.params = { id: "g1" }; gotoMock.mockClear(); activeView.reset(); resetShipGroupsTableState(); }); afterEach(() => { selection.dispose(); }); function planet(num: number, name?: string): ReportPlanet { return { number: num, name: name ?? `P${num}`, 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 localGroup( overrides: Partial & Pick, ): ReportLocalShipGroup { return { state: "In_Orbit", fleet: null, count: 1, tech: { drive: 0, weapons: 0, shields: 0, cargo: 0 }, cargo: "NONE", load: 0, origin: null, range: null, speed: 0, mass: 0, race: "Me", ...overrides, }; } function otherGroup( overrides: Partial & Pick, ): ReportOtherShipGroup { return { count: 1, tech: { drive: 0, weapons: 0, shields: 0, cargo: 0 }, cargo: "NONE", load: 0, origin: null, range: null, speed: 0, mass: 0, ...overrides, }; } function makeReport(opts: { planets?: ReportPlanet[]; local?: ReportLocalShipGroup[]; other?: ReportOtherShipGroup[]; }): GameReport { return { 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, localShipGroups: opts.local ?? [], otherShipGroups: opts.other ?? [], }; } 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(TableShipGroups, { context }); } describe("ship-groups table", () => { test("renders a loading placeholder before the report lands", () => { const ui = mount(null); expect(ui.getByTestId("ship-groups-loading")).toBeInTheDocument(); }); test("renders an empty placeholder when no groups are present", () => { const ui = mount(makeReport({ planets: [planet(1)] })); expect(ui.getByTestId("ship-groups-empty")).toBeInTheDocument(); }); test("renders local and foreign rows under one table", () => { const ui = mount( makeReport({ planets: [planet(1), planet(2)], local: [localGroup({ id: "L1", class: "Cruiser", destination: 1 })], other: [ otherGroup({ class: "Hunter", destination: 2, race: "Klingon" }), ], }), ); const rows = ui.getAllByTestId("ship-groups-row"); expect(rows).toHaveLength(2); const owners = rows.map((r) => r.getAttribute("data-owner")); expect(owners.sort()).toEqual(["foreign", "own"]); }); test("owner checkboxes filter independently", async () => { const ui = mount( makeReport({ planets: [planet(1), planet(2)], local: [localGroup({ id: "L1", class: "Cruiser", destination: 1 })], other: [ otherGroup({ class: "Hunter", destination: 2, race: "Klingon" }), ], }), ); await fireEvent.click(ui.getByTestId("ship-groups-filter-foreign")); const owners = ui .getAllByTestId("ship-groups-row") .map((r) => r.getAttribute("data-owner")); expect(owners).toEqual(["own"]); }); test("planet dropdown filters by destination OR origin", async () => { const ui = mount( makeReport({ planets: [planet(1), planet(2), planet(3)], local: [ localGroup({ id: "L1", class: "C", destination: 1 }), localGroup({ id: "L2", class: "C", destination: 3, origin: 2, range: 4, state: "In_Space", }), localGroup({ id: "L3", class: "C", destination: 3 }), ], }), ); const sel = ui.getByTestId( "ship-groups-filter-planet", ) as HTMLSelectElement; await fireEvent.change(sel, { target: { value: "2" } }); // only L2 touches planet 2 (origin === 2) const keys = ui .getAllByTestId("ship-groups-row") .map((r) => r.getAttribute("data-key")); expect(keys).toEqual(["local:L2"]); }); test("class dropdown filters by class name", async () => { const ui = mount( makeReport({ planets: [planet(1)], local: [ localGroup({ id: "L1", class: "Cruiser", destination: 1 }), localGroup({ id: "L2", class: "Drone", destination: 1 }), ], }), ); const sel = ui.getByTestId( "ship-groups-filter-class", ) as HTMLSelectElement; await fireEvent.change(sel, { target: { value: "Drone" } }); const keys = ui .getAllByTestId("ship-groups-row") .map((r) => r.getAttribute("data-key")); expect(keys).toEqual(["local:L2"]); }); test("click on on-planet group focuses the destination planet", async () => { const ui = mount( makeReport({ planets: [planet(7)], local: [localGroup({ id: "L1", class: "Cruiser", destination: 7 })], }), ); await fireEvent.click(ui.getByTestId("ship-groups-row")); expect(selection.selected).toEqual({ kind: "planet", id: 7 }); expect(selection.consumePendingFocus()).toEqual({ kind: "planet", id: 7, }); expect(activeView.view).toBe("map"); }); test("click on in-space local group focuses the ship-group ref", async () => { const ui = mount( makeReport({ planets: [planet(1), planet(2)], local: [ localGroup({ id: "L1", class: "Cruiser", destination: 2, origin: 1, range: 3, state: "In_Space", }), ], }), ); await fireEvent.click(ui.getByTestId("ship-groups-row")); expect(selection.selected).toEqual({ kind: "shipGroup", ref: { variant: "local", id: "L1" }, }); expect(selection.consumePendingFocus()).toEqual({ kind: "shipGroup", ref: { variant: "local", id: "L1" }, }); expect(activeView.view).toBe("map"); }); test("planet/class dropdowns narrow to the owner-checkbox cut", async () => { const ui = mount( makeReport({ planets: [planet(1), planet(2), planet(3)], local: [localGroup({ id: "L1", class: "Cruiser", destination: 1 })], other: [ otherGroup({ class: "Hunter", destination: 3, race: "Klingon" }), ], }), ); // All four planet options available when both owner kinds are on const planetSelect = ui.getByTestId( "ship-groups-filter-planet", ) as HTMLSelectElement; const valuesFull = Array.from(planetSelect.options).map((o) => o.value); expect(valuesFull).toContain("1"); expect(valuesFull).toContain("3"); // Hide foreign rows; planet 3 (only touched by Klingon) disappears await fireEvent.click(ui.getByTestId("ship-groups-filter-foreign")); const valuesNarrowed = Array.from(planetSelect.options).map((o) => o.value); expect(valuesNarrowed).toContain("1"); expect(valuesNarrowed).not.toContain("3"); // Class dropdown narrows too: Hunter disappears const classSelect = ui.getByTestId( "ship-groups-filter-class", ) as HTMLSelectElement; const classValues = Array.from(classSelect.options).map((o) => o.value); expect(classValues).not.toContain("Hunter"); expect(classValues).toContain("Cruiser"); }); test("click on in-space foreign group focuses other variant by index", async () => { const ui = mount( makeReport({ planets: [planet(1), planet(2)], other: [ otherGroup({ class: "Hunter", destination: 2, origin: 1, range: 5, race: "Klingon", }), ], }), ); await fireEvent.click(ui.getByTestId("ship-groups-row")); expect(selection.selected).toEqual({ kind: "shipGroup", ref: { variant: "other", index: 0 }, }); expect(activeView.view).toBe("map"); }); });