// Pure-state coverage for the pick-mode overlay helper. The // renderer owns the Pixi side (`render.ts.openPickMode`); this file // asserts that `computePickOverlay` produces the correct draw spec // for every meaningful input combination — Pixi-free, so it stays // fast and stable against renderer plumbing changes. import { describe, expect, test } from "vitest"; import { ANCHOR_PADDING_WORLD, HOVER_PADDING_WORLD, computePickOverlay, type PickModeOptions, } from "../src/map/pick-mode"; import { DEFAULT_POINT_RADIUS_PX, type PointPrim, type PrimitiveID, } from "../src/map/world"; function makePoint( id: PrimitiveID, x: number, y: number, pointRadiusPx?: number, ): PointPrim { return { kind: "point", id, priority: 0, hitSlopPx: 0, x, y, style: pointRadiusPx === undefined ? {} : { pointRadiusPx }, }; } function makeOptions( overrides: Partial = {}, ): PickModeOptions { return { sourcePrimitiveId: 1, sourceX: 100, sourceY: 100, reachableIds: new Set([2, 3]), onPick: () => {}, ...overrides, }; } describe("computePickOverlay", () => { const points = new Map([ [1, makePoint(1, 100, 100, 6)], [2, makePoint(2, 200, 100, 5)], [3, makePoint(3, 100, 200)], [4, makePoint(4, 300, 300, 4)], ]); const allIds: PrimitiveID[] = [1, 2, 3, 4]; test("anchor radius equals source pointRadiusPx + ANCHOR_PADDING_WORLD", () => { const spec = computePickOverlay(makeOptions(), null, null, points, allIds); expect(spec.anchor.x).toBe(100); expect(spec.anchor.y).toBe(100); expect(spec.anchor.radius).toBe(6 + ANCHOR_PADDING_WORLD); }); test("anchor radius falls back to default when source has no pointRadiusPx", () => { const sourceless = new Map(points); sourceless.set(1, makePoint(1, 100, 100)); const spec = computePickOverlay( makeOptions(), null, null, sourceless, allIds, ); expect(spec.anchor.radius).toBe( DEFAULT_POINT_RADIUS_PX + ANCHOR_PADDING_WORLD, ); }); test("dimmedIds covers everything outside source + reachable", () => { const spec = computePickOverlay(makeOptions(), null, null, points, allIds); expect(Array.from(spec.dimmedIds).sort()).toEqual([4]); }); test("dimmedIds is empty when every primitive is either source or reachable", () => { const spec = computePickOverlay( makeOptions({ reachableIds: new Set([2, 3, 4]) }), null, null, points, allIds, ); expect(spec.dimmedIds.size).toBe(0); }); test("line is null while the cursor is off-canvas", () => { const spec = computePickOverlay(makeOptions(), null, null, points, allIds); expect(spec.line).toBeNull(); }); test("line endpoints follow the cursor when present", () => { const spec = computePickOverlay( makeOptions(), { x: 250, y: 320 }, null, points, allIds, ); expect(spec.line).toEqual({ x1: 100, y1: 100, x2: 250, y2: 320, }); }); test("hoverOutline is null when nothing is hovered", () => { const spec = computePickOverlay( makeOptions(), { x: 1, y: 1 }, null, points, allIds, ); expect(spec.hoverOutline).toBeNull(); }); test("hoverOutline is null when the hover targets a non-reachable primitive", () => { const spec = computePickOverlay( makeOptions(), { x: 1, y: 1 }, 4, points, allIds, ); expect(spec.hoverOutline).toBeNull(); }); test("hoverOutline is null when the hover targets the source planet", () => { const spec = computePickOverlay( makeOptions(), { x: 1, y: 1 }, 1, points, allIds, ); expect(spec.hoverOutline).toBeNull(); }); test("hoverOutline reflects the reachable target with HOVER_PADDING_WORLD", () => { const spec = computePickOverlay( makeOptions(), { x: 1, y: 1 }, 2, points, allIds, ); expect(spec.hoverOutline).toEqual({ x: 200, y: 100, radius: 5 + HOVER_PADDING_WORLD, }); }); test("hoverOutline radius falls back to default radius for default-style points", () => { const spec = computePickOverlay( makeOptions(), { x: 1, y: 1 }, 3, points, allIds, ); expect(spec.hoverOutline?.radius).toBe( DEFAULT_POINT_RADIUS_PX + HOVER_PADDING_WORLD, ); }); });