fix(ui-e2e): Phase 29 map-toggles spec passes across all four projects
Tests · UI / test (push) Failing after 10m52s

Three independent bugs in `tests/e2e/map-toggles.spec.ts` made the
fresh-Phase-29 suite red on CI #216:

1. `visiblePlanets` filtered on `p.id < 1_000_000`, which JS interprets
   in signed space — high-bit-prefix primitives (cargo route 0x80…,
   battle 0xa0…, bombing 0xc0…) are stored as negative Numbers and
   leaked into the planet list. Filter switched to a `0 < id < 1e7`
   window that matches the engine planet-number range exactly.
2. The `visibleHighBitCount` helper now ToUint32-converts the id
   before masking so the bitmask comparison works regardless of
   whether the id is stored as positive or negative.
3. The fog and wrap-mode tests read the renderer state synchronously
   after the click — the Svelte effect re-runs asynchronously, so the
   tests saw stale state. Both now `waitForFunction` on the canonical
   "settled" signal: empty fog circles for the fog flip, and a new
   `getMapMode()` debug accessor for the wrap-mode remount.

Renderer side: registers a `MapModeProvider` next to the existing
camera / fog providers and exposes `getMapMode()` through the debug
surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-19 22:02:15 +02:00
parent 2bd1b54936
commit 2528d63b51
5 changed files with 108 additions and 67 deletions
@@ -69,6 +69,7 @@ preference the store already manages.
installRendererDebugSurface,
registerMapCameraProvider,
registerMapFogProvider,
registerMapModeProvider,
registerMapPickStateProvider,
registerMapPrimitivesProvider,
type MapCameraSnapshot,
@@ -505,11 +506,15 @@ preference the store already manages.
const detachFog = registerMapFogProvider(() => ({
circles: currentFogCircles.map((c) => ({ ...c })),
}) satisfies MapFogSnapshot);
const detachMode = registerMapModeProvider(() =>
handle === null ? null : handle.getMode(),
);
detachDebugProviders = (): void => {
detachPrim();
detachPick();
detachCamera();
detachFog();
detachMode();
};
mountedTurn = report.turn;
mountedGameId = targetGameId;
+31 -1
View File
@@ -10,7 +10,7 @@
// lazily on every read so the returned data always reflects the
// current frame, not the value at registration time.
import type { Primitive, PrimitiveID } from "../map/world";
import type { Primitive, PrimitiveID, WrapMode } from "../map/world";
/** Snapshot returned by `getMapPrimitives()`. The renderer applies
* pick-mode dimming via the underlying `Graphics.alpha`, so the
@@ -73,11 +73,13 @@ type PrimitivesProvider = () => readonly MapPrimitiveSnapshot[];
type PickStateProvider = () => MapPickStateSnapshot;
type CameraProvider = () => MapCameraSnapshot | null;
type FogProvider = () => MapFogSnapshot;
type ModeProvider = () => WrapMode | null;
let primitivesProvider: PrimitivesProvider | null = null;
let pickStateProvider: PickStateProvider | null = null;
let cameraProvider: CameraProvider | null = null;
let fogProvider: FogProvider | null = null;
let modeProvider: ModeProvider | null = null;
/**
* registerMapPrimitivesProvider attaches a provider that yields the
@@ -134,6 +136,22 @@ export function registerMapFogProvider(provider: FogProvider): () => void {
};
}
/**
* registerMapModeProvider attaches a provider that yields the
* renderer's current `WrapMode` ('torus' or 'no-wrap'). Used by
* Phase 29 e2e specs to await the renderer remount after a
* wrap-mode flip — `getMapCamera()` alone is not a reliable signal
* because the same camera survives across a remount, so the spec
* watches the mode flip instead. Same idempotent semantics as the
* other providers.
*/
export function registerMapModeProvider(provider: ModeProvider): () => void {
modeProvider = provider;
return () => {
if (modeProvider === provider) modeProvider = null;
};
}
const EMPTY_PICK_STATE: MapPickStateSnapshot = {
active: false,
sourcePlanetNumber: null,
@@ -166,12 +184,20 @@ export function getMapFog(): MapFogSnapshot {
return fogProvider?.() ?? { circles: [] };
}
/** Pulls the renderer's current `WrapMode`. Returns `null` when no
* map view is mounted (the surface is queried during navigation or
* before the first render). */
export function getMapMode(): WrapMode | null {
return modeProvider?.() ?? null;
}
interface RendererDebugWindow {
__galaxyDebug?: {
getMapPrimitives?: () => readonly MapPrimitiveSnapshot[];
getMapPickState?: () => MapPickStateSnapshot;
getMapCamera?: () => MapCameraSnapshot | null;
getMapFog?: () => MapFogSnapshot;
getMapMode?: () => WrapMode | null;
[key: string]: unknown;
};
}
@@ -195,6 +221,7 @@ export function installRendererDebugSurface(): () => void {
getMapPickState,
getMapCamera,
getMapFog,
getMapMode,
};
win.__galaxyDebug = surface;
return (): void => {
@@ -215,5 +242,8 @@ export function installRendererDebugSurface(): () => void {
if (current.getMapFog === getMapFog) {
delete current.getMapFog;
}
if (current.getMapMode === getMapMode) {
delete current.getMapMode;
}
};
}
@@ -10,6 +10,7 @@
import {
getMapCamera,
getMapFog,
getMapMode,
getMapPickState,
getMapPrimitives,
type MapCameraSnapshot,
@@ -17,6 +18,7 @@
type MapPickStateSnapshot,
type MapPrimitiveSnapshot,
} from "../../../lib/debug-surface.svelte";
import type { WrapMode } from "../../../map/world";
interface DebugSnapshot {
publicKey: number[];
@@ -42,6 +44,7 @@
getMapPickState(): MapPickStateSnapshot;
getMapCamera(): MapCameraSnapshot | null;
getMapFog(): MapFogSnapshot;
getMapMode(): WrapMode | null;
}
type DebugWindow = typeof globalThis & { __galaxyDebug?: DebugSurface };
@@ -142,6 +145,9 @@
getMapFog() {
return getMapFog();
},
getMapMode() {
return getMapMode();
},
};
(window as DebugWindow).__galaxyDebug = surface;
ready = true;