Files
TREK/client/src/components/Map/useHazardLayerGL.test.ts
T
Maurice c1d55c463c fix(dawarich): three things a tester on dev1 ran into
The Atlas country check timed out. It asked Dawarich for a year of visited
cities in one request, and Dawarich computes that over every point in the
window with nothing cached, so a dense recorder never got under the 15 s the
server allows. The year is now asked for a month at a time, one request after
another, and folded back together. The windows sit a second apart because
Dawarich reads the bounds as inclusive.

The recorded route only appeared after a reload. Every Dawarich call from the
browser carried the shared 8 s timeout, so the browser gave up while the server
carried on and cached the answer for the next page load. Calls that make the
server talk to Dawarich now wait two minutes, the Atlas four. On the GL
renderers the draw also gave up whenever isStyleLoaded() was false, which it is
while any tile is still loading, and only retried on style.load. It now waits
for idle. The hazard overlay had the same guard and gets the same wait. The
pill shows a ring while the route loads, so a slow first fetch no longer looks
like "done, nothing there".

A collapsed day now takes its recorded route off the map, following the same
folds the planner already applies to places, the road trip sidebar's included.
2026-09-14 23:00:57 +02:00

108 lines
3.9 KiB
TypeScript

/**
* FE-ROADTRIP-HAZARDGL-001 to FE-ROADTRIP-HAZARDGL-003: when the hazard overlay
* on the GL renderer actually gets drawn.
*
* It shares a failure with the recorded Dawarich route: `isStyleLoaded()` is
* false while any source still has tiles in flight, not only before the first
* style. A draw that gave up there and waited for `style.load` alone stayed off
* the map until the style was rebuilt, which in practice meant a reload. These
* cases pin the wait for `idle` that replaced it, and that the wait does not
* outlive the component.
*/
import { describe, it, expect, vi } from 'vitest'
import { renderHook } from '@testing-library/react'
import type { Map as MapboxMap, Popup } from 'mapbox-gl'
import type { RoadtripHazard } from '@trek/shared'
import { useHazardLayerGL } from './useHazardLayerGL'
vi.mock('../../i18n/TranslationContext', () => ({ useTranslation: () => ({ t: (key: string) => key }) }))
const SOURCE = 'roadtrip-hazards'
const HAZARD: RoadtripHazard = {
id: 'dwd-1',
source: 'DWD',
title: 'Sturmboeen',
description: 'Boeen bis 85 km/h',
updatedAt: '2026-09-14T10:00:00.000Z',
validUntil: null,
url: 'https://www.dwd.de/warnungen',
geometry: { type: 'Point', coordinates: [13.4, 52.5] },
}
function fakeGlMap(styleLoaded: boolean) {
const sources = new Set<string>()
const layers = new Set<string>()
const once = new Map<string, Set<() => void>>()
const state = { styleLoaded }
const map = {
isStyleLoaded: vi.fn(() => state.styleLoaded),
getSource: vi.fn((id: string) => (sources.has(id) ? {} : undefined)),
addSource: vi.fn((id: string) => { sources.add(id) }),
removeSource: vi.fn((id: string) => { sources.delete(id) }),
getLayer: vi.fn((id: string) => (layers.has(id) ? {} : undefined)),
addLayer: vi.fn((spec: { id: string }) => { layers.add(spec.id) }),
removeLayer: vi.fn((id: string) => { layers.delete(id) }),
on: vi.fn(),
once: vi.fn((type: string, fn: () => void) => {
const set = once.get(type) ?? new Set<() => void>()
set.add(fn)
once.set(type, set)
}),
off: vi.fn((type: string, fn: () => void) => { once.get(type)?.delete(fn) }),
}
return {
map: map as unknown as MapboxMap,
spies: map,
state,
sources,
idleListeners: () => once.get('idle')?.size ?? 0,
emitIdle: () => {
const pending = [...(once.get('idle') ?? [])]
once.delete('idle')
pending.forEach(fn => fn())
},
}
}
const popup = () => ({}) as Popup
describe('useHazardLayerGL', () => {
it('FE-ROADTRIP-HAZARDGL-001: draws straight away into a style that is ready', () => {
const gl = fakeGlMap(true)
renderHook(() => useHazardLayerGL(gl.map, true, [HAZARD], popup))
expect(gl.spies.addSource).toHaveBeenCalledWith(SOURCE, expect.objectContaining({ type: 'geojson' }))
expect(gl.spies.addLayer).toHaveBeenCalledTimes(3)
expect(gl.idleListeners()).toBe(0)
})
it('FE-ROADTRIP-HAZARDGL-002: hazards that arrive while tiles are loading are drawn once the map is idle', () => {
const gl = fakeGlMap(false)
renderHook(() => useHazardLayerGL(gl.map, true, [HAZARD], popup))
expect(gl.spies.addSource).not.toHaveBeenCalled()
expect(gl.idleListeners()).toBe(1)
// Still busy at the first idle: wait again, and only once.
gl.emitIdle()
expect(gl.spies.addSource).not.toHaveBeenCalled()
expect(gl.idleListeners()).toBe(1)
gl.state.styleLoaded = true
gl.emitIdle()
expect(gl.spies.addSource).toHaveBeenCalledTimes(1)
expect(gl.idleListeners()).toBe(0)
})
it('FE-ROADTRIP-HAZARDGL-003: an unmount while waiting takes the wait with it', () => {
const gl = fakeGlMap(false)
const view = renderHook(() => useHazardLayerGL(gl.map, true, [HAZARD], popup))
view.unmount()
expect(gl.idleListeners()).toBe(0)
expect(gl.spies.off).toHaveBeenCalledWith('idle', expect.any(Function))
expect(gl.sources.size).toBe(0)
})
})