/** * mapRenderer.test.ts — Tests for all mapRenderer.ts behavioral contracts * * Tests mock Leaflet, DOM APIs, and CSS variable access. * Each contract from the spec has its own describe block. * * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import type { Place } from "../src/parser"; // ─── Helpers ────────────────────────────────────────────────────────── function makePlace( name: string, overrides: Partial = {} ): Place { return { name, fields: {}, notes: [], startLine: 0, endLine: 0, ...overrides, }; } // ─── Leaflet Mock ───────────────────────────────────────────────────── // We build a comprehensive mock of the Leaflet APIs used by mapRenderer. // Each test gets a fresh set of mock objects. interface MockMarker { setLatLng: ReturnType; setIcon: ReturnType; bindPopup: ReturnType; openPopup: ReturnType; on: ReturnType; off: ReturnType; addTo: ReturnType; remove: ReturnType; getLatLng: ReturnType; _latlng: { lat: number; lng: number }; _events: Record void)[]>; } interface MockCircleMarker { addTo: ReturnType; remove: ReturnType; setLatLng: ReturnType; getElement: ReturnType; } interface MockMap { setView: ReturnType; fitBounds: ReturnType; remove: ReturnType; invalidateSize: ReturnType; getZoom: ReturnType; on: ReturnType; off: ReturnType; _events: Record void)[]>; } interface MockTileLayer { addTo: ReturnType; } interface MockLayerGroup { addTo: ReturnType; clearLayers: ReturnType; addLayer: ReturnType; getLayers: ReturnType; eachLayer: ReturnType; } interface MockIcon { options: Record; } interface MockLatLngBounds { extend: ReturnType; isValid: ReturnType; getCenter: ReturnType; pad: ReturnType; } let mockMapInstance: MockMap; let mockMarkers: MockMarker[]; let mockCircleMarkers: MockCircleMarker[]; let mockTileLayer: MockTileLayer; let mockLayerGroup: MockLayerGroup; let mockIcons: MockIcon[]; let mockBounds: MockLatLngBounds; function createMockMarker(lat: number, lng: number): MockMarker { const marker: MockMarker = { setLatLng: vi.fn().mockReturnThis(), setIcon: vi.fn().mockReturnThis(), bindPopup: vi.fn().mockReturnThis(), openPopup: vi.fn().mockReturnThis(), on: vi.fn().mockImplementation(function (this: MockMarker, event: string, handler: (...args: unknown[]) => void) { if (!this._events[event]) this._events[event] = []; this._events[event].push(handler); return this; }), off: vi.fn().mockReturnThis(), addTo: vi.fn().mockReturnThis(), remove: vi.fn().mockReturnThis(), getLatLng: vi.fn().mockReturnValue({ lat, lng }), _latlng: { lat, lng }, _events: {}, }; mockMarkers.push(marker); return marker; } function createMockCircleMarker(): MockCircleMarker { const cm: MockCircleMarker = { addTo: vi.fn().mockReturnThis(), remove: vi.fn().mockReturnThis(), setLatLng: vi.fn().mockReturnThis(), getElement: vi.fn().mockReturnValue(document.createElement("div")), }; mockCircleMarkers.push(cm); return cm; } function setupLeafletMock(): typeof import("leaflet") { mockMapInstance = { setView: vi.fn().mockReturnThis(), fitBounds: vi.fn().mockReturnThis(), remove: vi.fn(), invalidateSize: vi.fn(), getZoom: vi.fn().mockReturnValue(2), on: vi.fn().mockImplementation(function (this: MockMap, event: string, handler: (...args: unknown[]) => void) { if (!this._events[event]) this._events[event] = []; this._events[event].push(handler); return this; }), off: vi.fn().mockReturnThis(), _events: {}, }; mockTileLayer = { addTo: vi.fn().mockReturnThis(), }; mockLayerGroup = { addTo: vi.fn().mockReturnThis(), clearLayers: vi.fn(), addLayer: vi.fn(), getLayers: vi.fn().mockReturnValue([]), eachLayer: vi.fn(), }; mockBounds = { extend: vi.fn().mockReturnThis(), isValid: vi.fn().mockReturnValue(true), getCenter: vi.fn().mockReturnValue({ lat: 0, lng: 0 }), pad: vi.fn().mockReturnThis(), }; const L = { map: vi.fn().mockReturnValue(mockMapInstance), tileLayer: vi.fn().mockReturnValue(mockTileLayer), layerGroup: vi.fn().mockReturnValue(mockLayerGroup), marker: vi.fn().mockImplementation((latlng: [number, number]) => { return createMockMarker(latlng[0], latlng[1]); }), circleMarker: vi.fn().mockImplementation(() => { return createMockCircleMarker(); }), divIcon: vi.fn().mockImplementation((opts: Record) => { const icon: MockIcon = { options: opts }; mockIcons.push(icon); return icon; }), icon: vi.fn().mockImplementation((opts: Record) => { const icon: MockIcon = { options: opts }; mockIcons.push(icon); return icon; }), latLngBounds: vi.fn().mockReturnValue(mockBounds), latLng: vi.fn().mockImplementation((lat: number, lng: number) => ({ lat, lng })), DomEvent: { stopPropagation: vi.fn(), stop: vi.fn(), }, Util: { stamp: vi.fn().mockReturnValue(1), }, }; return L as unknown as typeof import("leaflet"); } // ─── Mock ResizeObserver ────────────────────────────────────────────── let resizeObserverCallback: ResizeObserverCallback | null = null; let resizeObserverDisconnected = false; class MockResizeObserver { callback: ResizeObserverCallback; constructor(callback: ResizeObserverCallback) { this.callback = callback; resizeObserverCallback = callback; resizeObserverDisconnected = false; } observe() {} unobserve() {} disconnect() { resizeObserverDisconnected = true; } } // ─── Test Setup ─────────────────────────────────────────────────────── let L: ReturnType; let createMap: typeof import("../src/mapRenderer").createMap; beforeEach(async () => { mockMarkers = []; mockCircleMarkers = []; mockIcons = []; L = setupLeafletMock(); // Mock leaflet module vi.doMock("leaflet", () => ({ default: L, ...L })); // Mock ResizeObserver globally vi.stubGlobal("ResizeObserver", MockResizeObserver); resizeObserverCallback = null; resizeObserverDisconnected = false; // Mock getComputedStyle for CSS variable access vi.stubGlobal( "getComputedStyle", vi.fn().mockReturnValue({ getPropertyValue: vi.fn().mockImplementation((prop: string) => { if (prop === "--interactive-accent") return "#7b6cd9"; return ""; }), }) ); // Import fresh module for each test const mod = await import("../src/mapRenderer"); createMap = mod.createMap; }); afterEach(() => { vi.restoreAllMocks(); vi.resetModules(); vi.unstubAllGlobals(); }); // ─── Contract 1: Leaflet CSS injection ──────────────────────────────── describe("Contract 1: Leaflet CSS injection", () => { it("injects a