// Phase 12 end-to-end coverage for the order composer skeleton. The // boot flow seeds an authenticated session and a draft directly // through `/__debug/store`, then enters the game via the dev-only // `window.__galaxyNav` affordance (the single-URL app-shell has no // `/games//` route) and exercises the order tab. // // The shell's per-game bootstrap now talks to the gateway on entry // (lobby validation, report, order read-back). This spec does not // stand up a real gateway, so those Connect-Web calls are aborted via // `page.route` — the shell tolerates the failure (cache fallback + // `failBootstrap`) and still renders the chrome. Aborting also keeps // a mid-spec `page.reload()` from hanging: an unrouted `/rpc` call // to a dead proxy never settles, which otherwise stalls the reload's // load event. // // Persistence is covered by reloading the page mid-spec: the // `OrderDraftStore` re-reads the same cache row on the next mount, // so the rendered list survives the round-trip. import { expect, test, type Page } from "@playwright/test"; // `window.__galaxyDebug` is owned by `routes/__debug/store/+page.svelte` // and `window.__galaxyNav` by `routes/+page.svelte`; both are typed by // `tests/e2e/storage-keypair-persistence.spec.ts`. const SESSION_ID = "phase-12-order-session"; const GAME_ID = "test-order"; // Fail-fast the shell's gateway calls so the spec needs no real // backend and reloads settle promptly. async function stubGateway(page: Page): Promise { await page.route("**/edge.v1.Gateway/**", (route) => route.abort()); } // Load the app (seeded session → authenticated lobby) and enter the // game on the map view through the in-memory nav affordance. async function enterGameMap(page: Page): Promise { await page.goto("/"); await page.waitForFunction(() => window.__galaxyNav !== undefined); await page.evaluate( (id) => window.__galaxyNav!.enterGame(id, "map", {}), GAME_ID, ); await expect(page.getByTestId("game-shell")).toBeVisible(); await expect(page.getByTestId("active-view-map")).toBeVisible(); } const SEED = [ { kind: "placeholder" as const, id: "cmd-a", label: "first command" }, { kind: "placeholder" as const, id: "cmd-b", label: "second command" }, { kind: "placeholder" as const, id: "cmd-c", label: "third command" }, ]; async function bootDebug(page: Page): Promise { await stubGateway(page); await page.goto("/__debug/store"); await expect(page.getByTestId("debug-store-ready")).toBeVisible(); await page.waitForFunction(() => window.__galaxyDebug?.ready === true); } async function seedShell(page: Page): Promise { await bootDebug(page); await page.evaluate(() => window.__galaxyDebug!.clearSession()); await page.evaluate( (id) => window.__galaxyDebug!.setDeviceSessionId(id), SESSION_ID, ); await page.evaluate( ({ gameId, commands }) => window.__galaxyDebug!.clearOrderDraft(gameId).then(() => window.__galaxyDebug!.seedOrderDraft(gameId, commands), ), { gameId: GAME_ID, commands: SEED }, ); } async function openOrderTool(page: Page, isMobile: boolean): Promise { if (isMobile) { await page.getByTestId("bottom-tab-order").click(); } else { await page.getByTestId("sidebar-tab-order").click(); } await expect(page.getByTestId("sidebar-tool-order")).toBeVisible(); } async function expectSeededRows(page: Page): Promise { const list = page.getByTestId("order-list"); await expect(list).toBeVisible(); for (let i = 0; i < SEED.length; i++) { const row = page.getByTestId(`order-command-${i}`); await expect(row).toBeVisible(); await expect(row.getByTestId(`order-command-label-${i}`)).toHaveText( SEED[i]!.label, ); } await expect(page.getByTestId("order-empty")).toHaveCount(0); } test("seeded draft renders on the order tab and survives a reload", async ({ page, }, testInfo) => { const isMobile = testInfo.project.name.startsWith("chromium-mobile"); await seedShell(page); await enterGameMap(page); await openOrderTool(page, isMobile); await expectSeededRows(page); // Reload restores the `game` screen from the persisted nav snapshot, // whose first authenticated render re-stamps screen history via // SvelteKit shallow routing. That `pushState` lands right after the // document loads and would abort a default `reload()` (which waits // for `load`); waiting only for the navigation to commit sidesteps // the race while still re-executing the app from scratch. await page.reload({ waitUntil: "commit" }); await expect(page.getByTestId("game-shell")).toBeVisible(); await openOrderTool(page, isMobile); await expectSeededRows(page); }); test("removing a command from the order tab persists the removal", async ({ page, }, testInfo) => { const isMobile = testInfo.project.name.startsWith("chromium-mobile"); await seedShell(page); await enterGameMap(page); await openOrderTool(page, isMobile); await expect(page.getByTestId("order-command-1")).toBeVisible(); await page.getByTestId("order-command-delete-1").click(); // The remaining two commands shift up by one slot. await expect(page.getByTestId("order-command-label-0")).toHaveText( SEED[0]!.label, ); await expect(page.getByTestId("order-command-label-1")).toHaveText( SEED[2]!.label, ); await expect(page.getByTestId("order-command-2")).toHaveCount(0); // See the note on the sibling test: the restored `game` screen // re-stamps history on reload, so wait only for the navigation to // commit to avoid the shallow-routing `pushState` aborting it. await page.reload({ waitUntil: "commit" }); await expect(page.getByTestId("game-shell")).toBeVisible(); await openOrderTool(page, isMobile); await expect(page.getByTestId("order-command-label-0")).toHaveText( SEED[0]!.label, ); await expect(page.getByTestId("order-command-label-1")).toHaveText( SEED[2]!.label, ); await expect(page.getByTestId("order-command-2")).toHaveCount(0); }); test("empty draft renders the empty-state copy", async ({ page, }, testInfo) => { const isMobile = testInfo.project.name.startsWith("chromium-mobile"); await bootDebug(page); await page.evaluate(() => window.__galaxyDebug!.clearSession()); await page.evaluate( (id) => window.__galaxyDebug!.setDeviceSessionId(id), SESSION_ID, ); await page.evaluate( (gameId) => window.__galaxyDebug!.clearOrderDraft(gameId), GAME_ID, ); await enterGameMap(page); await openOrderTool(page, isMobile); await expect(page.getByTestId("order-empty")).toBeVisible(); await expect(page.getByTestId("order-list")).toHaveCount(0); });