/** * HeatLayers regression test — post-walkthrough fix. * * Bug: the heat layer's `setLatLngs` was called BEFORE the layer was added to the * map. A leaflet.heat layer that is not on a map has a null `_map`, and * `setLatLngs -> redraw` dereferences `_map._animating`, throwing * "Cannot read properties of null (reading '_animating')" and white-screening * the whole SPA right after login. * * This test exercises the REAL HeatLayers code path (not a wholesale RecordsMap * mock) and asserts the layer is added to the map BEFORE setLatLngs is called. * Against the old code (setLatLngs first), the ordering assertion fails. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { render } from '@testing-library/react' const { callLog, setLatLngsSpy, mapAddLayerSpy } = vi.hoisted(() => { const callLog: string[] = [] const setLatLngsSpy = vi.fn((_pts: unknown) => { callLog.push('setLatLngs') }) const mapAddLayerSpy = vi.fn((_layer: unknown) => { callLog.push('addLayer') }) return { callLog, setLatLngsSpy, mapAddLayerSpy } }) // Mock leaflet. heatLayer returns a fake layer whose setLatLngs logs call order; // Icon/DivIcon/marker exist because RecordsMap.tsx runs icon setup at module load. vi.mock('leaflet', () => { class FakeIcon { constructor(_opts: unknown) {} static Default = { prototype: {}, mergeOptions: vi.fn() } } return { Icon: FakeIcon, DivIcon: vi.fn(function FakeDivIcon(_opts: unknown) { return {} }), heatLayer: vi.fn(() => ({ setLatLngs: setLatLngsSpy, setOptions: vi.fn(), addTo: vi.fn() })), markerClusterGroup: vi.fn(() => ({ addLayer: vi.fn(), addTo: vi.fn(), clearLayers: vi.fn() })), marker: vi.fn(() => ({ bindTooltip: vi.fn().mockReturnThis(), on: vi.fn().mockReturnThis() })), default: {}, } }) vi.mock('leaflet.heat', () => ({})) vi.mock('leaflet.markercluster', () => ({})) vi.mock('leaflet/dist/images/marker-icon-2x.png', () => ({ default: 'marker-icon-2x.png' })) vi.mock('leaflet/dist/images/marker-icon.png', () => ({ default: 'marker-icon.png' })) vi.mock('leaflet/dist/images/marker-shadow.png', () => ({ default: 'marker-shadow.png' })) vi.mock('leaflet/dist/leaflet.css', () => ({})) vi.mock('leaflet.markercluster/dist/MarkerCluster.css', () => ({})) vi.mock('leaflet.markercluster/dist/MarkerCluster.Default.css', () => ({})) // useMap returns a fake map; hasLayer=false so addLayer is exercised. vi.mock('react-leaflet', () => ({ MapContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, TileLayer: () => null, useMap: () => ({ addLayer: mapAddLayerSpy, removeLayer: vi.fn(), hasLayer: () => false, getSize: () => ({ x: 800, y: 600 }), latLngToContainerPoint: () => ({ x: 100, y: 100 }), on: vi.fn(), off: vi.fn(), }), })) import { HeatLayers } from './RecordsMap' import type { HeatPoint } from './mapUtils' const heatPoints: HeatPoint[] = [ [39.9, 116.4, 1], [39.91, 116.41, 1], ] describe('HeatLayers (real code path — regression for null _map crash)', () => { beforeEach(() => { vi.clearAllMocks() callLog.length = 0 }) it('adds the heat layer to the map BEFORE calling setLatLngs', () => { render( , ) // Data was applied... expect(setLatLngsSpy).toHaveBeenCalledWith(heatPoints) // ...and the layer was added to the map first. The old buggy order // (setLatLngs before addLayer) makes this fail. expect(callLog).toEqual(['addLayer', 'setLatLngs']) expect(callLog.indexOf('addLayer')).toBeLessThan(callLog.indexOf('setLatLngs')) }) it('does not call setLatLngs while the layer is hidden (off the map)', () => { render( , ) // Hidden layers are never on the map, so setLatLngs must not run on them. expect(setLatLngsSpy).not.toHaveBeenCalled() expect(mapAddLayerSpy).not.toHaveBeenCalled() }) })