// 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 { fireEvent, render, waitFor } from "@testing-library/svelte"; import { Builder } from "flatbuffers"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import InspectorTab from "../src/lib/sidebar/inspector-tab.svelte"; import OrderTab from "../src/lib/sidebar/order-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 { GALAXY_CLIENT_CONTEXT_KEY, GalaxyClientHolder, } from "../src/lib/galaxy-client-context.svelte"; import { i18n } from "../src/lib/i18n/index.svelte"; import { uuidToHiLo, type GameReport, type ReportPlanet } from "../src/api/game-state"; import type { GalaxyClient } from "../src/api/galaxy-client"; import { IDBCache } from "../src/platform/store/idb-cache"; import { openGalaxyDB } from "../src/platform/store/idb"; import { UUID } from "../src/proto/galaxy/fbs/common"; import { CommandItem, CommandPayload, CommandPlanetRename, UserGamesOrderResponse, } from "../src/proto/galaxy/fbs/order"; 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, }; } 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` does not participate in the overlay — the player // has not submitted yet, the inspector still shows the // server-side name. expect(draft.statuses[cmdId]).toBe("valid"); expect(ui.getByTestId("inspector-planet-name")).toHaveTextContent("Earth"); draft.markSubmitting([cmdId]); await waitFor(() => { expect(ui.getByTestId("inspector-planet-name")).toHaveTextContent( "New-Earth", ); }); draft.applyResults({ results: new Map([[cmdId, "applied"] as const]), updatedAt: 99, }); await waitFor(() => { expect(draft.statuses[cmdId]).toBe("applied"); }); 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("submit through the order tab applies the overlay end-to-end", async () => { const cache = new IDBCache(db); const draft = new OrderDraftStore(); await draft.init({ cache, gameId: "11111111-2222-3333-4444-555555555555", }); const cmdId = "00000000-0000-0000-0000-000000000abc"; await draft.add({ kind: "planetRename", id: cmdId, planetNumber: 7, name: "New-Earth", }); const gameState = new GameStateStore(); gameState.gameId = "11111111-2222-3333-4444-555555555555"; gameState.report = makeReport([ makePlanet({ number: 7, name: "Earth", kind: "local", size: 100 }), ]); gameState.status = "ready"; // Stub refresh to return the *un-renamed* server snapshot — // the engine has not applied the rename yet (turn cutoff // pending). The overlay must still show the new name. gameState.refresh = (async () => { gameState.report = makeReport([ makePlanet({ number: 7, name: "Earth", kind: "local", size: 100 }), ]); }) as unknown as typeof gameState.refresh; const selection = new SelectionStore(); selection.selectPlanet(7); const renderedReport = createRenderedReportSource(gameState, draft); const responsePayload = (() => { const builder = new Builder(256); const cmdIdOffset = builder.createString(cmdId); const nameOffset = builder.createString("New-Earth"); const inner = CommandPlanetRename.createCommandPlanetRename( builder, BigInt(7), nameOffset, ); CommandItem.startCommandItem(builder); CommandItem.addCmdId(builder, cmdIdOffset); CommandItem.addCmdApplied(builder, true); CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRename); CommandItem.addPayload(builder, inner); const item = CommandItem.endCommandItem(builder); const commandsVec = UserGamesOrderResponse.createCommandsVector(builder, [ item, ]); const [hi, lo] = uuidToHiLo("11111111-2222-3333-4444-555555555555"); const gameIdOffset = UUID.createUUID(builder, hi, lo); UserGamesOrderResponse.startUserGamesOrderResponse(builder); UserGamesOrderResponse.addGameId(builder, gameIdOffset); UserGamesOrderResponse.addUpdatedAt(builder, BigInt(99)); UserGamesOrderResponse.addCommands(builder, commandsVec); const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder); builder.finish(offset); return builder.asUint8Array(); })(); const exec = vi.fn(async () => ({ resultCode: "ok", payloadBytes: responsePayload, })); const clientHolder = new GalaxyClientHolder(); clientHolder.set({ executeCommand: exec } as unknown as GalaxyClient); const context = new Map([ [GAME_STATE_CONTEXT_KEY, gameState], [SELECTION_CONTEXT_KEY, selection], [ORDER_DRAFT_CONTEXT_KEY, draft], [RENDERED_REPORT_CONTEXT_KEY, renderedReport], [GALAXY_CLIENT_CONTEXT_KEY, clientHolder], ]); const inspector = render(InspectorTab, { context }); const orderTab = render(OrderTab, { context }); // Pre-submit: the inspector still shows the un-renamed snapshot. await waitFor(() => { expect(inspector.getByTestId("inspector-planet-name")).toHaveTextContent( "Earth", ); }); const submit = orderTab.getByTestId("order-submit"); expect(submit).not.toBeDisabled(); await fireEvent.click(submit); await waitFor(() => { expect(draft.statuses[cmdId]).toBe("applied"); }); expect(exec).toHaveBeenCalledTimes(1); await waitFor(() => { expect(inspector.getByTestId("inspector-planet-name")).toHaveTextContent( "New-Earth", ); }); draft.dispose(); }); });