// Phase 22 end-to-end coverage for the Races View. Boots an // authenticated session, mocks the gateway with three non-extinct // other races (mixed WAR/PEACE), navigates to the races table, then: // // 1. flips one row's stance from PEACE to WAR — observes the // submitted order envelope decoded as `CommandRaceRelation`, // with the expected `acceptor` + `relation`; // 2. changes the vote recipient — observes the submitted order // envelope decoded as `CommandRaceVote`; // 3. after the auto-sync round-trip both rows show as `applied` // in the sidebar order tab. import { fromJson, type JsonValue } from "@bufbuild/protobuf"; import { expect, test, type Page } from "@playwright/test"; import { ByteBuffer } from "flatbuffers"; import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { UUID } from "../../src/proto/galaxy/fbs/common"; import { CommandPayload, CommandRaceRelation, CommandRaceVote, Relation, UserGamesOrder, UserGamesOrderGet, } from "../../src/proto/galaxy/fbs/order"; import { GameReportRequest } from "../../src/proto/galaxy/fbs/report"; import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response"; import { buildMyGamesListPayload, type GameFixture, } from "./fixtures/lobby-fbs"; import { buildReportPayload } from "./fixtures/report-fbs"; import { buildOrderGetResponsePayload, buildOrderResponsePayload, type CommandResultFixture, } from "./fixtures/order-fbs"; const SESSION_ID = "phase-22-races-session"; const GAME_ID = "22222222-2222-2222-2222-222222222222"; interface MockHandle { get lastStance(): { acceptor: string; relation: "WAR" | "PEACE" } | null; get lastVote(): { acceptor: string } | null; } async function mockGateway(page: Page): Promise { const game: GameFixture = { gameId: GAME_ID, gameName: "Phase 22 Game", gameType: "private", status: "running", ownerUserId: "user-1", minPlayers: 2, maxPlayers: 8, enrollmentEndsAtMs: BigInt(Date.now() + 86_400_000), createdAtMs: BigInt(Date.now() - 86_400_000), updatedAtMs: BigInt(Date.now()), currentTurn: 1, }; let storedOrder: CommandResultFixture[] = []; let lastStance: MockHandle["lastStance"] = null; let lastVote: MockHandle["lastVote"] = null; await page.route( "**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", async (route) => { const reqText = route.request().postData(); if (reqText === null) { await route.fulfill({ status: 400 }); return; } const req = fromJson( ExecuteCommandRequestSchema, JSON.parse(reqText) as JsonValue, ); let resultCode = "ok"; let payload: Uint8Array; switch (req.messageType) { case "lobby.my.games.list": payload = buildMyGamesListPayload([game]); break; case "user.games.report": { GameReportRequest.getRootAsGameReportRequest( new ByteBuffer(req.payloadBytes), ).gameId(new UUID()); payload = buildReportPayload({ turn: 1, mapWidth: 4000, mapHeight: 4000, race: "Earthlings", myVotes: 4, myVoteFor: "Earthlings", players: [ { name: "Earthlings", drive: 1, weapons: 1, shields: 1, cargo: 1, population: 4000, industry: 3000, planets: 2, relation: "-", votes: 4, }, { name: "Andori", drive: 1, weapons: 1, shields: 1, cargo: 1, population: 3000, industry: 2500, planets: 2, relation: "PEACE", votes: 3, }, { name: "Bajori", drive: 1, weapons: 1, shields: 1, cargo: 1, population: 2000, industry: 1500, planets: 1, relation: "PEACE", votes: 2, }, { name: "Cardassian", drive: 1, weapons: 1, shields: 1, cargo: 1, population: 1000, industry: 800, planets: 1, relation: "WAR", votes: 1, }, ], localPlanets: [ { number: 1, name: "Earth", x: 2000, y: 2000, size: 1000, resources: 5, population: 4000, industry: 3000, }, ], }); break; } case "user.games.order": { const decoded = UserGamesOrder.getRootAsUserGamesOrder( new ByteBuffer(req.payloadBytes), ); const length = decoded.commandsLength(); const fixtures: CommandResultFixture[] = []; for (let i = 0; i < length; i++) { const item = decoded.commands(i); if (item === null) continue; const cmdId = item.cmdId() ?? ""; const payloadType = item.payloadType(); if (payloadType === CommandPayload.CommandRaceRelation) { const inner = new CommandRaceRelation(); item.payload(inner); const relation = inner.relation() === Relation.WAR ? "WAR" : "PEACE"; lastStance = { acceptor: inner.acceptor() ?? "", relation, }; fixtures.push({ kind: "setDiplomaticStance", cmdId, acceptor: lastStance.acceptor, relation, applied: true, errorCode: null, }); continue; } if (payloadType === CommandPayload.CommandRaceVote) { const inner = new CommandRaceVote(); item.payload(inner); lastVote = { acceptor: inner.acceptor() ?? "" }; fixtures.push({ kind: "setVoteRecipient", cmdId, acceptor: lastVote.acceptor, applied: true, errorCode: null, }); continue; } } storedOrder = fixtures; payload = buildOrderResponsePayload(GAME_ID, fixtures, Date.now()); break; } case "user.games.order.get": { UserGamesOrderGet.getRootAsUserGamesOrderGet( new ByteBuffer(req.payloadBytes), ); payload = buildOrderGetResponsePayload( GAME_ID, storedOrder, Date.now(), storedOrder.length > 0, ); break; } default: resultCode = "internal_error"; payload = new Uint8Array(); } const body = await forgeExecuteCommandResponseJson({ requestId: req.requestId, timestampMs: BigInt(Date.now()), resultCode, payloadBytes: payload, }); await route.fulfill({ status: 200, contentType: "application/json", body, }); }, ); await page.route( "**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", async () => { await new Promise(() => {}); }, ); return { get lastStance() { return lastStance; }, get lastVote() { return lastVote; }, }; } 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(() => window.__galaxyDebug!.clearSession()); await page.evaluate( (id) => window.__galaxyDebug!.setDeviceSessionId(id), SESSION_ID, ); await page.evaluate( (gameId) => window.__galaxyDebug!.clearOrderDraft(gameId), GAME_ID, ); } test("toggle stance and pick a vote target via the races table", async ({ page, }, testInfo) => { test.skip( testInfo.project.name.startsWith("chromium-mobile"), "phase 22 spec covers desktop layout; mobile inherits the same store", ); const handle = await mockGateway(page); await bootSession(page); await page.goto(`/games/${GAME_ID}/table/races`); const tableHost = page.getByTestId("active-view-table"); await expect(tableHost).toBeVisible(); await expect(page.getByTestId("races-table")).toBeVisible(); // Flip Andori from PEACE to WAR through the per-row segmented // control. The optimistic overlay flips the buttons immediately; // the auto-sync round-trip echoes back as applied. const andoriRow = page.locator( '[data-testid="races-row"][data-name="Andori"]', ); const andoriWar = andoriRow.getByTestId("races-stance-war"); await andoriWar.click(); await expect(andoriWar).toHaveAttribute("aria-pressed", "true"); // Pick Andori as the vote target. await page.getByTestId("races-vote-target").selectOption("Andori"); // Both commands appear in the sidebar order tab as applied. await page.getByTestId("sidebar-tab-order").click(); const orderTool = page.getByTestId("sidebar-tool-order"); await expect(orderTool.getByTestId("order-command-status-0")).toHaveText( "applied", ); await expect(orderTool.getByTestId("order-command-status-1")).toHaveText( "applied", ); // The gateway saw both commands with the expected payloads. expect(handle.lastStance?.acceptor).toBe("Andori"); expect(handle.lastStance?.relation).toBe("WAR"); expect(handle.lastVote?.acceptor).toBe("Andori"); });