Files
galaxy-game/ui/frontend/tests/map-toggles-state.test.ts
T
Ilia Denisov 2bd1b54936
Tests · Go / test (push) Successful in 2m31s
Tests · UI / test (push) Failing after 8m7s
feat(ui): Phase 29 map visibility toggles
Adds the gear-icon popover on the map view with per-game persistence
of every category toggle plus the wrap-mode radio. Hide-by-id and
visibility-fog facilities land on the renderer so every flip applies
within one frame without a Pixi remount; the wrap-mode toggle keeps
its existing remount + camera-preserve path. A new server-side turn
force-resets every flag to defaults so a hidden category never makes
the player miss the next turn's news.

Also fixes the FligthDistance → FlightDistance typo in pkg/calc/race.go
(plus the single Go caller); the TS side keeps duplicating the formula
until a race-level WASM bridge lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:33:53 +02:00

214 lines
7.0 KiB
TypeScript

// Phase 29 persistence + new-turn reset coverage for the
// `GameStateStore.mapToggles` rune. The tests drive the store
// against a real `fake-indexeddb`-backed Cache (the same harness
// `game-state.test.ts` uses) so the JSON-blob round-trip and the
// stale-`lastResetTurn` branch are exercised end-to-end.
import "@testing-library/jest-dom/vitest";
import "fake-indexeddb/auto";
import {
afterEach,
beforeEach,
describe,
expect,
test,
vi,
} from "vitest";
import { Builder } from "flatbuffers";
import {
DEFAULT_MAP_TOGGLES,
GameStateStore,
type MapToggles,
} from "../src/lib/game-state.svelte";
import type { GalaxyClient } from "../src/api/galaxy-client";
import type { Cache } from "../src/platform/store/index";
import { IDBCache } from "../src/platform/store/idb-cache";
import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb";
import type { IDBPDatabase } from "idb";
import { Report } from "../src/proto/galaxy/fbs/report";
const listMyGamesSpy = vi.fn();
vi.mock("../src/api/lobby", async () => {
const actual = await vi.importActual<typeof import("../src/api/lobby")>(
"../src/api/lobby",
);
return {
...actual,
listMyGames: (...args: unknown[]) => listMyGamesSpy(...args),
};
});
let db: IDBPDatabase<GalaxyDB>;
let dbName: string;
let cache: Cache;
beforeEach(async () => {
dbName = `galaxy-map-toggles-test-${crypto.randomUUID()}`;
db = await openGalaxyDB(dbName);
cache = new IDBCache(db);
listMyGamesSpy.mockReset();
});
afterEach(async () => {
db.close();
await new Promise<void>((resolve) => {
const req = indexedDB.deleteDatabase(dbName);
req.onsuccess = () => resolve();
req.onerror = () => resolve();
req.onblocked = () => resolve();
});
});
const GAME_ID = "11111111-2222-3333-4444-555555555555";
function makeGameSummary(currentTurn: number) {
return {
gameId: GAME_ID,
gameName: "Test Game",
gameType: "private",
status: "running",
ownerUserId: "owner-1",
minPlayers: 2,
maxPlayers: 8,
enrollmentEndsAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
currentTurn,
};
}
function buildReportPayload(turn: number): Uint8Array {
const builder = new Builder(64);
Report.startReport(builder);
Report.addTurn(builder, BigInt(turn));
Report.addWidth(builder, 4000);
Report.addHeight(builder, 4000);
Report.addPlanetCount(builder, 0);
builder.finish(Report.endReport(builder));
return builder.asUint8Array();
}
function makeFakeClient(turn: number): GalaxyClient {
return {
executeCommand: async () => ({
resultCode: "ok",
payloadBytes: buildReportPayload(turn),
}),
} as unknown as GalaxyClient;
}
describe("GameStateStore.mapToggles persistence", () => {
test("defaults apply when no blob is persisted", async () => {
listMyGamesSpy.mockResolvedValue([makeGameSummary(3)]);
const store = new GameStateStore();
await store.init({ client: makeFakeClient(3), cache, gameId: GAME_ID });
expect(store.mapToggles).toEqual(DEFAULT_MAP_TOGGLES);
store.dispose();
});
test("setMapToggle round-trips through Cache across instances", async () => {
listMyGamesSpy.mockResolvedValue([makeGameSummary(3)]);
const a = new GameStateStore();
await a.init({ client: makeFakeClient(3), cache, gameId: GAME_ID });
await a.setMapToggle("hyperspaceGroups", false);
await a.setMapToggle("battleMarkers", false);
await a.setMapToggle("visibilityFog", false);
a.dispose();
listMyGamesSpy.mockResolvedValue([makeGameSummary(3)]);
const b = new GameStateStore();
await b.init({ client: makeFakeClient(3), cache, gameId: GAME_ID });
expect(b.mapToggles.hyperspaceGroups).toBe(false);
expect(b.mapToggles.battleMarkers).toBe(false);
expect(b.mapToggles.visibilityFog).toBe(false);
// Untouched flags retain defaults.
expect(b.mapToggles.bombingMarkers).toBe(true);
b.dispose();
});
test("missing fields in a persisted blob fall back to defaults", async () => {
// Simulate an older client persisting a partial blob — only
// `hyperspaceGroups` is set, every other field must inherit
// the current default.
await cache.put("game-map-toggles", GAME_ID, {
toggles: { hyperspaceGroups: false } as Partial<MapToggles>,
lastResetTurn: 5,
});
listMyGamesSpy.mockResolvedValue([makeGameSummary(5)]);
const store = new GameStateStore();
await store.init({ client: makeFakeClient(5), cache, gameId: GAME_ID });
expect(store.mapToggles.hyperspaceGroups).toBe(false);
expect(store.mapToggles.battleMarkers).toBe(true);
expect(store.mapToggles.bombingMarkers).toBe(true);
expect(store.mapToggles.visibilityFog).toBe(true);
store.dispose();
});
});
describe("GameStateStore.mapToggles new-turn reset", () => {
test("a server turn newer than lastResetTurn resets every flag", async () => {
await cache.put("game-map-toggles", GAME_ID, {
toggles: {
...DEFAULT_MAP_TOGGLES,
hyperspaceGroups: false,
battleMarkers: false,
visibilityFog: false,
},
lastResetTurn: 4,
});
listMyGamesSpy.mockResolvedValue([makeGameSummary(5)]);
const store = new GameStateStore();
await store.init({ client: makeFakeClient(5), cache, gameId: GAME_ID });
expect(store.mapToggles).toEqual(DEFAULT_MAP_TOGGLES);
// The reset write back to cache so a subsequent reload sees the
// fresh state.
const persisted = await cache.get<{
toggles: MapToggles;
lastResetTurn: number;
}>("game-map-toggles", GAME_ID);
expect(persisted?.toggles).toEqual(DEFAULT_MAP_TOGGLES);
expect(persisted?.lastResetTurn).toBe(5);
store.dispose();
});
test("matching lastResetTurn restores persisted overrides", async () => {
await cache.put("game-map-toggles", GAME_ID, {
toggles: { ...DEFAULT_MAP_TOGGLES, hyperspaceGroups: false },
lastResetTurn: 5,
});
listMyGamesSpy.mockResolvedValue([makeGameSummary(5)]);
const store = new GameStateStore();
await store.init({ client: makeFakeClient(5), cache, gameId: GAME_ID });
expect(store.mapToggles.hyperspaceGroups).toBe(false);
store.dispose();
});
test("advanceToPending resets toggles after jumping onto the new turn", async () => {
await cache.put("game-prefs", `${GAME_ID}/last-viewed-turn`, 4);
await cache.put("game-map-toggles", GAME_ID, {
toggles: DEFAULT_MAP_TOGGLES,
lastResetTurn: 4,
});
// First setGame opens the user on turn 4 with currentTurn=5
// (last-viewed-turn bookmark < currentTurn). The new-turn
// reset path fires immediately because lastResetTurn=4 < 5.
listMyGamesSpy.mockResolvedValue([makeGameSummary(5)]);
const store = new GameStateStore();
await store.init({ client: makeFakeClient(4), cache, gameId: GAME_ID });
// Drift the toggles after the setGame reset so we can verify
// that advanceToPending resets them again on the user's
// explicit jump onto turn 5.
await store.setMapToggle("battleMarkers", false);
expect(store.mapToggles.battleMarkers).toBe(false);
expect(store.pendingTurn).toBe(5);
// User clicks "Return to current turn" — the store fetches the
// turn-5 report and resets toggles.
await store.advanceToPending();
expect(store.mapToggles).toEqual(DEFAULT_MAP_TOGGLES);
expect(store.pendingTurn).toBeNull();
store.dispose();
});
});