// Component tests for the create-game form. The lobby API is mocked // at module level; the GalaxyClient is replaced with a stub that does // nothing (the test only asserts the createGame wrapper is invoked // with the right shape). import "fake-indexeddb/auto"; import { fireEvent, render, waitFor } from "@testing-library/svelte"; import { afterEach, beforeEach, describe, expect, test, vi, } from "vitest"; import type { IDBPDatabase } from "idb"; import { i18n } from "../src/lib/i18n/index.svelte"; import { session } from "../src/lib/session-store.svelte"; import { type GalaxyDB, openGalaxyDB } from "../src/platform/store/idb"; import { IDBCache } from "../src/platform/store/idb-cache"; import { WebCryptoKeyStore } from "../src/platform/store/webcrypto-keystore"; const gotoSpy = vi.fn<(url: string) => Promise>(async () => {}); vi.mock("$app/navigation", () => ({ goto: (url: string) => gotoSpy(url), })); const createGameSpy = vi.fn(); vi.mock("../src/api/lobby", async () => { const actual = await vi.importActual( "../src/api/lobby", ); return { ...actual, createGame: (...args: unknown[]) => createGameSpy(...args), }; }); vi.mock("../src/lib/env", () => ({ GATEWAY_BASE_URL: "http://gateway.test", GATEWAY_RESPONSE_PUBLIC_KEY: new Uint8Array(32).fill(0x55), })); vi.mock("../src/api/connect", () => ({ createEdgeGatewayClient: vi.fn(() => ({})), })); vi.mock("../src/api/galaxy-client", () => { class FakeGalaxyClient { executeCommand = vi.fn(async () => ({ resultCode: "ok", payloadBytes: new Uint8Array(), })); } return { GalaxyClient: FakeGalaxyClient }; }); vi.mock("../src/platform/core/index", () => ({ loadCore: async () => ({ signRequest: () => new Uint8Array(), verifyResponse: () => true, verifyEvent: () => true, verifyPayloadHash: () => true, }), })); let db: IDBPDatabase; let dbName: string; beforeEach(async () => { dbName = `galaxy-ui-test-${crypto.randomUUID()}`; db = await openGalaxyDB(dbName); const store = { keyStore: new WebCryptoKeyStore(db), cache: new IDBCache(db), }; session.resetForTests(); session.setStoreLoaderForTests(async () => store); await session.init(); await session.signIn("device-1"); i18n.resetForTests("en"); createGameSpy.mockReset(); gotoSpy.mockReset(); }); afterEach(async () => { session.resetForTests(); i18n.resetForTests("en"); db.close(); await new Promise((resolve) => { const req = indexedDB.deleteDatabase(dbName); req.onsuccess = () => resolve(); req.onerror = () => resolve(); req.onblocked = () => resolve(); }); }); async function importCreatePage(): Promise { return import("../src/routes/lobby/create/+page.svelte"); } describe("lobby/create page", () => { test("submitting a valid form invokes createGame with the entered values and navigates back", async () => { createGameSpy.mockResolvedValue({ gameId: "private-new", gameName: "First Contact", gameType: "private", status: "draft", ownerUserId: "user-1", minPlayers: 2, maxPlayers: 8, enrollmentEndsAt: new Date(), createdAt: new Date(), updatedAt: new Date(), }); const Page = (await importCreatePage()).default; const ui = render(Page); await waitFor(() => expect(ui.getByTestId("lobby-create-form")).toBeInTheDocument(), ); await fireEvent.input(ui.getByTestId("lobby-create-game-name"), { target: { value: "First Contact" }, }); await fireEvent.input(ui.getByTestId("lobby-create-description"), { target: { value: "" }, }); await fireEvent.input(ui.getByTestId("lobby-create-turn-schedule"), { target: { value: "0 0 * * *" }, }); await fireEvent.input(ui.getByTestId("lobby-create-enrollment-ends-at"), { target: { value: "2026-06-01T12:00" }, }); await fireEvent.click(ui.getByTestId("lobby-create-submit")); await waitFor(() => { expect(createGameSpy).toHaveBeenCalledTimes(1); const call = createGameSpy.mock.calls[0]!; const input = call[1] as Record; expect(input.gameName).toBe("First Contact"); expect(input.turnSchedule).toBe("0 0 * * *"); expect(input.minPlayers).toBe(2); expect(input.maxPlayers).toBe(8); expect(input.startGapHours).toBe(24); expect(input.startGapPlayers).toBe(2); expect(input.targetEngineVersion).toBe("v1"); expect(input.enrollmentEndsAt).toBeInstanceOf(Date); expect(gotoSpy).toHaveBeenCalledWith("/lobby"); }); }); test("submitting with an empty game name surfaces a validation error and does not call the API", async () => { const Page = (await importCreatePage()).default; const ui = render(Page); await waitFor(() => expect(ui.getByTestId("lobby-create-form")).toBeInTheDocument(), ); // turn_schedule starts populated with the default; clear game_name to trigger the error await fireEvent.input(ui.getByTestId("lobby-create-game-name"), { target: { value: " " }, }); await fireEvent.input(ui.getByTestId("lobby-create-enrollment-ends-at"), { target: { value: "2026-06-01T12:00" }, }); await fireEvent.click(ui.getByTestId("lobby-create-submit")); await waitFor(() => { expect(ui.getByTestId("lobby-create-error")).toHaveTextContent( "game name must not be empty", ); expect(createGameSpy).not.toHaveBeenCalled(); }); }); test("cancel button navigates back to /lobby without calling the API", async () => { const Page = (await importCreatePage()).default; const ui = render(Page); await waitFor(() => expect(ui.getByTestId("lobby-create-cancel")).toBeInTheDocument(), ); await fireEvent.click(ui.getByTestId("lobby-create-cancel")); await waitFor(() => { expect(gotoSpy).toHaveBeenCalledWith("/lobby"); expect(createGameSpy).not.toHaveBeenCalled(); }); }); });