// Component coverage for the Phase 14 order-tab submit flow. Drives // the tab against an in-memory `OrderDraftStore`, a synthetic // `GalaxyClient`, and a stubbed `GameStateStore.refresh`. Every // case asserts both the rendered DOM (status badges, button state) // and the side effect on the draft store (per-command status flips). 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 OrderTab from "../src/lib/sidebar/order-tab.svelte"; import { ORDER_DRAFT_CONTEXT_KEY, OrderDraftStore, } from "../src/sync/order-draft.svelte"; import { GAME_STATE_CONTEXT_KEY, GameStateStore, } from "../src/lib/game-state.svelte"; import { GALAXY_CLIENT_CONTEXT_KEY, GalaxyClientHolder, } from "../src/lib/galaxy-client-context.svelte"; import { i18n } from "../src/lib/i18n/index.svelte"; import { uuidToHiLo } from "../src/api/game-state"; import type { GalaxyClient } from "../src/api/galaxy-client"; import type { OrderCommand } from "../src/sync/order-types"; 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 { UUID } from "../src/proto/galaxy/fbs/common"; import { CommandItem, CommandPayload, CommandPlanetRename, UserGamesOrderResponse, } from "../src/proto/galaxy/fbs/order"; const GAME_ID = "11111111-2222-3333-4444-555555555555"; let db: Awaited>; let dbName: string; let cache: Cache; beforeEach(async () => { dbName = `galaxy-order-tab-${crypto.randomUUID()}`; db = await openGalaxyDB(dbName); cache = new IDBCache(db); 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(); }); }); interface Setup { context: Map; draft: OrderDraftStore; gameState: GameStateStore; clientHolder: GalaxyClientHolder; exec: ReturnType; refresh: ReturnType; } function buildResponse( commands: { id: string; applied: boolean | null; errorCode: number | null }[], updatedAt: number, ): Uint8Array { const builder = new Builder(256); const itemOffsets = commands.map((c) => { const cmdIdOffset = builder.createString(c.id); const nameOffset = builder.createString("ignored"); const inner = CommandPlanetRename.createCommandPlanetRename( builder, BigInt(0), nameOffset, ); CommandItem.startCommandItem(builder); CommandItem.addCmdId(builder, cmdIdOffset); if (c.applied !== null) CommandItem.addCmdApplied(builder, c.applied); if (c.errorCode !== null) { CommandItem.addCmdErrorCode(builder, BigInt(c.errorCode)); } CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRename); CommandItem.addPayload(builder, inner); return CommandItem.endCommandItem(builder); }); const commandsVec = UserGamesOrderResponse.createCommandsVector( builder, itemOffsets, ); const [hi, lo] = uuidToHiLo(GAME_ID); const gameIdOffset = UUID.createUUID(builder, hi, lo); UserGamesOrderResponse.startUserGamesOrderResponse(builder); UserGamesOrderResponse.addGameId(builder, gameIdOffset); UserGamesOrderResponse.addUpdatedAt(builder, BigInt(updatedAt)); UserGamesOrderResponse.addCommands(builder, commandsVec); const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder); builder.finish(offset); return builder.asUint8Array(); } async function makeSetup(commands: OrderCommand[]): Promise { const draft = new OrderDraftStore(); await draft.init({ cache, gameId: GAME_ID }); for (const cmd of commands) { await draft.add(cmd); } const gameState = new GameStateStore(); gameState.gameId = GAME_ID; gameState.status = "ready"; const refresh = vi.fn(async () => {}); gameState.refresh = refresh as unknown as typeof gameState.refresh; const clientHolder = new GalaxyClientHolder(); const exec = vi.fn(async (_messageType: string, _payload: Uint8Array) => ({ resultCode: "ok", payloadBytes: buildResponse( commands.map((cmd) => ({ id: cmd.id, applied: true, errorCode: null, })), 17, ), })); clientHolder.set({ executeCommand: exec } as unknown as GalaxyClient); const context = new Map([ [ORDER_DRAFT_CONTEXT_KEY, draft], [GAME_STATE_CONTEXT_KEY, gameState], [GALAXY_CLIENT_CONTEXT_KEY, clientHolder], ]); return { context, draft, gameState, clientHolder, exec, refresh }; } describe("order-tab", () => { test("renders the empty state when the draft has no commands", async () => { const { context } = await makeSetup([]); const ui = render(OrderTab, { context }); expect(ui.getByTestId("order-empty")).toBeVisible(); expect(ui.queryByTestId("order-submit")).toBeNull(); }); test("Submit is disabled when every entry is invalid", async () => { const { context } = await makeSetup([ { kind: "planetRename", id: "id-1", planetNumber: 1, name: "" }, ]); const ui = render(OrderTab, { context }); const submit = ui.getByTestId("order-submit"); expect(submit).toBeDisabled(); expect(ui.getByTestId("order-command-status-0")).toHaveTextContent( "invalid", ); }); test("Submit posts every valid command and applies returned statuses", async () => { const { context, draft, exec, refresh } = await makeSetup([ { kind: "planetRename", id: "id-1", planetNumber: 1, name: "Earth" }, ]); const ui = render(OrderTab, { context }); const submit = ui.getByTestId("order-submit"); expect(submit).not.toBeDisabled(); expect(ui.getByTestId("order-command-status-0")).toHaveTextContent("valid"); await fireEvent.click(submit); await waitFor(() => { expect(draft.statuses["id-1"]).toBe("applied"); }); expect(exec).toHaveBeenCalledTimes(1); expect(refresh).toHaveBeenCalledTimes(1); expect(ui.getByTestId("order-command-status-0")).toHaveTextContent( "applied", ); }); test("Non-ok response marks every submitting entry as rejected", async () => { const { context, draft, refresh } = await makeSetup([ { kind: "planetRename", id: "id-1", planetNumber: 1, name: "Earth" }, ]); const exec = vi.fn(async () => ({ resultCode: "invalid_request", payloadBytes: new TextEncoder().encode( JSON.stringify({ code: "boom", message: "down" }), ), })); const holder = context.get(GALAXY_CLIENT_CONTEXT_KEY) as GalaxyClientHolder; holder.set({ executeCommand: exec } as unknown as GalaxyClient); const ui = render(OrderTab, { context }); await fireEvent.click(ui.getByTestId("order-submit")); await waitFor(() => { expect(draft.statuses["id-1"]).toBe("rejected"); }); expect(refresh).not.toHaveBeenCalled(); expect(ui.getByTestId("order-submit-error")).toHaveTextContent("down"); }); test("Already-applied entries do not get re-submitted", async () => { const { context, draft, exec } = await makeSetup([ { kind: "planetRename", id: "id-1", planetNumber: 1, name: "Earth" }, ]); draft.markSubmitting(["id-1"]); draft.applyResults({ results: new Map([["id-1", "applied"] as const]), updatedAt: 1, }); const ui = render(OrderTab, { context }); const submit = ui.getByTestId("order-submit"); expect(submit).toBeDisabled(); expect(exec).not.toHaveBeenCalled(); }); });