M2: frontend walkthrough fixes + explicit dev compose stack
Post-M2 self-walkthrough polish, batched into one commit. Map / heat: - fix heat-layer white-screen crash after login (add layer to map before setLatLngs; an off-map leaflet.heat layer has a null _map and throws) - normalize each heat layer to the densest pixel cell visible in the CURRENT viewport (maxZoom:0 so intensity factor f=1) and recompute on moveend/zoomend, so sparse poo data reaches red and stays normalized at any zoom level - dark CARTO basemap tiles when the color scheme is dark UI: - dark-mode toggle in the top-right, beside the settings gear - switch top-right nav (records / theme / settings / logout) to Feather icons with hover tooltips - home: Grafana-style quick time-range presets + back/forward shift buttons, placed between the From/To pickers and Apply; fix Select/tooltip z-index (Leaflet stacking) and the shift-button height alignment API client: - stop flooding GET /api/session with 401s: the session probe and the login endpoint own their 401s (no global redirect), which fixes the logout hang and the spinning login page Compose: - rename docker-compose.override.yml -> docker-compose.dev.yml as an explicit, non-auto-layered dev stack (8001, -dev container names, prod-copy ./data DB); update tests/test_deployment.py (read dev.yml, tolerate the !override tag) and the README "Docker Compose" section Tests: - pixel-grid peak counter, time-range presets, heat-layer ordering regression, and 401-redirect regression
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 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 }) => <div>{children}</div>,
|
||||
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(
|
||||
<HeatLayers
|
||||
locationHeatPoints={heatPoints}
|
||||
pooHeatPoints={[]}
|
||||
showLocationHeat={true}
|
||||
showPooHeat={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
// 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(
|
||||
<HeatLayers
|
||||
locationHeatPoints={heatPoints}
|
||||
pooHeatPoints={heatPoints}
|
||||
showLocationHeat={false}
|
||||
showPooHeat={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Hidden layers are never on the map, so setLatLngs must not run on them.
|
||||
expect(setLatLngsSpy).not.toHaveBeenCalled()
|
||||
expect(mapAddLayerSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user