Files
galaxy-game/ui/frontend/tests/e2e/ship-group-send.spec.ts
T
Ilia Denisov cff7cc3859
Tests · UI / test (push) Failing after 3m8s
fix(ui): F8-04b e2e — viewport-agnostic nav + refresh after create
- lobby-create-screen: call lobbyData.refresh() after a successful
  POST so the new game shows up in the private-games panel
  immediately. The shared lobby-data store is otherwise lazy
  (ensure-on-first-mount), which rendered a stale list across the
  post-create navigation in the e2e suite.
- e2e tests that move between lobby sub-panels now go through
  `window.__galaxyNav.go(...)` rather than clicking the sidebar
  items. The mobile sidebar tucks the submenu behind a dropdown, so
  testid-based clicks fail on the mobile-iphone-13 / pixel-5
  viewports — the dev nav surface bypasses that UX (which has its
  own coverage in `lobby-tier-gate` / future submenu specs).
- game-shell-map missing-membership test: assert
  `lobby-account-name` instead of `lobby-create-button` on
  drop-back-to-lobby (the button moved into the paid-only
  private-games sub-panel; the identity strip is the constant lobby
  chrome).
- inspector-ship-group + ship-group-send synthetic loader specs:
  jump straight to the dev-only `synthetic-reports` top-level
  screen via the dev nav surface before looking for the file
  input (the loader moved off Overview in F8-04b).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:25:49 +02:00

256 lines
7.4 KiB
TypeScript

// 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<void> {
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<void> {
// Seeded session → the dispatcher renders the lobby. F8-04b moved
// the synthetic-report loader off of Overview into its own
// dev-only top-level screen; jump straight to it via the dev nav
// surface so the spec is viewport-agnostic.
await page.goto("/");
await page.waitForFunction(() => window.__galaxyNav !== undefined);
await page.evaluate(() => window.__galaxyNav!.go("synthetic-reports"));
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)),
});
// Loading the report enters the game in place (the address bar stays
// at the app base); the map view reaching `ready` is the signal.
await expect(page.getByTestId("game-shell")).toBeVisible({ 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",
);
});