f7109af55c
Two follow-up fixes after the initial Phase 19 landing:
1. The IncomingGroup dashed trajectory was drawn between raw
(x1, y1) and (x2, y2) world coordinates. On torus wrap mode
this took the long way around when origin and destination
sat near opposite seams. The line now picks endpoints via
`torusShortestDelta` so the segment crosses the seam when
that's the shorter visual path. The interpolated clickable
point follows the same unwrapped vector. The same helper
fixes the in-hyperspace position for local / foreign groups.
2. On-planet local and foreign groups previously rendered as
small offset points next to every populated planet, which
turned the canvas into noise as soon as a player held more
than a handful of planets. The map no longer renders any
in-orbit group; the planet inspector grows a compact
"stationed ship groups" subsection
(`lib/inspectors/planet/ship-groups.svelte`) that lists
each in-orbit group as a row of `<race> · <class> · <count>
ships · <mass>`. Race attribution: LocalGroup → the player's
race, OtherGroup on a foreign-owned planet → the planet's
owner, OtherGroup elsewhere → "foreign" placeholder. Rows
are non-interactive in Phase 19; Phase 21+ will deep-link
into the ship-groups table view with a (planet, race) filter.
Tests:
- `state-binding-groups.test.ts` swaps the on-planet rendering
expectation for the new "no map primitive" rule, and adds a
regression that asserts the incoming line crosses the torus
seam via `torusShortestDelta`.
- new `inspector-planet-ship-groups.test.ts` covers row
composition, the destination-mismatch filter, the
in-hyperspace exclusion, the foreign-planet owner fallback,
and the empty-state collapse.
- `inspector-planet.test.ts` and `inspector-ship-group.spec.ts`
pick up the new prop chain (`localShipGroups`,
`otherShipGroups`, `localRace`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
160 lines
3.9 KiB
TypeScript
160 lines
3.9 KiB
TypeScript
// Phase 19 end-to-end smoke against the synthetic-report path. Loads
|
|
// a hand-crafted JSON with a Tancordia-style mix of planets and ship
|
|
// groups through the DEV-only file picker on `/lobby`, lets the
|
|
// in-game shell layout swap into synthetic mode, and asserts the map
|
|
// canvas mounts. Detailed click / hit-test fidelity for ship-group
|
|
// variants lives in the unit tests (`tests/state-binding-groups.test.ts`
|
|
// and `tests/inspector-ship-group.test.ts`); this spec catches the
|
|
// glue: lobby loader → in-memory registry → layout bypass → renderer
|
|
// boot.
|
|
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
// Seed an authenticated session through `/__debug/store` so the
|
|
// root layout's redirect-to-login guard passes. The synthetic flow
|
|
// itself does not talk to the gateway, but the session check still
|
|
// runs at every navigation. The full `__galaxyDebug` shape is
|
|
// declared globally in `tests/e2e/storage-keypair-persistence.spec.ts`;
|
|
// here we only need `loadSession` + `setDeviceSessionId`.
|
|
async function seedSession(page: Page): Promise<void> {
|
|
await page.goto("/__debug/store");
|
|
await expect(page.getByTestId("debug-store-ready")).toBeVisible();
|
|
await page.waitForFunction(
|
|
() => (window as unknown as { __galaxyDebug?: { ready?: boolean } }).__galaxyDebug?.ready === true,
|
|
);
|
|
await page.evaluate(async () => {
|
|
const debug = (window as unknown as {
|
|
__galaxyDebug: {
|
|
loadSession(): Promise<unknown>;
|
|
setDeviceSessionId(id: string): Promise<void>;
|
|
};
|
|
}).__galaxyDebug;
|
|
await debug.loadSession();
|
|
await debug.setDeviceSessionId("phase-19-synthetic-session");
|
|
});
|
|
}
|
|
|
|
const SYNTHETIC_REPORT_FIXTURE = {
|
|
turn: 39,
|
|
mapWidth: 200,
|
|
mapHeight: 200,
|
|
mapPlanets: 4,
|
|
race: "Earthlings",
|
|
player: [
|
|
{
|
|
name: "Earthlings",
|
|
drive: 5,
|
|
weapons: 3,
|
|
shields: 2,
|
|
cargo: 1,
|
|
population: 1000,
|
|
industry: 1000,
|
|
planets: 2,
|
|
relation: "-",
|
|
votes: 5,
|
|
extinct: false,
|
|
},
|
|
],
|
|
localPlanet: [
|
|
{
|
|
number: 1,
|
|
name: "Earth",
|
|
x: 50,
|
|
y: 100,
|
|
size: 1000,
|
|
population: 1000,
|
|
industry: 1000,
|
|
resources: 10,
|
|
production: "Capital",
|
|
capital: 0,
|
|
material: 0,
|
|
colonists: 100,
|
|
freeIndustry: 1000,
|
|
},
|
|
{
|
|
number: 2,
|
|
name: "Mars",
|
|
x: 150,
|
|
y: 100,
|
|
size: 500,
|
|
population: 500,
|
|
industry: 500,
|
|
resources: 5,
|
|
production: "Capital",
|
|
capital: 0,
|
|
material: 0,
|
|
colonists: 50,
|
|
freeIndustry: 500,
|
|
},
|
|
],
|
|
otherPlanet: [],
|
|
uninhabitedPlanet: [],
|
|
unidentifiedPlanet: [
|
|
{ number: 3, x: 50, y: 50 },
|
|
{ number: 4, x: 150, y: 50 },
|
|
],
|
|
localShipClass: [
|
|
{
|
|
name: "Frontier",
|
|
drive: 5,
|
|
armament: 0,
|
|
weapons: 0,
|
|
shields: 0,
|
|
cargo: 1,
|
|
mass: 12,
|
|
},
|
|
],
|
|
localGroup: [
|
|
{
|
|
id: "11111111-2222-3333-4444-555555555555",
|
|
number: 2,
|
|
class: "Frontier",
|
|
tech: { drive: 5, weapons: 0, shields: 0, cargo: 1 },
|
|
cargo: "-",
|
|
load: 0,
|
|
destination: 1,
|
|
speed: 0,
|
|
mass: 12,
|
|
state: "In_Orbit",
|
|
},
|
|
],
|
|
otherGroup: [],
|
|
incomingGroup: [
|
|
{
|
|
origin: 4,
|
|
destination: 1,
|
|
distance: 50,
|
|
speed: 25,
|
|
mass: 4,
|
|
},
|
|
],
|
|
unidentifiedGroup: [],
|
|
localFleet: [],
|
|
};
|
|
|
|
test("synthetic-report loader navigates from lobby to map and renders", async ({
|
|
page,
|
|
}) => {
|
|
await seedSession(page);
|
|
await page.goto("/lobby");
|
|
await expect(page.getByTestId("lobby-synthetic-section")).toBeVisible();
|
|
|
|
const file = page.getByTestId("lobby-synthetic-file");
|
|
await file.setInputFiles({
|
|
name: "phase19.json",
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from(JSON.stringify(SYNTHETIC_REPORT_FIXTURE)),
|
|
});
|
|
|
|
await page.waitForURL(/\/games\/synthetic-[^/]+\/map$/, {
|
|
timeout: 5_000,
|
|
});
|
|
|
|
// The renderer canvas mounts inside the active-view host. Even if
|
|
// the WebGL/WebGPU backend is unavailable in CI, the layout still
|
|
// reaches `ready` once the report is loaded — the assertion is
|
|
// gentle on purpose so the spec doesn't flake on headless renders.
|
|
const canvas = page.locator("canvas");
|
|
await expect(canvas.first()).toBeVisible({ timeout: 10_000 });
|
|
});
|