// Vitest coverage for the Phase 21 sciences table active view. // The component renders against a synthetic `RenderedReportSource` // (so the suite does not need a live `GameStateStore`) and a real // `OrderDraftStore` (so the per-row Delete affordance exercises // the `removeScience` add path including persistence). 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 type { GameReport, ScienceSummary } 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/sciences"), params: { id: "g1" } as Record, })); const gotoMock = vi.hoisted(() => vi.fn()); vi.mock("$app/state", () => ({ page: pageMock, })); vi.mock("$app/navigation", () => ({ goto: gotoMock, })); import TableSciences from "../src/lib/active-view/table-sciences.svelte"; let db: IDBPDatabase; let dbName: string; let cache: Cache; let draft: OrderDraftStore; beforeEach(async () => { dbName = `galaxy-table-sciences-${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 science( overrides: Partial & Pick, ): ScienceSummary { return { drive: 0, weapons: 0, shields: 0, cargo: 0, ...overrides, }; } function makeReport(localScience: ScienceSummary[]): GameReport { const baseEmpty = { ...EMPTY_SHIP_GROUPS, localScience }; return { turn: 1, mapWidth: 1000, mapHeight: 1000, planetCount: 0, planets: [], race: "", localShipClass: [], routes: [], localPlayerDrive: 0, localPlayerWeapons: 0, localPlayerShields: 0, localPlayerCargo: 0, ...baseEmpty, }; } function mountTable(report: GameReport | null) { const renderedReport = { get report() { return report; }, }; const context = new Map([ [ORDER_DRAFT_CONTEXT_KEY, draft], [RENDERED_REPORT_CONTEXT_KEY, renderedReport], ]); return render(TableSciences, { context }); } describe("sciences table", () => { test("renders a loading placeholder before the report lands", () => { const ui = mountTable(null); expect(ui.getByTestId("sciences-loading")).toBeInTheDocument(); }); test("renders an empty placeholder when no sciences are defined", () => { const ui = mountTable(makeReport([])); expect(ui.getByTestId("sciences-empty")).toBeInTheDocument(); }); test("renders one row per science with percent-formatted attributes", () => { const ui = mountTable( makeReport([ science({ name: "FirstStep", drive: 0.222, weapons: 0.111, shields: 0.667, cargo: 0, }), ]), ); const rows = ui.getAllByTestId("sciences-row"); expect(rows).toHaveLength(1); expect(rows[0]).toHaveAttribute("data-name", "FirstStep"); expect(ui.getByTestId("sciences-cell-drive")).toHaveTextContent("22.2"); expect(ui.getByTestId("sciences-cell-weapons")).toHaveTextContent("11.1"); expect(ui.getByTestId("sciences-cell-shields")).toHaveTextContent("66.7"); expect(ui.getByTestId("sciences-cell-cargo")).toHaveTextContent("0"); }); test("filters rows by case-insensitive name match", async () => { const ui = mountTable( makeReport([ science({ name: "Alpha", drive: 1 }), science({ name: "Beta", drive: 1 }), science({ name: "Gamma", drive: 1 }), ]), ); await fireEvent.input(ui.getByTestId("sciences-filter"), { target: { value: "ph" }, }); const rows = ui.getAllByTestId("sciences-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([ science({ name: "Alpha", drive: 0.1 }), science({ name: "Beta", drive: 0.5 }), science({ name: "Gamma", drive: 0.3 }), ]), ); const driveHeader = ui.getByTestId("sciences-column-drive"); await fireEvent.click(driveHeader); let names = ui .getAllByTestId("sciences-row") .map((row) => row.getAttribute("data-name")); expect(names).toEqual(["Alpha", "Gamma", "Beta"]); await fireEvent.click(driveHeader); names = ui .getAllByTestId("sciences-row") .map((row) => row.getAttribute("data-name")); expect(names).toEqual(["Beta", "Gamma", "Alpha"]); }); test("dblclick on a row navigates to the designer for that science", async () => { const ui = mountTable( makeReport([science({ name: "FirstStep", drive: 1 })]), ); await fireEvent.dblClick(ui.getByTestId("sciences-row")); expect(gotoMock).toHaveBeenCalledWith( "/games/g1/designer/science/FirstStep", ); }); test("delete button adds a removeScience to the draft", async () => { const ui = mountTable(makeReport([science({ name: "FirstStep", drive: 1 })])); await fireEvent.click(ui.getByTestId("sciences-delete")); await waitFor(() => expect(draft.commands).toHaveLength(1)); const cmd = draft.commands[0]!; if (cmd.kind !== "removeScience") throw new Error("wrong kind"); expect(cmd.name).toBe("FirstStep"); }); test("new button navigates to the empty designer", async () => { const ui = mountTable(makeReport([])); await fireEvent.click(ui.getByTestId("sciences-new")); expect(gotoMock).toHaveBeenCalledWith("/games/g1/designer/science"); }); });