// Coverage for the map's light / dark palette threading. The map // renderer follows the resolved app theme: `map.svelte` selects // `DARK_THEME` or `LIGHT_THEME` and threads it through every primitive // builder so the canvas, planets, ship groups, routes, markers, and // overlays all switch with the rest of the chrome. These tests pin the // palette plumbing — that the builders honour the supplied palette and // that the two palettes actually differ role-for-role — without booting // Pixi. The per-builder colour tests (battle-markers, cargo-routes, // selection-ring) cover their own surfaces; this spec covers the // planet, ship-group, and reach-ring paths plus the palette invariants. import { describe, expect, test } from "vitest"; import type { GameReport, ReportPlanet } from "../src/api/game-state"; import { computeReachCircles } from "../src/map/reach-circles"; import { reportToWorld } from "../src/map/state-binding"; import { DARK_THEME, LIGHT_THEME, type Theme } from "../src/map/world"; import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups"; function makeReport(overrides: Partial = {}): GameReport { return { turn: 1, mapWidth: 4000, mapHeight: 4000, planetCount: 0, planets: [], race: "", localShipClass: [], routes: [], localPlayerDrive: 0, localPlayerWeapons: 0, localPlayerShields: 0, localPlayerCargo: 0, ...EMPTY_SHIP_GROUPS, ...overrides, }; } function makePlanet(overrides: Partial): ReportPlanet { return { number: 0, name: "", x: 0, y: 0, kind: "local", owner: null, size: null, resources: null, industryStockpile: null, materialsStockpile: null, industry: null, population: null, colonists: null, production: null, freeIndustry: null, ...overrides, }; } function planetFill( kind: ReportPlanet["kind"], theme?: Theme, ): number | undefined { const { world } = reportToWorld( makeReport({ planets: [makePlanet({ number: 1, kind })] }), theme, ); return world.primitives[0]?.style.fillColor; } describe("map palette threading", () => { test("planet glyphs default to the dark palette", () => { expect(planetFill("local")).toBe(DARK_THEME.planetLocal); }); test("planet glyphs follow the supplied palette per kind", () => { expect(planetFill("local", LIGHT_THEME)).toBe(LIGHT_THEME.planetLocal); expect(planetFill("other", LIGHT_THEME)).toBe(LIGHT_THEME.planetOther); expect(planetFill("uninhabited", LIGHT_THEME)).toBe( LIGHT_THEME.planetUninhabited, ); expect(planetFill("unidentified", LIGHT_THEME)).toBe( LIGHT_THEME.planetUnidentified, ); }); test("incoming-group accent follows the palette", () => { const report = makeReport({ planets: [ makePlanet({ number: 1, x: 0, y: 0, kind: "local" }), makePlanet({ number: 2, x: 100, y: 0, kind: "local" }), ], incomingShipGroups: [ { origin: 1, destination: 2, distance: 10, speed: 5, mass: 1 }, ], }); for (const theme of [DARK_THEME, LIGHT_THEME]) { const { world, hitLookup } = reportToWorld(report, theme); // Locate the clickable incoming point via the hit-lookup. let incomingId: number | null = null; for (const [id, target] of hitLookup) { if ( target.kind === "shipGroup" && target.ref.variant === "incoming" ) { incomingId = id; break; } } expect(incomingId).not.toBeNull(); const point = world.primitives.find((p) => p.id === incomingId); expect(point?.style.fillColor).toBe(theme.shipIncoming); } }); test("reach rings follow the supplied palette", () => { const dark = computeReachCircles({ x: 0, y: 0 }, 100, 1000, 1000, "torus"); expect(dark[0]?.style.strokeColor).toBe(DARK_THEME.reachCircle); const light = computeReachCircles( { x: 0, y: 0 }, 100, 1000, 1000, "torus", LIGHT_THEME, ); expect(light[0]?.style.strokeColor).toBe(LIGHT_THEME.reachCircle); }); }); describe("palette invariants", () => { test("the two palettes define the same fields", () => { expect(Object.keys(LIGHT_THEME).sort()).toEqual( Object.keys(DARK_THEME).sort(), ); }); test("the canvas background and accents differ between palettes", () => { expect(LIGHT_THEME.background).not.toBe(DARK_THEME.background); expect(LIGHT_THEME.shipIncoming).not.toBe(DARK_THEME.shipIncoming); expect(LIGHT_THEME.battleMarker).not.toBe(DARK_THEME.battleMarker); expect(LIGHT_THEME.bombingWiped).not.toBe(DARK_THEME.bombingWiped); }); });