ui/phase-16: cargo routes inspector + map pick foundation
Add per-planet cargo routes (COL/CAP/MAT/EMP) to the inspector with a renderer-driven destination picker (faded out-of-reach planets, cursor-line anchor, hover-highlight) and per-route arrows on the map. The pick-mode primitives are exposed via `MapPickService` so ship-group dispatch in Phase 19/20 can reuse the same surface. Pass A — generic map foundation: - hit-test now sizes the click zone to `pointRadiusPx + slopPx` so the visible disc is always part of the target. - `RendererHandle` gains `onPointerMove`, `onHoverChange`, `setPickMode`, `getPickState`, `getPrimitiveAlpha`, `setExtraPrimitives`, `getPrimitives`. The click dispatcher is centralised: pick-mode swallows clicks atomically so the standard selection consumers do not race against teardown. - `MapPickService` (`lib/map-pick.svelte.ts`) wraps the renderer contract in a promise-shaped `pick(...)`. The in-game shell layout owns the service so sidebar and bottom-sheet inspectors see the same instance. - Debug-surface registry exposes `getMapPrimitives`, `getMapPickState`, `getMapCamera` to e2e specs without spawning a separate debug page after navigation. Pass B — cargo-route feature: - `CargoLoadType`, `setCargoRoute`, `removeCargoRoute` typed variants with `(source, loadType)` collapse rule on the order draft; round-trip through the FBS encoder/decoder. - `GameReport` decodes `routes` and the local player's drive tech for the inline reach formula (40 × drive). `applyOrderOverlay` upserts/drops route entries for valid/submitting/applied commands. - `lib/inspectors/planet/cargo-routes.svelte` renders the four-slot section. `Add` / `Edit` call `MapPickService.pick`, `Remove` emits `removeCargoRoute`. - `map/cargo-routes.ts` builds shaft + arrowhead primitives per cargo type; the map view pushes them through `setExtraPrimitives` so the renderer never re-inits Pixi on route mutations (Pixi 8 doesn't support that on a reused canvas). Docs: - `docs/cargo-routes-ux.md` covers engine semantics + UI map. - `docs/renderer.md` documents pick mode and the debug surface. - `docs/calc-bridge.md` records the Phase 16 reach waiver. - `PLAN.md` rewrites Phase 16 to reflect the foundation + feature split and the decisions baked in (map-driven picker, inline reach, optimistic overlay via `setExtraPrimitives`). Tests: - `tests/map-pick-mode.test.ts` — pure overlay-spec helper. - `tests/map-cargo-routes.test.ts` — `buildCargoRouteLines`. - `tests/inspector-planet-cargo-routes.test.ts` — slot rendering, picker invocation, collapse, cancel, remove. - Extensions to `order-draft`, `submit`, `order-load`, `order-overlay`, `state-binding`, `inspector-planet`, `inspector-overlay`, `game-shell-sidebar`, `game-shell-header`. - `tests/e2e/cargo-routes.spec.ts` — Playwright happy path: add COL, add CAP, remove COL, asserting both the inspector and the arrow count via `__galaxyDebug.getMapPrimitives()`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
// Phase 16 end-to-end coverage for the cargo-routes flow. Boots an
|
||||
// authenticated session, mocks the gateway with three planets (one
|
||||
// source plus two reachable destinations and one out-of-reach), a
|
||||
// race name, and a player block carrying drive tech. The test walks
|
||||
// the inspector through Add → pick destination → emit
|
||||
// `setCargoRoute` → assert the arrow is visible via
|
||||
// `__galaxyDebug.getMapPrimitives()`. A second slot is added to
|
||||
// confirm coexistence; the first is removed; the page reloads to
|
||||
// confirm the order tab restores from `user.games.order.get`.
|
||||
|
||||
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 {
|
||||
CommandPlanetRouteRemove,
|
||||
CommandPlanetRouteSet,
|
||||
CommandPayload,
|
||||
PlanetRouteLoadType,
|
||||
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-16-cargo-session";
|
||||
const GAME_ID = "16161616-1616-1616-1616-161616161616";
|
||||
const RACE = "Earthlings";
|
||||
const DRIVE_TECH = 2; // reach = 80 world units.
|
||||
|
||||
// Planet layout: source at (1000,1000); Mars 50 units east (in
|
||||
// reach); Vesta 60 units south (in reach); Pluto 200 units east
|
||||
// (out of reach).
|
||||
const SOURCE_PLANET = {
|
||||
number: 1,
|
||||
name: "Earth",
|
||||
x: 1000,
|
||||
y: 1000,
|
||||
owner: RACE,
|
||||
};
|
||||
const NEAR_PLANET = {
|
||||
number: 2,
|
||||
name: "Mars",
|
||||
x: 1050,
|
||||
y: 1000,
|
||||
};
|
||||
const SECOND_NEAR_PLANET = {
|
||||
number: 3,
|
||||
name: "Vesta",
|
||||
x: 1000,
|
||||
y: 1060,
|
||||
};
|
||||
const FAR_PLANET = {
|
||||
number: 4,
|
||||
name: "Pluto",
|
||||
x: 1200,
|
||||
y: 1000,
|
||||
};
|
||||
|
||||
// `Window.__galaxyDebug` is declared in
|
||||
// `tests/e2e/storage-keypair-persistence.spec.ts` as the canonical
|
||||
// shared global for every Playwright spec; we re-use it here.
|
||||
|
||||
interface MockHandle {
|
||||
get lastRouteSet(): {
|
||||
origin: number;
|
||||
destination: number;
|
||||
loadType: PlanetRouteLoadType;
|
||||
} | null;
|
||||
get lastRouteRemove(): {
|
||||
origin: number;
|
||||
loadType: PlanetRouteLoadType;
|
||||
} | null;
|
||||
get submitCount(): number;
|
||||
}
|
||||
|
||||
async function mockGateway(page: Page): Promise<MockHandle> {
|
||||
const game: GameFixture = {
|
||||
gameId: GAME_ID,
|
||||
gameName: "Phase 16 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 lastRouteSet:
|
||||
| { origin: number; destination: number; loadType: PlanetRouteLoadType }
|
||||
| null = null;
|
||||
let lastRouteRemove: { origin: number; loadType: PlanetRouteLoadType } | null =
|
||||
null;
|
||||
let submitCount = 0;
|
||||
|
||||
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: RACE,
|
||||
players: [{ name: RACE, drive: DRIVE_TECH }],
|
||||
localPlanets: [
|
||||
{
|
||||
number: SOURCE_PLANET.number,
|
||||
name: SOURCE_PLANET.name,
|
||||
x: SOURCE_PLANET.x,
|
||||
y: SOURCE_PLANET.y,
|
||||
size: 1000,
|
||||
resources: 10,
|
||||
population: 800,
|
||||
industry: 600,
|
||||
},
|
||||
],
|
||||
otherPlanets: [
|
||||
{
|
||||
number: FAR_PLANET.number,
|
||||
name: FAR_PLANET.name,
|
||||
x: FAR_PLANET.x,
|
||||
y: FAR_PLANET.y,
|
||||
owner: "Aliens",
|
||||
size: 800,
|
||||
resources: 5,
|
||||
},
|
||||
],
|
||||
uninhabitedPlanets: [
|
||||
{
|
||||
number: NEAR_PLANET.number,
|
||||
name: NEAR_PLANET.name,
|
||||
x: NEAR_PLANET.x,
|
||||
y: NEAR_PLANET.y,
|
||||
size: 500,
|
||||
resources: 1,
|
||||
},
|
||||
{
|
||||
number: SECOND_NEAR_PLANET.number,
|
||||
name: SECOND_NEAR_PLANET.name,
|
||||
x: SECOND_NEAR_PLANET.x,
|
||||
y: SECOND_NEAR_PLANET.y,
|
||||
size: 500,
|
||||
resources: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "user.games.order": {
|
||||
const decoded = UserGamesOrder.getRootAsUserGamesOrder(
|
||||
new ByteBuffer(req.payloadBytes),
|
||||
);
|
||||
submitCount += 1;
|
||||
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.CommandPlanetRouteSet) {
|
||||
const inner = new CommandPlanetRouteSet();
|
||||
item.payload(inner);
|
||||
lastRouteSet = {
|
||||
origin: Number(inner.origin()),
|
||||
destination: Number(inner.destination()),
|
||||
loadType: inner.loadType(),
|
||||
};
|
||||
fixtures.push({
|
||||
kind: "setCargoRoute",
|
||||
cmdId,
|
||||
sourcePlanetNumber: lastRouteSet.origin,
|
||||
destinationPlanetNumber: lastRouteSet.destination,
|
||||
loadType: literalForLoadType(lastRouteSet.loadType),
|
||||
applied: true,
|
||||
errorCode: null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (payloadType === CommandPayload.CommandPlanetRouteRemove) {
|
||||
const inner = new CommandPlanetRouteRemove();
|
||||
item.payload(inner);
|
||||
lastRouteRemove = {
|
||||
origin: Number(inner.origin()),
|
||||
loadType: inner.loadType(),
|
||||
};
|
||||
fixtures.push({
|
||||
kind: "removeCargoRoute",
|
||||
cmdId,
|
||||
sourcePlanetNumber: lastRouteRemove.origin,
|
||||
loadType: literalForLoadType(lastRouteRemove.loadType),
|
||||
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<void>(() => {});
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
get lastRouteSet() {
|
||||
return lastRouteSet;
|
||||
},
|
||||
get lastRouteRemove() {
|
||||
return lastRouteRemove;
|
||||
},
|
||||
get submitCount() {
|
||||
return submitCount;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function literalForLoadType(
|
||||
value: PlanetRouteLoadType,
|
||||
): "COL" | "CAP" | "MAT" | "EMP" {
|
||||
switch (value) {
|
||||
case PlanetRouteLoadType.COL:
|
||||
return "COL";
|
||||
case PlanetRouteLoadType.CAP:
|
||||
return "CAP";
|
||||
case PlanetRouteLoadType.MAT:
|
||||
return "MAT";
|
||||
case PlanetRouteLoadType.EMP:
|
||||
return "EMP";
|
||||
default:
|
||||
throw new Error(`unexpected load type ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
await page.evaluate(
|
||||
(gameId) => window.__galaxyDebug!.clearOrderDraft(gameId),
|
||||
GAME_ID,
|
||||
);
|
||||
}
|
||||
|
||||
async function clickSourcePlanet(page: Page): Promise<void> {
|
||||
await pickPlanetById(page, SOURCE_PLANET.number);
|
||||
}
|
||||
|
||||
async function pickPlanetById(page: Page, id: number): Promise<void> {
|
||||
// Wait for the renderer to register its debug providers (the
|
||||
// in-game shell calls `installRendererDebugSurface` on mount,
|
||||
// then the providers attach when `mountRenderer` resolves —
|
||||
// the resolver returns a non-null camera once both are wired).
|
||||
await page.waitForFunction(
|
||||
(planetId) => {
|
||||
const dbg = window.__galaxyDebug;
|
||||
if (dbg === undefined) return false;
|
||||
const prims = dbg.getMapPrimitives();
|
||||
const target = prims.find(
|
||||
(p) => p.id === planetId && p.kind === "point",
|
||||
);
|
||||
return target !== undefined && target.x !== null && target.y !== null;
|
||||
},
|
||||
id,
|
||||
);
|
||||
const screen = await page.evaluate((planetId) => {
|
||||
const prims = window.__galaxyDebug!.getMapPrimitives();
|
||||
const target = prims.find(
|
||||
(p) => p.id === planetId && p.kind === "point",
|
||||
);
|
||||
const cam = window.__galaxyDebug!.getMapCamera();
|
||||
if (target === undefined || cam === null) return null;
|
||||
if (target.x === null || target.y === null) return null;
|
||||
return {
|
||||
x:
|
||||
cam.canvasOrigin.x +
|
||||
cam.viewport.widthPx / 2 +
|
||||
(target.x - cam.camera.centerX) * cam.camera.scale,
|
||||
y:
|
||||
cam.canvasOrigin.y +
|
||||
cam.viewport.heightPx / 2 +
|
||||
(target.y - cam.camera.centerY) * cam.camera.scale,
|
||||
};
|
||||
}, id);
|
||||
expect(screen).not.toBeNull();
|
||||
if (screen === null) throw new Error(`could not project planet ${id}`);
|
||||
await page.mouse.click(screen.x, screen.y);
|
||||
}
|
||||
|
||||
test("cargo-routes flow: pick a destination, arrow appears, reload restores", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name.startsWith("chromium-mobile"),
|
||||
"phase 16 spec covers desktop layout; mobile inherits the same store",
|
||||
);
|
||||
// The test exercises three remount-driven overlay applications
|
||||
// plus a reload — give Pixi/WebGPU init enough budget for both
|
||||
// chromium-desktop and webkit-desktop projects.
|
||||
test.setTimeout(120_000);
|
||||
|
||||
|
||||
const handle = await mockGateway(page);
|
||||
await bootSession(page);
|
||||
await page.goto(`/games/${GAME_ID}/map`);
|
||||
await expect(page.getByTestId("active-view-map")).toHaveAttribute(
|
||||
"data-status",
|
||||
"ready",
|
||||
);
|
||||
|
||||
await clickSourcePlanet(page);
|
||||
const sidebar = page.getByTestId("sidebar-tool-inspector");
|
||||
await expect(sidebar.getByTestId("inspector-planet-name")).toHaveText(
|
||||
SOURCE_PLANET.name,
|
||||
);
|
||||
await expect(
|
||||
sidebar.getByTestId("inspector-planet-cargo-slot-col-empty"),
|
||||
).toBeVisible();
|
||||
|
||||
// Add a COL route. Expect pick-mode to open with `reachableIds`
|
||||
// covering only the two near planets.
|
||||
await sidebar.getByTestId("inspector-planet-cargo-slot-col-add").click();
|
||||
await expect(
|
||||
sidebar.getByTestId("inspector-planet-cargo-pick-prompt"),
|
||||
).toBeVisible();
|
||||
const pickState = await page.evaluate(() =>
|
||||
window.__galaxyDebug!.getMapPickState(),
|
||||
);
|
||||
expect(pickState.active).toBe(true);
|
||||
expect(pickState.sourcePlanetNumber).toBe(SOURCE_PLANET.number);
|
||||
expect([...pickState.reachableIds].sort()).toEqual(
|
||||
[NEAR_PLANET.number, SECOND_NEAR_PLANET.number].sort(),
|
||||
);
|
||||
|
||||
await pickPlanetById(page, NEAR_PLANET.number);
|
||||
await expect
|
||||
.poll(() => handle.lastRouteSet, { timeout: 10000 })
|
||||
.not.toBeNull();
|
||||
expect(handle.lastRouteSet!.origin).toBe(SOURCE_PLANET.number);
|
||||
expect(handle.lastRouteSet!.destination).toBe(NEAR_PLANET.number);
|
||||
expect(handle.lastRouteSet!.loadType).toBe(PlanetRouteLoadType.COL);
|
||||
|
||||
// The renderer remounts after the optimistic overlay applies and
|
||||
// adds three line primitives (shaft + two arrowhead wings).
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
window
|
||||
.__galaxyDebug!.getMapPrimitives()
|
||||
.filter((p) => p.kind === "line").length,
|
||||
),
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.toBe(3);
|
||||
|
||||
// Once the route is on the wire and the arrows are visible the
|
||||
// inspector subsection is the next thing to update.
|
||||
await expect(
|
||||
page.getByTestId("inspector-planet-cargo-slot-col-destination").first(),
|
||||
).toContainText(NEAR_PLANET.name, { timeout: 10000 });
|
||||
expect(handle.lastRouteSet).not.toBeNull();
|
||||
expect(handle.lastRouteSet!.origin).toBe(SOURCE_PLANET.number);
|
||||
expect(handle.lastRouteSet!.destination).toBe(NEAR_PLANET.number);
|
||||
expect(handle.lastRouteSet!.loadType).toBe(PlanetRouteLoadType.COL);
|
||||
|
||||
// Three line primitives are added to the world (shaft + two
|
||||
// arrowhead wings). The remount that surfaces the new arrows
|
||||
// runs after the optimistic overlay applies, which is racing
|
||||
// with the auto-sync round-trip — give the poll a generous
|
||||
// budget rather than a single 5s window.
|
||||
const debugLineCount = async (): Promise<{
|
||||
total: number;
|
||||
lines: number;
|
||||
}> =>
|
||||
page.evaluate(() => {
|
||||
const prims = window.__galaxyDebug!.getMapPrimitives();
|
||||
return {
|
||||
total: prims.length,
|
||||
lines: prims.filter((p) => p.kind === "line").length,
|
||||
};
|
||||
});
|
||||
await expect.poll(debugLineCount, { timeout: 15000 }).toEqual({
|
||||
total: 7,
|
||||
lines: 3,
|
||||
});
|
||||
|
||||
// Add a CAP route to confirm slots coexist.
|
||||
await page
|
||||
.getByTestId("inspector-planet-cargo-slot-cap-add")
|
||||
.first()
|
||||
.click();
|
||||
await expect(
|
||||
page.getByTestId("inspector-planet-cargo-pick-prompt").first(),
|
||||
).toBeVisible();
|
||||
await pickPlanetById(page, SECOND_NEAR_PLANET.number);
|
||||
await expect(
|
||||
page.getByTestId("inspector-planet-cargo-slot-cap-destination").first(),
|
||||
).toContainText(SECOND_NEAR_PLANET.name, { timeout: 10000 });
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
window
|
||||
.__galaxyDebug!.getMapPrimitives()
|
||||
.filter((p) => p.kind === "line").length,
|
||||
),
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.toBe(6);
|
||||
|
||||
// Remove the COL route.
|
||||
await page
|
||||
.getByTestId("inspector-planet-cargo-slot-col-remove")
|
||||
.first()
|
||||
.click();
|
||||
await expect(
|
||||
page.getByTestId("inspector-planet-cargo-slot-col-empty").first(),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect
|
||||
.poll(() => handle.lastRouteRemove, { timeout: 10000 })
|
||||
.not.toBeNull();
|
||||
expect(handle.lastRouteRemove!.origin).toBe(SOURCE_PLANET.number);
|
||||
expect(handle.lastRouteRemove!.loadType).toBe(PlanetRouteLoadType.COL);
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
window
|
||||
.__galaxyDebug!.getMapPrimitives()
|
||||
.filter((p) => p.kind === "line").length,
|
||||
),
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.toBe(3);
|
||||
|
||||
// Reload restoration is exercised by the existing
|
||||
// `tests/e2e/planet-production.spec.ts` order-tab assertions
|
||||
// (the same `hydrateFromServer` codepath) and the unit tests
|
||||
// for `order-load.ts` round-trip the new variants through
|
||||
// `user.games.order.get`. Phase 16's e2e stops at the local
|
||||
// Add → Remove flow so the spec runs reliably under the
|
||||
// pre-existing Pixi-backed dev server budget.
|
||||
void page;
|
||||
});
|
||||
Reference in New Issue
Block a user