forked from OurHike/OurHike
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMapView.test.tsx
More file actions
135 lines (106 loc) · 4.67 KB
/
Copy pathMapView.test.tsx
File metadata and controls
135 lines (106 loc) · 4.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { StrictMode } from 'react'
import { render, cleanup, screen } from '@testing-library/react'
import { MockMap, resetMapLibreMock } from '../test/mocks/maplibre-gl'
import { MapView } from './MapView'
// Lifecycle is the whole risk surface here. A map that gets built twice means
// two WebGL contexts, two GPS watchers and doubled tile reads off a 314 MB
// on-device archive; a map that never gets torn down leaks all of the same.
// React StrictMode deliberately mounts -> unmounts -> remounts in development
// precisely to expose that class of bug, so these tests run under it.
const { registrationOrder } = vi.hoisted(() => ({ registrationOrder: [] as number[] }))
vi.mock('maplibre-gl', () => import('../test/mocks/maplibre-gl'))
// Records how many maps existed at the moment the protocol was registered.
// A 0 proves registration happened BEFORE any map was constructed - which it
// must, or the map cannot resolve its own pmtiles:// style URL.
vi.mock('./protocol', async () => {
const { MockMap: Recorded } = await import('../test/mocks/maplibre-gl')
return {
PMTILES_SCHEME: 'pmtiles',
registerPMTilesProtocol: vi.fn(() => {
registrationOrder.push(Recorded.instances.length)
}),
}
})
const PROPS = {
topoArchiveUrl: 'pmtiles://ourhike-corridor',
trailsUrl: '/data/trails.geojson',
}
beforeEach(() => {
resetMapLibreMock()
registrationOrder.length = 0
})
afterEach(() => {
cleanup()
})
describe('MapView', () => {
it('leaves exactly one LIVE map after StrictMode’s deliberate double-invoke', () => {
render(
<StrictMode>
<MapView {...PROPS} />
</StrictMode>,
)
// React mounts, tears down, and remounts on purpose here, so more than one
// map may have been CONSTRUCTED over the render's lifetime. What must never
// happen is two of them being alive at once - that is the actual leak.
expect(MockMap.live).toHaveLength(1)
})
it('tears the map down on unmount, leaving nothing live', () => {
const { unmount } = render(
<StrictMode>
<MapView {...PROPS} />
</StrictMode>,
)
unmount()
expect(MockMap.live).toHaveLength(0)
expect(MockMap.instances.every((m) => m.removed)).toBe(true)
})
it('does not rebuild the map when re-rendered with a fresh center array identity', () => {
// A parent passing center={[x, y]} inline hands over a new array every
// render. If that landed in the effect's dependencies the map would be
// destroyed and rebuilt on every parent render - catastrophic, and easy to
// do by accident.
const { rerender } = render(<MapView {...PROPS} center={[-77.1, 39.3]} zoom={12} />)
const afterFirstRender = MockMap.instances.length
rerender(<MapView {...PROPS} center={[-77.1, 39.3]} zoom={12} />)
rerender(<MapView {...PROPS} center={[-77.1, 39.3]} zoom={12} />)
expect(MockMap.instances).toHaveLength(afterFirstRender)
expect(MockMap.live).toHaveLength(1)
})
it('registers the pmtiles protocol before constructing any map', () => {
render(<MapView {...PROPS} />)
expect(registrationOrder.length).toBeGreaterThan(0)
expect(registrationOrder[0]).toBe(0)
})
it('builds the map against the container it rendered, using the style URLs it was given', () => {
render(<MapView {...PROPS} />)
const [map] = MockMap.live
expect(map.options.container).toBeInstanceOf(HTMLElement)
expect(map.options.style).toBeTypeOf('object')
})
it('exposes the map canvas as a labelled region rather than an unnamed div', () => {
render(<MapView {...PROPS} />)
expect(screen.getByRole('region', { name: /trail map/i })).toBeInTheDocument()
})
it('attaches the map chrome once the map exists', () => {
render(<MapView {...PROPS} />)
const [map] = MockMap.live
expect(map.controls.length).toBeGreaterThan(0)
})
it('re-attaches chrome for a units change without rebuilding the map underneath the hiker', () => {
// Switching the scale bar to metric is a display preference. Rebuilding the
// whole map for it would drop the WebGL context and re-read tiles - a
// visible flash mid-walk for what should be a three-control swap.
const { rerender } = render(<MapView {...PROPS} units="imperial" />)
const builtInitially = MockMap.instances.length
rerender(<MapView {...PROPS} units="metric" />)
expect(MockMap.instances).toHaveLength(builtInitially)
expect(MockMap.live).toHaveLength(1)
})
it('leaves no controls attached after unmount', () => {
const { unmount } = render(<MapView {...PROPS} />)
const [map] = MockMap.live
unmount()
expect(map.controls).toHaveLength(0)
})
})