// Integration test for the Phase 14 optimistic overlay. Mounts the // inspector tab against a real `OrderDraftStore` + `GameStateStore` // + the rendered-report context and walks the full happy path: // add a `planetRename` command → mark it submitting → applied → the // inspector picks up the new name through the overlay without a // re-fetch of the report. import "@testing-library/jest-dom/vitest"; import "fake-indexeddb/auto"; import { render, waitFor } from "@testing-library/svelte"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import InspectorTab from "../src/lib/sidebar/inspector-tab.svelte"; import { GAME_STATE_CONTEXT_KEY, GameStateStore, } from "../src/lib/game-state.svelte"; import { SELECTION_CONTEXT_KEY, SelectionStore, } from "../src/lib/selection.svelte"; import { ORDER_DRAFT_CONTEXT_KEY, OrderDraftStore, } from "../src/sync/order-draft.svelte"; import { RENDERED_REPORT_CONTEXT_KEY, createRenderedReportSource, } from "../src/lib/rendered-report.svelte"; import { i18n } from "../src/lib/i18n/index.svelte"; import type { GameReport, ReportPlanet } from "../src/api/game-state"; import { IDBCache } from "../src/platform/store/idb-cache"; import { openGalaxyDB } from "../src/platform/store/idb"; let db: Awaited>; let dbName: string; beforeEach(async () => { dbName = `galaxy-overlay-${crypto.randomUUID()}`; db = await openGalaxyDB(dbName); i18n.resetForTests("en"); }); afterEach(async () => { db.close(); await new Promise((resolve) => { const req = indexedDB.deleteDatabase(dbName); req.onsuccess = () => resolve(); req.onerror = () => resolve(); req.onblocked = () => resolve(); }); }); function makePlanet(overrides: Partial): ReportPlanet { return { number: 0, 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, ...overrides, }; } function makeReport(planets: ReportPlanet[]): GameReport { return { turn: 4, mapWidth: 1000, mapHeight: 1000, planetCount: planets.length, planets, race: "", localShipClass: [], routes: [], localPlayerDrive: 0, }; } describe("inspector overlay reactivity", () => { test("applied planetRename swaps the name without a report refresh", async () => { const cache = new IDBCache(db); const draft = new OrderDraftStore(); await draft.init({ cache, gameId: "00000000-0000-0000-0000-000000000abc", }); const gameState = new GameStateStore(); gameState.report = makeReport([ makePlanet({ number: 7, name: "Earth", kind: "local", size: 100 }), ]); gameState.status = "ready"; const selection = new SelectionStore(); selection.selectPlanet(7); const renderedReport = createRenderedReportSource(gameState, draft); const context = new Map([ [GAME_STATE_CONTEXT_KEY, gameState], [SELECTION_CONTEXT_KEY, selection], [ORDER_DRAFT_CONTEXT_KEY, draft], [RENDERED_REPORT_CONTEXT_KEY, renderedReport], ]); const ui = render(InspectorTab, { context }); await waitFor(() => { expect(ui.getByTestId("inspector-planet-name")).toHaveTextContent("Earth"); }); const cmdId = "00000000-0000-0000-0000-000000000001"; await draft.add({ kind: "planetRename", id: cmdId, planetNumber: 7, name: "New-Earth", }); // `valid` already participates in the overlay (auto-sync may // not have fired yet, but the player's intent is committed). expect(draft.statuses[cmdId]).toBe("valid"); await waitFor(() => { expect(ui.getByTestId("inspector-planet-name")).toHaveTextContent( "New-Earth", ); }); // A simulated server refresh that returns the *un-renamed* // snapshot must not erase the overlay (turn cutoff has not // run yet, the engine still reports the old name). gameState.report = makeReport([ makePlanet({ number: 7, name: "Earth", kind: "local", size: 100 }), ]); await waitFor(() => { expect(ui.getByTestId("inspector-planet-name")).toHaveTextContent( "New-Earth", ); }); draft.dispose(); }); test("auto-sync after add applies the overlay end-to-end", async () => { const { recordingClient } = await import("./helpers/fake-order-client"); const handle = recordingClient(GAME_ID, "ok"); const cache = new IDBCache(db); const draft = new OrderDraftStore(); await draft.init({ cache, gameId: GAME_ID }); draft.bindClient(handle.client); const gameState = new GameStateStore(); gameState.gameId = GAME_ID; gameState.report = makeReport([ makePlanet({ number: 7, name: "Earth", kind: "local", size: 100 }), ]); gameState.status = "ready"; const selection = new SelectionStore(); selection.selectPlanet(7); const renderedReport = createRenderedReportSource(gameState, draft); const context = new Map([ [GAME_STATE_CONTEXT_KEY, gameState], [SELECTION_CONTEXT_KEY, selection], [ORDER_DRAFT_CONTEXT_KEY, draft], [RENDERED_REPORT_CONTEXT_KEY, renderedReport], ]); const inspector = render(InspectorTab, { context }); await waitFor(() => { expect(inspector.getByTestId("inspector-planet-name")).toHaveTextContent( "Earth", ); }); const cmdId = "00000000-0000-0000-0000-000000000abc"; await draft.add({ kind: "planetRename", id: cmdId, planetNumber: 7, name: "New-Earth", }); // Overlay applies on `valid` immediately — auto-sync hasn't // landed yet but the player's intent is committed. await waitFor(() => { expect(inspector.getByTestId("inspector-planet-name")).toHaveTextContent( "New-Earth", ); }); await handle.waitForCalls(1); await waitFor(() => { expect(draft.statuses[cmdId]).toBe("applied"); }); expect(handle.calls).toHaveLength(1); expect(handle.calls[0]!.commandIds).toEqual([cmdId]); // Inspector still shows the new name after auto-sync. expect(inspector.getByTestId("inspector-planet-name")).toHaveTextContent( "New-Earth", ); draft.dispose(); }); }); const GAME_ID = "11111111-2222-3333-4444-555555555555";