From 309bef4db7211fe48ba6cfd7bdbd63258f147dcb Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 14 Oct 2026 23:37:58 +0200 Subject: [PATCH] fix(ferratas): leaving 3D with via ferratas on no longer crashes, the removed map is not asked for its layers --- .../Ferratas/useFerrataLayerGL.test.tsx | 20 ++++++++++++++++--- .../components/Ferratas/useFerrataLayerGL.ts | 3 +++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/client/src/components/Ferratas/useFerrataLayerGL.test.tsx b/client/src/components/Ferratas/useFerrataLayerGL.test.tsx index bedd48353..507503f35 100644 --- a/client/src/components/Ferratas/useFerrataLayerGL.test.tsx +++ b/client/src/components/Ferratas/useFerrataLayerGL.test.tsx @@ -1,5 +1,5 @@ /** - * FE-FERRATA-GL-001 to FE-FERRATA-GL-012: the via ferrata layer on a + * FE-FERRATA-GL-001 to FE-FERRATA-GL-013: the via ferrata layer on a * MapLibre or Mapbox GL map. * * Pinned: the layers go in once per map (and wait for `idle` when tiles are @@ -64,7 +64,7 @@ function fakeGlMap(styleLoaded = true) { const images = new Set(); const handlers = new Map>(); const once = new Map>(); - const state = { styleLoaded, hit: null as null | { properties: { id: string; group: string } } }; + const state = { styleLoaded, removed: false, hit: null as null | { properties: { id: string; group: string } } }; const canvas = document.createElement('canvas'); const container = document.createElement('div'); const keyOf = (type: string, layer?: unknown) => (typeof layer === 'string' ? `${type}:${layer}` : type); @@ -77,7 +77,12 @@ function fakeGlMap(styleLoaded = true) { removeSource: vi.fn((id: string) => { sources.delete(id); }), - getLayer: vi.fn((id: string) => (layers.has(id) ? {} : undefined)), + // Like MapLibre: a removed map has no style, and asking it for a layer throws. + getStyle: vi.fn(() => (state.removed ? undefined : { version: 8 })), + getLayer: vi.fn((id: string) => { + if (state.removed) throw new TypeError('can\'t access property "getLayer", this.style is undefined'); + return layers.has(id) ? {} : undefined; + }), addLayer: vi.fn((spec: { id: string }) => { layers.add(spec.id); }), @@ -267,6 +272,15 @@ describe('useFerrataLayerGL', () => { expect(() => renderHook(() => useFerrataLayerGL(null, GROUPS, vi.fn()))).not.toThrow(); }); + it('FE-FERRATA-GL-013: a map already removed with its view (leaving 3D) is left alone on unmount', async () => { + const gl = fakeGlMap(); + const { unmount } = render(); + await waitFor(() => expect(gl.layers.has(LABEL)).toBe(true)); + gl.state.removed = true; + expect(() => unmount()).not.toThrow(); + expect(gl.listeners()).toBe(0); + }); + it('FE-FERRATA-GL-010: a theme change repaints the installed layers without rebuilding them', async () => { const root = document.documentElement; root.style.setProperty('--map-ferrata', 'black'); diff --git a/client/src/components/Ferratas/useFerrataLayerGL.ts b/client/src/components/Ferratas/useFerrataLayerGL.ts index 4cc358a00..ef31a9d9f 100644 --- a/client/src/components/Ferratas/useFerrataLayerGL.ts +++ b/client/src/components/Ferratas/useFerrataLayerGL.ts @@ -136,6 +136,9 @@ function addFerrataIcon(map: Map, paint: FerrataPaint, gone: () => boolean): voi } function removeFerrataLayers(map: Map): void { + // A map already removed with its view (leaving 3D) took its style and the layers with it; + // asking it for a layer then throws. + if (!map.getStyle?.()) return; for (const layer of [...LAYERS].reverse()) if (map.getLayer(layer)) map.removeLayer(layer); for (const source of [LABEL_SOURCE, SOURCE]) if (map.getSource(source)) map.removeSource(source); if (map.hasImage(FERRATA_ICON_IMAGE)) map.removeImage(FERRATA_ICON_IMAGE);