// Vitest coverage for the Phase 22 races table active view. The // component renders against a synthetic `RenderedReportSource` (no // live `GameStateStore`) and a real `OrderDraftStore` (so the per-row // stance toggle and the vote picker exercise the `add` path and the // IndexedDB persistence end-to-end). The render path also flows // through `applyOrderOverlay`, so the optimistic flips made by the // component must keep the test fixture's report intact: each test // passes the *raw* report and the helper recomputes the overlay on // every snapshot. import "@testing-library/jest-dom/vitest"; import "fake-indexeddb/auto"; import { fireEvent, render, waitFor } from "@testing-library/svelte"; import { afterEach, beforeEach, describe, expect, test, vi, } from "vitest"; import { i18n } from "../src/lib/i18n/index.svelte"; import { applyOrderOverlay, type GameReport, type ReportOtherRace, } from "../src/api/game-state"; import { ORDER_DRAFT_CONTEXT_KEY, OrderDraftStore, } from "../src/sync/order-draft.svelte"; import { RENDERED_REPORT_CONTEXT_KEY } from "../src/lib/rendered-report.svelte"; import { IDBCache } from "../src/platform/store/idb-cache"; import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb"; import type { Cache } from "../src/platform/store/index"; import type { IDBPDatabase } from "idb"; import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups"; const GAME_ID = "11111111-2222-3333-4444-555555555555"; const pageMock = vi.hoisted(() => ({ url: new URL("http://localhost/games/g1/table/races"), params: { id: "g1" } as Record, })); const gotoMock = vi.hoisted(() => vi.fn()); vi.mock("$app/state", () => ({ page: pageMock, })); vi.mock("$app/navigation", () => ({ goto: gotoMock, })); import TableRaces from "../src/lib/active-view/table-races.svelte"; let db: IDBPDatabase; let dbName: string; let cache: Cache; let draft: OrderDraftStore; beforeEach(async () => { dbName = `galaxy-table-races-${crypto.randomUUID()}`; db = await openGalaxyDB(dbName); cache = new IDBCache(db); draft = new OrderDraftStore(); await draft.init({ cache, gameId: GAME_ID }); i18n.resetForTests("en"); pageMock.params = { id: "g1" }; gotoMock.mockClear(); }); afterEach(async () => { draft.dispose(); db.close(); await new Promise((resolve) => { const req = indexedDB.deleteDatabase(dbName); req.onsuccess = () => resolve(); req.onerror = () => resolve(); req.onblocked = () => resolve(); }); }); function race( overrides: Partial & Pick, ): ReportOtherRace { return { drive: 0, weapons: 0, shields: 0, cargo: 0, population: 0, industry: 0, planets: 0, relation: "PEACE", votesReceived: 0, ...overrides, }; } function makeReport( races: ReportOtherRace[], opts: { myVotes?: number; myVoteFor?: string } = {}, ): GameReport { const baseEmpty = { ...EMPTY_SHIP_GROUPS, races, myVotes: opts.myVotes ?? 0, myVoteFor: opts.myVoteFor ?? "", }; return { turn: 1, mapWidth: 1000, mapHeight: 1000, planetCount: 0, planets: [], race: "Self", localShipClass: [], routes: [], localPlayerDrive: 0, localPlayerWeapons: 0, localPlayerShields: 0, localPlayerCargo: 0, ...baseEmpty, }; } function mountTable(report: GameReport | null) { const renderedReport = { get report() { if (report === null) return null; return applyOrderOverlay(report, draft.commands, draft.statuses); }, }; const context = new Map([ [ORDER_DRAFT_CONTEXT_KEY, draft], [RENDERED_REPORT_CONTEXT_KEY, renderedReport], ]); return render(TableRaces, { context }); } describe("races table", () => { test("renders a loading placeholder before the report lands", () => { const ui = mountTable(null); expect(ui.getByTestId("races-loading")).toBeInTheDocument(); }); test("renders an empty placeholder when no other races are known", () => { const ui = mountTable(makeReport([])); expect(ui.getByTestId("races-empty")).toBeInTheDocument(); // vote picker stays mounted but disabled expect(ui.getByTestId("races-vote-target")).toBeDisabled(); }); test("renders one row per race with all ten columns populated", () => { const ui = mountTable( makeReport([ race({ name: "Andori", drive: 0.25, weapons: 0.5, shields: 0.75, cargo: 1.0, population: 12345, industry: 6789, planets: 4, relation: "WAR", votesReceived: 3.5, }), ]), ); const rows = ui.getAllByTestId("races-row"); expect(rows).toHaveLength(1); expect(rows[0]).toHaveAttribute("data-name", "Andori"); expect(ui.getByTestId("races-cell-name")).toHaveTextContent("Andori"); expect(ui.getByTestId("races-cell-drive")).toHaveTextContent("25"); expect(ui.getByTestId("races-cell-weapons")).toHaveTextContent("50"); expect(ui.getByTestId("races-cell-shields")).toHaveTextContent("75"); expect(ui.getByTestId("races-cell-cargo")).toHaveTextContent("100"); expect(ui.getByTestId("races-cell-population")).toHaveTextContent( /12[,\s]345/, ); expect(ui.getByTestId("races-cell-industry")).toHaveTextContent( /6[,\s]?789/, ); expect(ui.getByTestId("races-cell-planets")).toHaveTextContent("4"); expect(ui.getByTestId("races-cell-votes")).toHaveTextContent("3.5"); }); test("filters rows by case-insensitive name match", async () => { const ui = mountTable( makeReport([ race({ name: "Alpha" }), race({ name: "Beta" }), race({ name: "Gamma" }), ]), ); await fireEvent.input(ui.getByTestId("races-filter"), { target: { value: "PH" }, }); const rows = ui.getAllByTestId("races-row"); expect(rows).toHaveLength(1); expect(rows[0]).toHaveAttribute("data-name", "Alpha"); }); test("toggles sort direction when the same column is clicked twice", async () => { const ui = mountTable( makeReport([ race({ name: "Alpha", votesReceived: 1 }), race({ name: "Beta", votesReceived: 5 }), race({ name: "Gamma", votesReceived: 3 }), ]), ); const header = ui.getByTestId("races-column-votesReceived"); await fireEvent.click(header); let names = ui .getAllByTestId("races-row") .map((row) => row.getAttribute("data-name")); expect(names).toEqual(["Alpha", "Gamma", "Beta"]); await fireEvent.click(header); names = ui .getAllByTestId("races-row") .map((row) => row.getAttribute("data-name")); expect(names).toEqual(["Beta", "Gamma", "Alpha"]); }); test("clicking PEACE on a WAR row appends setDiplomaticStance and flips the overlay", async () => { const ui = mountTable( makeReport([race({ name: "Andori", relation: "WAR" })]), ); const peaceButton = ui.getByTestId("races-stance-peace"); await fireEvent.click(peaceButton); await waitFor(() => expect(draft.commands).toHaveLength(1)); const cmd = draft.commands[0]!; if (cmd.kind !== "setDiplomaticStance") { throw new Error("wrong kind"); } expect(cmd.acceptor).toBe("Andori"); expect(cmd.relation).toBe("PEACE"); // After overlay the WAR button loses its `aria-pressed=true`. await waitFor(() => { expect(ui.getByTestId("races-stance-war")).toHaveAttribute( "aria-pressed", "false", ); expect(ui.getByTestId("races-stance-peace")).toHaveAttribute( "aria-pressed", "true", ); }); }); test("a second stance click for the same race collapses on acceptor", async () => { const ui = mountTable( makeReport([race({ name: "Andori", relation: "WAR" })]), ); await fireEvent.click(ui.getByTestId("races-stance-peace")); await waitFor(() => expect(draft.commands).toHaveLength(1)); const firstId = draft.commands[0]!.id; await fireEvent.click(ui.getByTestId("races-stance-war")); await waitFor(() => { expect(draft.commands).toHaveLength(1); }); const cmd = draft.commands[0]!; if (cmd.kind !== "setDiplomaticStance") { throw new Error("wrong kind"); } expect(cmd.id).not.toBe(firstId); expect(cmd.relation).toBe("WAR"); }); test("changing the vote picker appends setVoteRecipient", async () => { const ui = mountTable( makeReport( [race({ name: "Andori" }), race({ name: "Bajori" })], { myVoteFor: "Andori" }, ), ); await fireEvent.change(ui.getByTestId("races-vote-target"), { target: { value: "Bajori" }, }); await waitFor(() => expect(draft.commands).toHaveLength(1)); const cmd = draft.commands[0]!; if (cmd.kind !== "setVoteRecipient") { throw new Error("wrong kind"); } expect(cmd.acceptor).toBe("Bajori"); }); test("a second vote pick collapses singleton regardless of target", async () => { const ui = mountTable( makeReport( [ race({ name: "Andori" }), race({ name: "Bajori" }), race({ name: "Cardassian" }), ], { myVoteFor: "Andori" }, ), ); const select = ui.getByTestId("races-vote-target"); await fireEvent.change(select, { target: { value: "Bajori" } }); await waitFor(() => expect(draft.commands).toHaveLength(1)); await fireEvent.change(select, { target: { value: "Cardassian" } }); await waitFor(() => { expect(draft.commands).toHaveLength(1); }); const cmd = draft.commands[0]!; if (cmd.kind !== "setVoteRecipient") { throw new Error("wrong kind"); } expect(cmd.acceptor).toBe("Cardassian"); }); test("my votes summary reads from the report", () => { const ui = mountTable( makeReport([race({ name: "Andori" })], { myVotes: 7.5 }), ); expect(ui.getByTestId("races-my-votes")).toHaveTextContent("7.5"); }); });