// Phase 20 end-to-end coverage for the ship-group Send action. // Loads a synthetic report with a local group of three Frontier // ships in orbit over Earth and a reachable destination planet // (Mars), opens the inspector by clicking the rendered group, // drives the Send form (asking for 2 ships out of 3), picks Mars // through the map-pick service, and asserts the resulting order // draft has both an implicit `breakShipGroup` and the targeted // `sendShipGroup` whose `groupId` references the freshly minted // sub-group ID. The synthetic flow uses a non-UUID game id, so // the auto-sync pipeline skips the network — the assertion // targets the in-memory draft via the order-tab UI. import { expect, test, type Page } from "@playwright/test"; const SESSION_ID = "phase-20-send-session"; // `Window.__galaxyDebug` is declared as a global in // `tests/e2e/storage-keypair-persistence.spec.ts`; reuse that // declaration so the two specs do not collide on the symbol type. const SYNTHETIC_FIXTURE = { turn: 1, mapWidth: 200, mapHeight: 200, mapPlanets: 2, race: "Earthlings", player: [ { name: "Earthlings", drive: 5, weapons: 0, shields: 0, cargo: 1, population: 1000, industry: 1000, planets: 1, relation: "-", votes: 0, extinct: false, }, { name: "Aliens", drive: 4, weapons: 2, shields: 1, cargo: 1, population: 800, industry: 800, planets: 1, relation: "-", votes: 0, extinct: false, }, ], localPlanet: [ { number: 1, name: "Earth", x: 100, y: 100, size: 1000, population: 1000, industry: 1000, resources: 10, production: "Capital", capital: 0, material: 0, colonists: 100, freeIndustry: 1000, }, ], otherPlanet: [ { number: 2, name: "Mars", x: 110, y: 100, size: 800, population: 800, industry: 800, resources: 8, production: "Capital", capital: 0, material: 0, colonists: 80, freeIndustry: 800, owner: "Aliens", }, ], uninhabitedPlanet: [], unidentifiedPlanet: [], localShipClass: [ { name: "Frontier", drive: 5, armament: 0, weapons: 0, shields: 0, cargo: 1, mass: 12, }, ], localGroup: [ { id: "11111111-2222-3333-4444-555555555555", number: 3, class: "Frontier", tech: { drive: 5, weapons: 0, shields: 0, cargo: 1 }, cargo: "-", load: 0, destination: 1, speed: 25, mass: 12, state: "In_Orbit", }, ], otherGroup: [], incomingGroup: [], unidentifiedGroup: [], localFleet: [], }; async function bootSession(page: Page): Promise { await page.goto("/__debug/store"); await expect(page.getByTestId("debug-store-ready")).toBeVisible(); await page.waitForFunction( () => window.__galaxyDebug?.ready === true, ); await page.evaluate(async () => { const debug = window.__galaxyDebug!; await debug.loadSession(); await debug.setDeviceSessionId("phase-20-send-session"); }); void SESSION_ID; } async function loadSyntheticGame(page: Page): Promise { await page.goto("/lobby"); await expect(page.getByTestId("lobby-synthetic-section")).toBeVisible(); const file = page.getByTestId("lobby-synthetic-file"); await file.setInputFiles({ name: "phase20.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify(SYNTHETIC_FIXTURE)), }); await page.waitForURL(/\/games\/synthetic-[^/]+\/map$/, { timeout: 10_000, }); await expect(page.getByTestId("active-view-map")).toHaveAttribute( "data-status", "ready", ); } // projectWorldToScreen returns the pixel coordinates of a world-space // point (x, y) relative to the document, using the renderer's // debug-surface camera snapshot. Waits for the renderer to register // its debug providers (the in-game shell calls // `installRendererDebugSurface` on mount, then the providers attach // when `mountRenderer` resolves) so the spec is robust against the // async Pixi boot. async function projectWorldToScreen( page: Page, x: number, y: number, ): Promise<{ x: number; y: number }> { await page.waitForFunction(() => { const dbg = window.__galaxyDebug as unknown as | { getMapCamera(): unknown } | undefined; if (dbg === undefined) return false; return dbg.getMapCamera() !== null; }); return page.evaluate(({ wx, wy }) => { const debug = window.__galaxyDebug as unknown as { getMapCamera(): { camera: { centerX: number; centerY: number; scale: number }; viewport: { widthPx: number; heightPx: number }; canvasOrigin: { x: number; y: number }; } | null; }; const cam = debug.getMapCamera(); if (cam === null) throw new Error("camera unavailable"); const sx = cam.canvasOrigin.x + cam.viewport.widthPx / 2 + (wx - cam.camera.centerX) * cam.camera.scale; const sy = cam.canvasOrigin.y + cam.viewport.heightPx / 2 + (wy - cam.camera.centerY) * cam.camera.scale; return { x: sx, y: sy }; }, { wx: x, wy: y }); } test("send 2 of 3 ships emits implicit Break + Send into the order draft", async ({ page, }, testInfo) => { test.skip( testInfo.project.name.startsWith("chromium-mobile"), "phase 20 spec covers desktop layout; mobile inherits the same store", ); await bootSession(page); await loadSyntheticGame(page); // On-planet ship groups are *not* rendered as map primitives (the // renderer hides them to avoid crowding); the player navigates to // them through the planet inspector's stationed-ship row, which // pivots the SelectionStore to the ship-group variant. const earthScreen = await projectWorldToScreen(page, 100, 100); await page.mouse.click(earthScreen.x, earthScreen.y); const sidebar = page.getByTestId("sidebar-tool-inspector"); await expect(sidebar.getByTestId("inspector-planet-name")).toHaveText("Earth"); await sidebar .getByTestId("inspector-planet-ship-groups-select") .first() .click(); await expect( sidebar.getByTestId("inspector-ship-group-class"), ).toHaveText("Frontier"); // Click Send: the inspector enters map-pick mode immediately; the // form (ship count + confirm) only mounts after the destination // is chosen. await sidebar.getByTestId("inspector-ship-group-action-send").click(); await expect( sidebar.getByTestId("inspector-ship-group-form-send-pick-prompt"), ).toBeVisible(); const marsScreen = await projectWorldToScreen(page, 110, 100); await page.mouse.click(marsScreen.x, marsScreen.y); const sendShips = sidebar.getByTestId("inspector-ship-group-form-send-ships"); await sendShips.fill("2"); await expect( sidebar.getByTestId("inspector-ship-group-form-send-destination"), ).toContainText("Mars"); // Confirm. await sidebar.getByTestId("inspector-ship-group-form-send-confirm").click(); // Verify the order tab carries both commands in submission order. await page.getByTestId("sidebar-tab-order").click(); const orderTool = page.getByTestId("sidebar-tool-order"); await expect(orderTool.getByTestId("order-command-label-0")).toContainText( "split group", ); await expect(orderTool.getByTestId("order-command-label-1")).toContainText( "send group", ); });