80ed11e3b6
Lights up three previously-stubbed table active views and tightens the
existing one:
- table-planets: 4 kind checkboxes (own / foreign / uninhabited /
unknown) + race dropdown that filters the foreign slice; row click
selects + centres the planet on the map.
- table-ship-groups: local + foreign groups in one grid, owner
checkboxes, planet dropdown (destination OR origin), class
dropdown; on-planet click focuses the destination planet, in-space
click focuses the ship group itself (camera follows interpolated
position).
- table-fleets: own fleets only with the shared planet dropdown;
on-planet click focuses the planet, in-space click centres the
camera on the interpolated fleet position without altering the
selection (no fleet variant in Selected).
- table-ship-classes: per-row Delete is disabled with a count tooltip
while at least one local ship group references the class. The
engine refuses the removal anyway; the UI pre-empts the surface.
Wires the click → map flow through a transient `SelectionStore.focus`
/ `focusPoint` channel that `map.svelte` consumes once on mount —
in-memory only, so an F5 does not re-centre.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
144 lines
4.1 KiB
TypeScript
144 lines
4.1 KiB
TypeScript
// F8-10 end-to-end coverage for the planets table → map navigation.
|
|
// Boots an authenticated session, mocks the gateway with a small
|
|
// planets-only report, navigates to `table → planets`, clicks the
|
|
// first row, and asserts the active view switches to the map (which
|
|
// also implicitly proves that the `SelectionStore.focus` → map mount
|
|
// hand-off lands inside the live shell). Ship-groups and fleets are
|
|
// covered by their vitest specs — one e2e is enough to smoke the
|
|
// composed flow.
|
|
|
|
import { fromJson, type JsonValue } from "@bufbuild/protobuf";
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
|
|
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
|
|
import {
|
|
buildMyGamesListPayload,
|
|
type GameFixture,
|
|
} from "./fixtures/lobby-fbs";
|
|
import { buildReportPayload } from "./fixtures/report-fbs";
|
|
|
|
const SESSION_ID = "f8-10-tables-session";
|
|
const GAME_ID = "f8101010-0000-4000-8000-101010101010";
|
|
|
|
async function mockGateway(page: Page): Promise<void> {
|
|
const game: GameFixture = {
|
|
gameId: GAME_ID,
|
|
gameName: "F8-10 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,
|
|
};
|
|
|
|
await page.route(
|
|
"**/edge.v1.Gateway/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 payload: Uint8Array;
|
|
switch (req.messageType) {
|
|
case "lobby.my.games.list":
|
|
payload = buildMyGamesListPayload([game]);
|
|
break;
|
|
case "user.games.report":
|
|
payload = buildReportPayload({
|
|
turn: 1,
|
|
mapWidth: 4000,
|
|
mapHeight: 4000,
|
|
localPlanets: [
|
|
{ number: 1, name: "Home", x: 1000, y: 1000 },
|
|
],
|
|
otherPlanets: [
|
|
{
|
|
number: 2,
|
|
name: "Frontier",
|
|
x: 2000,
|
|
y: 1500,
|
|
owner: "Federation",
|
|
},
|
|
],
|
|
uninhabitedPlanets: [
|
|
{ number: 3, name: "Rock", x: 1500, y: 2200 },
|
|
],
|
|
});
|
|
break;
|
|
default:
|
|
payload = new Uint8Array();
|
|
}
|
|
|
|
const body = await forgeExecuteCommandResponseJson({
|
|
requestId: req.requestId,
|
|
timestampMs: BigInt(Date.now()),
|
|
resultCode: "ok",
|
|
payloadBytes: payload,
|
|
});
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body,
|
|
});
|
|
},
|
|
);
|
|
|
|
// Hold SubscribeEvents open — mirrors the pattern in other e2e
|
|
// specs to avoid the revocation watcher signing the session out.
|
|
await page.route(
|
|
"**/edge.v1.Gateway/SubscribeEvents",
|
|
async () => {
|
|
await new Promise<void>(() => {});
|
|
},
|
|
);
|
|
}
|
|
|
|
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(() => window.__galaxyDebug!.clearSession());
|
|
await page.evaluate(
|
|
(id) => window.__galaxyDebug!.setDeviceSessionId(id),
|
|
SESSION_ID,
|
|
);
|
|
}
|
|
|
|
test("clicking a row in the planets table opens the map", async ({ page }) => {
|
|
await mockGateway(page);
|
|
await bootSession(page);
|
|
await page.goto("/");
|
|
await page.waitForFunction(() => window.__galaxyNav !== undefined);
|
|
await page.evaluate(
|
|
(id) =>
|
|
window.__galaxyNav!.enterGame(id, "table", {
|
|
tableEntity: "planets",
|
|
}),
|
|
GAME_ID,
|
|
);
|
|
|
|
const table = page.getByTestId("planets-table");
|
|
await expect(table).toBeVisible();
|
|
const rows = page.getByTestId("planets-row");
|
|
await expect(rows).toHaveCount(3);
|
|
|
|
// Click the foreign planet — the data-* stamps let the spec assert
|
|
// against a deterministic row regardless of default sort.
|
|
await page
|
|
.locator('[data-testid="planets-row"][data-number="2"]')
|
|
.click();
|
|
|
|
await expect(page.getByTestId("active-view-map")).toBeVisible();
|
|
});
|