import { describe, expect, it, vi } from 'vitest'; import { Popup, type LatLngBounds } from 'leaflet'; import '../../src/components/leaflet-map.ts'; import '../../src/components/leaflet-tile-layer.ts'; import '../../src/components/leaflet-marker.ts'; // The initial (and every re-)frame is deferred to a microtask so the burst of // `layeradd` events during load collapses into one `fitBounds`. Awaiting a // freshly-queued microtask flushes the pending one first. const flush = () => new Promise((resolve) => { queueMicrotask(resolve); }); function makeMap(attrs: Record = {}) { const map = document.createElement('leaflet-map'); for (const [k, v] of Object.entries(attrs)) map.setAttribute(k, v); return map; } function marker(lat: number, lng: number) { const m = document.createElement('leaflet-marker'); m.setAttribute('lat', String(lat)); m.setAttribute('lng', String(lng)); return m; } describe('', () => { it('frames every marker once on load instead of honouring lat/lng/zoom', async () => { const map = makeMap({ 'fit-to-markers': '', lat: '0', lng: '0', zoom: '2' }); map.append(marker(34.0537, -118.2427), marker(33.9416, -118.4085), marker(34.1341, -118.3215)); document.body.append(map); // Spy after connect but before the microtask flush, so the initial frame is // still pending and gets captured. const spy = vi.spyOn(map.leafletObject!, 'fitBounds'); await flush(); expect(spy).toHaveBeenCalledTimes(1); const bounds = spy.mock.calls[0]![0] as LatLngBounds; expect(bounds.contains([34.0537, -118.2427])).toBe(true); expect(bounds.contains([33.9416, -118.4085])).toBe(true); expect(bounds.contains([34.1341, -118.3215])).toBe(true); expect(spy.mock.calls[0]![1]).toMatchObject({ padding: [20, 20] }); map.remove(); }); it('passes fit-padding and fit-max-zoom through to fitBounds', async () => { const map = makeMap({ 'fit-to-markers': '', 'fit-padding': '50', 'fit-max-zoom': '12' }); map.append(marker(1, 2), marker(3, 4)); document.body.append(map); const spy = vi.spyOn(map.leafletObject!, 'fitBounds'); await flush(); expect(spy.mock.calls[0]![1]).toMatchObject({ padding: [50, 50], maxZoom: 12 }); map.remove(); }); it('re-frames when a marker is added later', async () => { const map = makeMap({ 'fit-to-markers': '' }); map.append(marker(10, 10), marker(20, 20)); document.body.append(map); await flush(); const spy = vi.spyOn(map.leafletObject!, 'fitBounds'); map.append(marker(40, -100)); await flush(); expect(spy).toHaveBeenCalledTimes(1); expect((spy.mock.calls[0]![0] as LatLngBounds).contains([40, -100])).toBe(true); map.remove(); }); it('activates when the attribute is toggled on after connect', async () => { const map = makeMap({ lat: '0', lng: '0', zoom: '3' }); map.append(marker(34, -118), marker(35, -119)); document.body.append(map); const spy = vi.spyOn(map.leafletObject!, 'fitBounds'); await flush(); expect(spy).not.toHaveBeenCalled(); map.setAttribute('fit-to-markers', ''); await flush(); expect(spy).toHaveBeenCalledTimes(1); map.remove(); }); it('does not reframe when the user pans or zooms', async () => { const map = makeMap({ 'fit-to-markers': '' }); map.append(marker(10, 10), marker(20, 20)); document.body.append(map); await flush(); const spy = vi.spyOn(map.leafletObject!, 'fitBounds'); // Simulate a manual pan + zoom. map.leafletObject!.setView([0, 0], 6); map.leafletObject!.fire('moveend'); map.leafletObject!.fire('zoomend'); await flush(); expect(spy).not.toHaveBeenCalled(); map.remove(); }); it('does not reframe when a popup is added to / opened on the map', async () => { const map = makeMap({ 'fit-to-markers': '' }); map.append(marker(34, -118), marker(35, -119)); document.body.append(map); await flush(); const spy = vi.spyOn(map.leafletObject!, 'fitBounds'); new Popup().setLatLng([34, -118]).setContent('hi').openOn(map.leafletObject!); await flush(); expect(spy).not.toHaveBeenCalled(); map.remove(); }); it('does nothing without the attribute', async () => { const map = makeMap({ lat: '10', lng: '20', zoom: '5' }); map.append(marker(1, 2), marker(3, 4)); document.body.append(map); const spy = vi.spyOn(map.leafletObject!, 'fitBounds'); await flush(); expect(spy).not.toHaveBeenCalled(); expect(map.leafletObject!.getZoom()).toBe(5); map.remove(); }); it('leaves the view alone when no layer has a position', async () => { const map = makeMap({ 'fit-to-markers': '', lat: '5', lng: '6', zoom: '4' }); const tiles = document.createElement('leaflet-tile-layer'); tiles.setAttribute('url', 'https://tile.example/{z}/{x}/{y}.png'); map.append(tiles); document.body.append(map); const spy = vi.spyOn(map.leafletObject!, 'fitBounds'); await flush(); expect(spy).not.toHaveBeenCalled(); expect(map.leafletObject!.getZoom()).toBe(4); map.remove(); }); it('stops re-framing after disconnect', async () => { const map = makeMap({ 'fit-to-markers': '' }); map.append(marker(1, 1), marker(2, 2)); document.body.append(map); await flush(); const mapObj = map.leafletObject!; const spy = vi.spyOn(mapObj, 'fitBounds'); map.remove(); await flush(); expect(spy).not.toHaveBeenCalled(); }); });