@defra/interactive-map 0.0.25-alpha → 0.0.27-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/map-style-config.md +30 -0
- package/docs/assets/css/docusaurus.scss +4 -0
- package/package.json +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/src/defaults.js +14 -14
- package/plugins/beta/draw-ml/src/mapboxDraw.js +5 -0
- package/plugins/beta/draw-ml/src/mapboxSnap.js +1 -1
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +10 -2
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +2 -0
- package/plugins/beta/draw-ml/src/reducer.js +4 -1
- package/plugins/beta/draw-ml/src/styles.js +42 -39
- package/plugins/beta/draw-ol/dist/css/index.css +13 -0
- package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/geometryHelpers.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/index.js +1 -0
- package/plugins/beta/draw-ol/src/DrawInit.jsx +67 -0
- package/plugins/beta/draw-ol/src/api/addFeature.js +29 -0
- package/plugins/beta/draw-ol/src/api/deleteFeature.js +11 -0
- package/plugins/beta/draw-ol/src/api/editFeature.js +50 -0
- package/plugins/beta/draw-ol/src/api/newLine.js +44 -0
- package/plugins/beta/draw-ol/src/api/newPolygon.js +44 -0
- package/plugins/beta/draw-ol/src/core/OLDrawManager.js +150 -0
- package/plugins/beta/draw-ol/src/core/featureStore.js +65 -0
- package/plugins/beta/draw-ol/src/core/styles.js +98 -0
- package/plugins/beta/draw-ol/src/core/undoStack.js +31 -0
- package/plugins/beta/draw-ol/src/defaults.js +16 -0
- package/plugins/beta/draw-ol/src/draw/DrawMode.js +113 -0
- package/plugins/beta/draw-ol/src/draw/drawInput.js +199 -0
- package/plugins/beta/draw-ol/src/draw.scss +17 -0
- package/plugins/beta/draw-ol/src/edit/EditMode.js +424 -0
- package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +227 -0
- package/plugins/beta/draw-ol/src/edit/midpointLayer.js +57 -0
- package/plugins/beta/draw-ol/src/edit/touchHandler.js +171 -0
- package/plugins/beta/draw-ol/src/edit/undoOps.js +95 -0
- package/plugins/beta/draw-ol/src/edit/vertexHitTest.js +72 -0
- package/plugins/beta/draw-ol/src/edit/vertexLayer.js +49 -0
- package/plugins/beta/draw-ol/src/edit/vertexOps.js +99 -0
- package/plugins/beta/draw-ol/src/events.js +114 -0
- package/plugins/beta/draw-ol/src/index.js +12 -0
- package/plugins/beta/draw-ol/src/manifest.js +121 -0
- package/plugins/beta/draw-ol/src/olDraw.js +38 -0
- package/plugins/beta/draw-ol/src/reducer.js +41 -0
- package/plugins/beta/draw-ol/src/snap/snapEngine.js +109 -0
- package/plugins/beta/draw-ol/src/snap/snapGeometry.js +200 -0
- package/plugins/beta/draw-ol/src/snap/snapIndicator.js +81 -0
- package/plugins/beta/draw-ol/src/snap/snapInteraction.js +56 -0
- package/plugins/beta/draw-ol/src/snap/snapManager.js +90 -0
- package/plugins/beta/draw-ol/src/utils/flattenStyleProperties.js +47 -0
- package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +97 -0
- package/plugins/beta/draw-ol/src/utils/olCoords.js +35 -0
- package/plugins/beta/draw-ol/src/utils/resolveColors.js +39 -0
- package/plugins/beta/draw-ol/src/utils/spatial.js +28 -0
- package/plugins/beta/draw-ol/src/utils/touchTarget.js +61 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/src/defaults.js +1 -0
- package/plugins/interact/src/hooks/useInteractionHandlers.js +150 -43
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +240 -8
- package/plugins/interact/src/reducer.js +7 -0
- package/plugins/interact/src/reducer.test.js +16 -0
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/index.js +1 -1
- package/providers/beta/openlayers/src/appEvents.js +9 -13
- package/providers/beta/openlayers/src/appEvents.test.js +27 -18
- package/providers/beta/openlayers/src/mapEvents.js +34 -20
- package/providers/beta/openlayers/src/mapEvents.test.js +65 -6
- package/providers/beta/openlayers/src/openlayersProvider.js +45 -26
- package/providers/beta/openlayers/src/openlayersProvider.test.js +12 -34
- package/providers/beta/openlayers/src/utils/highlightFeatures.js +245 -0
- package/providers/beta/openlayers/src/utils/hoverCursor.js +65 -0
- package/providers/beta/openlayers/src/utils/hoverCursor.test.js +235 -0
- package/providers/beta/openlayers/src/utils/openLayersFixes.js +11 -0
- package/providers/beta/openlayers/src/utils/openLayersFixes.test.js +53 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.js +77 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.test.js +181 -0
- package/providers/beta/openlayers/src/utils/tileLayers.js +23 -5
- package/providers/beta/openlayers/src/utils/tileLayers.test.js +81 -5
- package/providers/beta/openlayers/src/utils/vtTileFragments.js +86 -0
- package/rollup.esm.mjs +6 -0
- package/src/App/controls/keyboardActions.js +2 -2
- package/src/InteractiveMap/InteractiveMap.js +38 -10
- package/src/InteractiveMap/InteractiveMap.test.js +282 -356
- package/src/InteractiveMap/historyManager.js +7 -0
- package/src/InteractiveMap/historyManager.test.js +11 -0
- package/webpack.dev.mjs +3 -1
|
@@ -54,89 +54,109 @@ const mapProviderMock = {
|
|
|
54
54
|
})
|
|
55
55
|
}
|
|
56
56
|
const mockButtonInstance = { style: {}, removeAttribute: jest.fn(), focus: jest.fn() }
|
|
57
|
+
const MAP_ROOT_HTML = '<div id="map"></div>'
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
let rootEl, openButtonCallback
|
|
59
|
+
// --- Shared setup ---
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
jest.clearAllMocks()
|
|
63
|
-
document.body.innerHTML = '<div id="map"></div>'
|
|
64
|
-
rootEl = document.getElementById('map')
|
|
65
|
-
initialiseApp.mockResolvedValue({ _root: {}, someApi: jest.fn(), unmount: jest.fn() })
|
|
66
|
-
createButtonMock.mockImplementation((config, root, cb) => {
|
|
67
|
-
openButtonCallback = cb
|
|
68
|
-
return mockButtonInstance
|
|
69
|
-
})
|
|
70
|
-
})
|
|
61
|
+
let rootEl, openButtonCallback
|
|
71
62
|
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
function setupBeforeEach () {
|
|
64
|
+
jest.clearAllMocks()
|
|
65
|
+
checkDeviceSupport.mockReturnValue(true)
|
|
66
|
+
document.body.innerHTML = MAP_ROOT_HTML
|
|
67
|
+
rootEl = document.getElementById('map')
|
|
68
|
+
initialiseApp.mockResolvedValue({ _root: {}, someApi: jest.fn(), unmount: jest.fn() })
|
|
69
|
+
createButtonMock.mockImplementation((_config, _root, cb) => {
|
|
70
|
+
openButtonCallback = cb
|
|
71
|
+
return mockButtonInstance
|
|
74
72
|
})
|
|
73
|
+
}
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
// --- Construction ---
|
|
76
|
+
|
|
77
|
+
describe('InteractiveMap — Construction', () => {
|
|
78
|
+
beforeEach(setupBeforeEach)
|
|
79
|
+
afterEach(() => jest.restoreAllMocks())
|
|
80
|
+
|
|
81
|
+
it('throws if root element not found', () => {
|
|
82
|
+
expect(() => new InteractiveMap('nonexistent')).toThrow(/not found/)
|
|
81
83
|
})
|
|
82
84
|
|
|
83
|
-
it('returns early
|
|
85
|
+
it('returns early if device not supported', () => {
|
|
84
86
|
checkDeviceSupport.mockReturnValue(false)
|
|
85
|
-
|
|
86
|
-
expect(createMap).not.toThrow()
|
|
87
|
+
expect(() => new InteractiveMap('map', { mapProvider: mapProviderMock })).not.toThrow()
|
|
87
88
|
expect(historyManager.register).not.toHaveBeenCalled()
|
|
88
89
|
expect(createBreakpointDetector).not.toHaveBeenCalled()
|
|
89
90
|
expect(createInterfaceDetector).not.toHaveBeenCalled()
|
|
90
91
|
})
|
|
91
92
|
|
|
92
93
|
it('registers with historyManager for buttonFirst behaviour', () => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
expect(createMap).not.toThrow()
|
|
96
|
-
expect(historyManager.register).toHaveBeenCalled()
|
|
94
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
95
|
+
expect(historyManager.register).toHaveBeenCalledWith(map)
|
|
97
96
|
})
|
|
98
97
|
|
|
99
|
-
it('
|
|
100
|
-
const
|
|
101
|
-
expect(
|
|
102
|
-
expect(createBreakpointDetector).toHaveBeenCalled()
|
|
98
|
+
it('creates breakpoint and interface detectors', () => {
|
|
99
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
100
|
+
expect(createBreakpointDetector).toHaveBeenCalledWith(expect.objectContaining({ containerEl: map.rootEl }))
|
|
103
101
|
expect(createInterfaceDetector).toHaveBeenCalled()
|
|
104
102
|
})
|
|
105
103
|
|
|
106
|
-
it('
|
|
104
|
+
it('merges dataset props and constructor props into config', () => {
|
|
107
105
|
parseDataProperties.mockReturnValue({ test: 123 })
|
|
108
106
|
const map = new InteractiveMap('map', { custom: 'value', mapProvider: mapProviderMock })
|
|
109
107
|
expect(map.config.test).toBe(123)
|
|
110
108
|
expect(map.config.custom).toBe('value')
|
|
111
109
|
})
|
|
112
110
|
|
|
113
|
-
it('creates open button
|
|
111
|
+
it('creates open button, sets up behavior, and exposes eventBus methods', () => {
|
|
114
112
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
115
113
|
expect(createButtonMock).toHaveBeenCalled()
|
|
116
114
|
expect(setupBehavior).toHaveBeenCalledWith(map)
|
|
115
|
+
expect(typeof map.on).toBe('function')
|
|
116
|
+
expect(typeof map.off).toBe('function')
|
|
117
|
+
expect(typeof map.emit).toBe('function')
|
|
117
118
|
})
|
|
119
|
+
})
|
|
118
120
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
// --- loadApp ---
|
|
122
|
+
|
|
123
|
+
describe('InteractiveMap — loadApp', () => {
|
|
124
|
+
beforeEach(setupBeforeEach)
|
|
125
|
+
afterEach(() => jest.restoreAllMocks())
|
|
126
|
+
|
|
127
|
+
it('returns early without loading when _root is set or _isLoading is true', async () => {
|
|
128
|
+
shouldLoadComponent.mockReturnValue(false)
|
|
129
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
130
|
+
|
|
131
|
+
map._root = {}
|
|
132
|
+
await map.loadApp()
|
|
133
|
+
expect(initialiseApp).not.toHaveBeenCalled()
|
|
124
134
|
|
|
125
|
-
|
|
135
|
+
map._root = null
|
|
136
|
+
map._isLoading = true
|
|
137
|
+
await map.loadApp()
|
|
138
|
+
expect(initialiseApp).not.toHaveBeenCalled()
|
|
139
|
+
})
|
|
126
140
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
it('calls loadApp if shouldLoadComponent returns true', async () => {
|
|
142
|
+
shouldLoadComponent.mockReturnValue(true)
|
|
143
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
145
|
+
expect(initialiseApp).toHaveBeenCalled()
|
|
146
|
+
expect(map).toBeDefined()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('calls removeLoadingState if shouldLoadComponent returns false', () => {
|
|
150
|
+
shouldLoadComponent.mockReturnValue(false)
|
|
151
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
152
|
+
expect(removeLoadingState).toHaveBeenCalled()
|
|
153
|
+
expect(map).toBeDefined()
|
|
131
154
|
})
|
|
132
155
|
|
|
133
156
|
it('initializes reverseGeocode if reverseGeocodeProvider is provided', async () => {
|
|
157
|
+
shouldLoadComponent.mockReturnValue(false)
|
|
134
158
|
const mapProviderWithCRS = {
|
|
135
|
-
load: jest.fn().mockResolvedValue({
|
|
136
|
-
MapProvider: {},
|
|
137
|
-
mapFramework: {},
|
|
138
|
-
mapProviderConfig: { crs: 'EPSG:27700' }
|
|
139
|
-
})
|
|
159
|
+
load: jest.fn().mockResolvedValue({ MapProvider: {}, mapFramework: {}, mapProviderConfig: { crs: 'EPSG:27700' } })
|
|
140
160
|
}
|
|
141
161
|
const configWithReverse = {
|
|
142
162
|
behaviour: 'buttonFirst',
|
|
@@ -148,440 +168,365 @@ describe('InteractiveMap Core Functionality', () => {
|
|
|
148
168
|
expect(createReverseGeocode).toHaveBeenCalledWith(configWithReverse.reverseGeocodeProvider, 'EPSG:27700')
|
|
149
169
|
})
|
|
150
170
|
|
|
151
|
-
it('
|
|
152
|
-
|
|
153
|
-
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
154
|
-
await map.loadApp()
|
|
155
|
-
expect(initialiseApp).toHaveBeenCalled()
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('does not call loadApp if shouldLoadComponent returns false', () => {
|
|
159
|
-
shouldLoadComponent.mockReturnValue(false)
|
|
160
|
-
const createMap = () => new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
161
|
-
expect(createMap).not.toThrow()
|
|
162
|
-
expect(removeLoadingState).toHaveBeenCalled()
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('handles loadApp errors', async () => {
|
|
166
|
-
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
171
|
+
it('renders error and re-throws on provider failure', async () => {
|
|
172
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
167
173
|
const failingProvider = { load: jest.fn().mockRejectedValue(new Error('fail')) }
|
|
168
174
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: failingProvider, genericErrorText: 'error' })
|
|
169
|
-
|
|
170
175
|
await expect(map.loadApp()).rejects.toThrow('fail')
|
|
171
176
|
expect(renderError).toHaveBeenCalledWith(rootEl, 'error')
|
|
172
|
-
consoleErrorSpy.mockRestore()
|
|
173
177
|
})
|
|
174
178
|
|
|
175
|
-
it('does not overwrite eventBus methods
|
|
179
|
+
it('does not overwrite eventBus methods; emits APP_OPENED', async () => {
|
|
176
180
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
177
181
|
const originalOn = map.on
|
|
178
|
-
|
|
179
182
|
initialiseApp.mockResolvedValue({ _root: {}, on: jest.fn(), off: jest.fn(), emit: jest.fn(), someMethod: jest.fn() })
|
|
180
183
|
await map.loadApp()
|
|
181
|
-
|
|
182
184
|
expect(map.on).toBe(originalOn)
|
|
183
185
|
expect(typeof map.someMethod).toBe('function')
|
|
186
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: false })
|
|
184
187
|
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// --- _handleButtonClick ---
|
|
185
191
|
|
|
186
|
-
|
|
192
|
+
describe('InteractiveMap — _handleButtonClick', () => {
|
|
193
|
+
beforeEach(setupBeforeEach)
|
|
194
|
+
afterEach(() => jest.restoreAllMocks())
|
|
195
|
+
|
|
196
|
+
it('pushState and loadApp on first open', async () => {
|
|
197
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: true, mapProvider: mapProviderMock })
|
|
198
|
+
jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
199
|
+
jest.spyOn(history, 'pushState').mockImplementation(() => {})
|
|
200
|
+
await openButtonCallback({ currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } })
|
|
201
|
+
expect(map.loadApp).toHaveBeenCalled()
|
|
202
|
+
expect(history.pushState).toHaveBeenCalledWith({ isBack: true }, '', '/?mv=map')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('calls showApp (not loadApp) when map is hidden', async () => {
|
|
206
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: true, mapProvider: mapProviderMock })
|
|
207
|
+
map._isHidden = true
|
|
208
|
+
jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
209
|
+
jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
210
|
+
jest.spyOn(history, 'pushState').mockImplementation(() => {})
|
|
211
|
+
await openButtonCallback({ currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } })
|
|
212
|
+
expect(map.showApp).toHaveBeenCalled()
|
|
213
|
+
expect(map.loadApp).not.toHaveBeenCalled()
|
|
214
|
+
expect(history.pushState).toHaveBeenCalled()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('skips pushState when manageHistoryState is false', async () => {
|
|
218
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: false, mapProvider: mapProviderMock })
|
|
219
|
+
expect(map.config.manageHistoryState).toBe(false)
|
|
220
|
+
jest.spyOn(history, 'pushState').mockImplementation(() => {})
|
|
221
|
+
await openButtonCallback({ currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } })
|
|
222
|
+
expect(history.pushState).not.toHaveBeenCalled()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('shows hidden app without pushState when manageHistoryState is false', async () => {
|
|
226
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: false, mapProvider: mapProviderMock })
|
|
227
|
+
map._isHidden = true
|
|
228
|
+
jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
229
|
+
jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
230
|
+
jest.spyOn(history, 'pushState').mockImplementation(() => {})
|
|
231
|
+
await openButtonCallback({ currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } })
|
|
232
|
+
expect(map.showApp).toHaveBeenCalled()
|
|
233
|
+
expect(map.loadApp).not.toHaveBeenCalled()
|
|
234
|
+
expect(history.pushState).not.toHaveBeenCalled()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('is a no-op when app is already open (_root is set)', async () => {
|
|
238
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: true, mapProvider: mapProviderMock })
|
|
239
|
+
map._root = {}
|
|
240
|
+
jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
241
|
+
jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
242
|
+
jest.spyOn(history, 'pushState').mockImplementation(() => {})
|
|
243
|
+
await openButtonCallback({ currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } })
|
|
244
|
+
expect(map.showApp).not.toHaveBeenCalled()
|
|
245
|
+
expect(map.loadApp).not.toHaveBeenCalled()
|
|
246
|
+
expect(history.pushState).not.toHaveBeenCalled()
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// --- removeApp + destroy ---
|
|
251
|
+
|
|
252
|
+
describe('InteractiveMap — removeApp and destroy', () => {
|
|
253
|
+
beforeEach(setupBeforeEach)
|
|
254
|
+
afterEach(() => jest.restoreAllMocks())
|
|
255
|
+
|
|
256
|
+
it('removeApp: unmounts, resets button, updates DOM, emits APP_CLOSED', () => {
|
|
187
257
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
188
258
|
map._root = {}
|
|
189
259
|
map.unmount = jest.fn()
|
|
190
260
|
map._openButton = mockButtonInstance
|
|
191
|
-
|
|
192
261
|
map.removeApp()
|
|
193
|
-
|
|
194
262
|
expect(map._root).toBeNull()
|
|
195
263
|
expect(map.unmount).toHaveBeenCalled()
|
|
196
264
|
expect(mockButtonInstance.removeAttribute).toHaveBeenCalledWith('style')
|
|
197
265
|
expect(mockButtonInstance.focus).toHaveBeenCalled()
|
|
198
266
|
expect(updateDOMState).toHaveBeenCalledWith(map, { isFullscreen: false })
|
|
267
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:closed', { statePreserved: false })
|
|
199
268
|
})
|
|
200
269
|
|
|
201
|
-
it('skips unmount
|
|
202
|
-
const map = new InteractiveMap('map', {
|
|
203
|
-
map._root = null
|
|
204
|
-
map.unmount = jest.fn()
|
|
205
|
-
map._openButton = mockButtonInstance
|
|
206
|
-
|
|
207
|
-
map.removeApp()
|
|
208
|
-
|
|
209
|
-
expect(map.unmount).not.toHaveBeenCalled()
|
|
210
|
-
expect(updateDOMState).toHaveBeenCalled()
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it('skips _openButton actions if _openButton is falsy', () => {
|
|
214
|
-
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
215
|
-
map._root = { some: 'root' }
|
|
270
|
+
it('removeApp: skips unmount when _root is null; skips button when _openButton is null', () => {
|
|
271
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
216
272
|
map.unmount = jest.fn()
|
|
217
273
|
map._openButton = null
|
|
218
|
-
|
|
219
274
|
map.removeApp()
|
|
220
|
-
|
|
221
|
-
expect(map.unmount).toHaveBeenCalled()
|
|
275
|
+
expect(map.unmount).not.toHaveBeenCalled()
|
|
222
276
|
expect(updateDOMState).toHaveBeenCalled()
|
|
223
277
|
})
|
|
224
278
|
|
|
225
|
-
it('does not destroy
|
|
279
|
+
it('removeApp: does not destroy detectors (they persist for history navigation)', () => {
|
|
226
280
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
227
281
|
map._root = {}
|
|
228
282
|
map.unmount = jest.fn()
|
|
229
|
-
|
|
230
283
|
map.removeApp()
|
|
231
|
-
|
|
232
284
|
expect(mockBreakpointDetector.destroy).not.toHaveBeenCalled()
|
|
233
285
|
expect(mockInterfaceDetectorCleanup).not.toHaveBeenCalled()
|
|
234
286
|
})
|
|
235
287
|
|
|
236
|
-
it('
|
|
288
|
+
it('destroy: cleans up detectors, unregisters, calls hybrid cleanup', () => {
|
|
289
|
+
const mockCleanup = jest.fn()
|
|
290
|
+
setupBehavior.mockReturnValue(mockCleanup)
|
|
237
291
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
238
292
|
map._root = {}
|
|
239
293
|
map.unmount = jest.fn()
|
|
240
|
-
|
|
241
294
|
map.destroy()
|
|
242
|
-
|
|
243
295
|
expect(mockBreakpointDetector.destroy).toHaveBeenCalled()
|
|
244
296
|
expect(mockInterfaceDetectorCleanup).toHaveBeenCalled()
|
|
245
297
|
expect(historyManager.unregister).toHaveBeenCalledWith(map)
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
it('calls _hybridBehaviourCleanup on destroy if defined', () => {
|
|
249
|
-
const mockCleanup = jest.fn()
|
|
250
|
-
setupBehavior.mockReturnValue(mockCleanup)
|
|
251
|
-
|
|
252
|
-
const map = new InteractiveMap('map', { behaviour: 'hybrid', mapProvider: mapProviderMock })
|
|
253
|
-
map._root = {}
|
|
254
|
-
map.unmount = jest.fn()
|
|
255
|
-
|
|
256
|
-
map.destroy()
|
|
257
|
-
|
|
258
298
|
expect(mockCleanup).toHaveBeenCalled()
|
|
259
299
|
})
|
|
260
300
|
|
|
261
|
-
it('handles null _hybridBehaviourCleanup gracefully', () => {
|
|
301
|
+
it('destroy: handles null _hybridBehaviourCleanup gracefully', () => {
|
|
262
302
|
setupBehavior.mockReturnValue(null)
|
|
263
|
-
|
|
264
|
-
const map = new InteractiveMap('map', { behaviour: 'inline', mapProvider: mapProviderMock })
|
|
265
|
-
map._root = {}
|
|
266
|
-
map.unmount = jest.fn()
|
|
267
|
-
|
|
268
|
-
expect(() => map.destroy()).not.toThrow()
|
|
303
|
+
expect(() => new InteractiveMap('map', { mapProvider: mapProviderMock }).destroy()).not.toThrow()
|
|
269
304
|
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
// --- _handleExitClick ---
|
|
270
308
|
|
|
271
|
-
|
|
272
|
-
|
|
309
|
+
describe('InteractiveMap — _handleExitClick', () => {
|
|
310
|
+
beforeEach(setupBeforeEach)
|
|
311
|
+
afterEach(() => jest.restoreAllMocks())
|
|
273
312
|
|
|
313
|
+
it.each([
|
|
314
|
+
[false, 'removeApp', 'hideApp'],
|
|
315
|
+
[true, 'hideApp', 'removeApp']
|
|
316
|
+
])('preserveStateOnClose=%s: calls %s and replaces URL', (preserve, called, notCalled) => {
|
|
317
|
+
jest.spyOn(history, 'replaceState').mockImplementation(() => {})
|
|
274
318
|
const map = new InteractiveMap('map', {
|
|
275
319
|
behaviour: 'buttonFirst',
|
|
276
320
|
mapProvider: mapProviderMock,
|
|
277
321
|
mapViewQueryParam: 'mv',
|
|
278
322
|
manageHistoryState: true,
|
|
279
|
-
preserveStateOnClose:
|
|
323
|
+
preserveStateOnClose: preserve
|
|
280
324
|
})
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const hideAppSpy = jest.spyOn(map, 'hideApp').mockImplementation(() => {})
|
|
284
|
-
|
|
325
|
+
jest.spyOn(map, 'removeApp').mockImplementation(() => {})
|
|
326
|
+
jest.spyOn(map, 'hideApp').mockImplementation(() => {})
|
|
285
327
|
map._handleExitClick()
|
|
286
|
-
|
|
287
|
-
expect(
|
|
288
|
-
expect(
|
|
289
|
-
expect(replaceStateSpy).toHaveBeenCalledWith(
|
|
290
|
-
history.state,
|
|
291
|
-
'',
|
|
292
|
-
expect.any(String)
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
removeAppSpy.mockRestore()
|
|
296
|
-
hideAppSpy.mockRestore()
|
|
297
|
-
replaceStateSpy.mockRestore()
|
|
328
|
+
expect(map[called]).toHaveBeenCalled()
|
|
329
|
+
expect(map[notCalled]).not.toHaveBeenCalled()
|
|
330
|
+
expect(history.replaceState).toHaveBeenCalled()
|
|
298
331
|
})
|
|
299
332
|
|
|
300
|
-
it('
|
|
301
|
-
|
|
302
|
-
|
|
333
|
+
it('calls history.back() when history.state.isBack is true', () => {
|
|
334
|
+
jest.spyOn(history, 'back').mockImplementation(() => {})
|
|
335
|
+
Object.defineProperty(history, 'state', { value: { isBack: true }, writable: true, configurable: true })
|
|
303
336
|
const map = new InteractiveMap('map', {
|
|
304
337
|
behaviour: 'buttonFirst',
|
|
305
338
|
mapProvider: mapProviderMock,
|
|
306
339
|
mapViewQueryParam: 'mv',
|
|
307
|
-
manageHistoryState: true
|
|
308
|
-
preserveStateOnClose: true
|
|
340
|
+
manageHistoryState: true
|
|
309
341
|
})
|
|
310
|
-
|
|
311
|
-
const removeAppSpy = jest.spyOn(map, 'removeApp').mockImplementation(() => {})
|
|
312
|
-
const hideAppSpy = jest.spyOn(map, 'hideApp').mockImplementation(() => {})
|
|
313
|
-
|
|
314
342
|
map._handleExitClick()
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
expect(removeAppSpy).not.toHaveBeenCalled()
|
|
318
|
-
expect(replaceStateSpy).toHaveBeenCalled()
|
|
319
|
-
|
|
320
|
-
removeAppSpy.mockRestore()
|
|
321
|
-
hideAppSpy.mockRestore()
|
|
322
|
-
replaceStateSpy.mockRestore()
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
it('_handleButtonClick calls showApp when map is hidden', async () => {
|
|
326
|
-
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: true, mapProvider: mapProviderMock })
|
|
327
|
-
map._isHidden = true
|
|
328
|
-
const showAppSpy = jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
329
|
-
const loadAppSpy = jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
330
|
-
const pushStateSpy = jest.spyOn(history, 'pushState').mockImplementation(() => {})
|
|
331
|
-
const fakeEvent = { currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } }
|
|
332
|
-
|
|
333
|
-
await openButtonCallback(fakeEvent)
|
|
334
|
-
|
|
335
|
-
expect(showAppSpy).toHaveBeenCalled()
|
|
336
|
-
expect(loadAppSpy).not.toHaveBeenCalled()
|
|
337
|
-
expect(pushStateSpy).toHaveBeenCalled()
|
|
338
|
-
|
|
339
|
-
showAppSpy.mockRestore()
|
|
340
|
-
loadAppSpy.mockRestore()
|
|
341
|
-
pushStateSpy.mockRestore()
|
|
343
|
+
expect(history.back).toHaveBeenCalled()
|
|
344
|
+
Object.defineProperty(history, 'state', { value: null, writable: true, configurable: true })
|
|
342
345
|
})
|
|
343
346
|
|
|
344
|
-
it('
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
expect(
|
|
353
|
-
pushStateSpy.mockRestore()
|
|
347
|
+
it('skips all history when manageHistoryState is false', () => {
|
|
348
|
+
jest.spyOn(history, 'back').mockImplementation(() => {})
|
|
349
|
+
jest.spyOn(history, 'replaceState').mockImplementation(() => {})
|
|
350
|
+
const map = new InteractiveMap('map', {
|
|
351
|
+
behaviour: 'buttonFirst', mapProvider: mapProviderMock, manageHistoryState: false
|
|
352
|
+
})
|
|
353
|
+
map._handleExitClick()
|
|
354
|
+
expect(history.back).not.toHaveBeenCalled()
|
|
355
|
+
expect(history.replaceState).not.toHaveBeenCalled()
|
|
354
356
|
})
|
|
355
357
|
|
|
356
|
-
it('
|
|
357
|
-
const backSpy = jest.spyOn(history, 'back').mockImplementation(() => {})
|
|
358
|
-
Object.defineProperty(history, 'state', { value: { isBack: true }, writable: true, configurable: true })
|
|
359
|
-
|
|
358
|
+
it('calls hideApp (not removeApp) when manageHistoryState is false and preserveStateOnClose is true', () => {
|
|
360
359
|
const map = new InteractiveMap('map', {
|
|
361
360
|
behaviour: 'buttonFirst',
|
|
362
361
|
mapProvider: mapProviderMock,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
preserveStateOnClose: false
|
|
362
|
+
manageHistoryState: false,
|
|
363
|
+
preserveStateOnClose: true
|
|
366
364
|
})
|
|
365
|
+
jest.spyOn(map, 'hideApp').mockImplementation(() => {})
|
|
367
366
|
jest.spyOn(map, 'removeApp').mockImplementation(() => {})
|
|
368
|
-
|
|
369
367
|
map._handleExitClick()
|
|
370
|
-
|
|
371
|
-
expect(
|
|
372
|
-
Object.defineProperty(history, 'state', { value: null, writable: true, configurable: true })
|
|
373
|
-
backSpy.mockRestore()
|
|
368
|
+
expect(map.hideApp).toHaveBeenCalled()
|
|
369
|
+
expect(map.removeApp).not.toHaveBeenCalled()
|
|
374
370
|
})
|
|
375
371
|
|
|
376
|
-
it('
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
372
|
+
it('returns early without calling history.back() when _isClosingViaBack is already true', () => {
|
|
373
|
+
jest.spyOn(history, 'back').mockImplementation(() => {})
|
|
374
|
+
Object.defineProperty(history, 'state', { value: { isBack: true }, writable: true, configurable: true })
|
|
380
375
|
const map = new InteractiveMap('map', {
|
|
381
376
|
behaviour: 'buttonFirst',
|
|
382
377
|
mapProvider: mapProviderMock,
|
|
383
378
|
mapViewQueryParam: 'mv',
|
|
384
|
-
manageHistoryState:
|
|
385
|
-
preserveStateOnClose: false
|
|
379
|
+
manageHistoryState: true
|
|
386
380
|
})
|
|
387
|
-
|
|
388
|
-
|
|
381
|
+
map._isClosingViaBack = true
|
|
389
382
|
map._handleExitClick()
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
expect(replaceStateSpy).not.toHaveBeenCalled()
|
|
393
|
-
backSpy.mockRestore()
|
|
394
|
-
replaceStateSpy.mockRestore()
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
it('loadApp emits APP_OPENED with statePreserved: false', async () => {
|
|
398
|
-
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
399
|
-
await map.loadApp()
|
|
400
|
-
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: false })
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
it('removeApp emits APP_CLOSED with statePreserved: false', () => {
|
|
404
|
-
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
405
|
-
map._root = {}
|
|
406
|
-
map.unmount = jest.fn()
|
|
407
|
-
map.removeApp()
|
|
408
|
-
expect(map.eventBus.emit).toHaveBeenCalledWith('app:closed', { statePreserved: false })
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
it('hideApp emits APP_CLOSED with statePreserved: true', () => {
|
|
412
|
-
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
413
|
-
map.hideApp()
|
|
414
|
-
expect(map.eventBus.emit).toHaveBeenCalledWith('app:closed', { statePreserved: true })
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
it('showApp emits APP_OPENED with statePreserved: true', () => {
|
|
418
|
-
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
419
|
-
map._isHidden = true
|
|
420
|
-
map.showApp()
|
|
421
|
-
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: true })
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
it('open() shows hidden map', () => {
|
|
425
|
-
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
426
|
-
const showSpy = jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
427
|
-
map._isHidden = true
|
|
428
|
-
map.open()
|
|
429
|
-
expect(showSpy).toHaveBeenCalled()
|
|
430
|
-
showSpy.mockRestore()
|
|
383
|
+
expect(history.back).not.toHaveBeenCalled()
|
|
384
|
+
Object.defineProperty(history, 'state', { value: null, writable: true, configurable: true })
|
|
431
385
|
})
|
|
432
386
|
|
|
433
|
-
it('
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
map
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
387
|
+
it('resets _isClosingViaBack after 1000ms safety-net timeout', () => {
|
|
388
|
+
jest.useFakeTimers()
|
|
389
|
+
jest.spyOn(history, 'back').mockImplementation(() => {})
|
|
390
|
+
Object.defineProperty(history, 'state', { value: { isBack: true }, writable: true, configurable: true })
|
|
391
|
+
const map = new InteractiveMap('map', {
|
|
392
|
+
behaviour: 'buttonFirst', mapProvider: mapProviderMock, manageHistoryState: true
|
|
393
|
+
})
|
|
394
|
+
map._handleExitClick()
|
|
395
|
+
expect(map._isClosingViaBack).toBe(true)
|
|
396
|
+
jest.advanceTimersByTime(1000)
|
|
397
|
+
expect(map._isClosingViaBack).toBe(false)
|
|
398
|
+
jest.useRealTimers()
|
|
399
|
+
Object.defineProperty(history, 'state', { value: null, writable: true, configurable: true })
|
|
441
400
|
})
|
|
401
|
+
})
|
|
442
402
|
|
|
443
|
-
|
|
444
|
-
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
445
|
-
const loadSpy = jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
446
|
-
const showSpy = jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
447
|
-
map._root = {}
|
|
448
|
-
map._isHidden = false
|
|
449
|
-
map.open()
|
|
450
|
-
expect(loadSpy).not.toHaveBeenCalled()
|
|
451
|
-
expect(showSpy).not.toHaveBeenCalled()
|
|
452
|
-
loadSpy.mockRestore()
|
|
453
|
-
showSpy.mockRestore()
|
|
454
|
-
})
|
|
403
|
+
// --- hideApp / showApp ---
|
|
455
404
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
map.close()
|
|
460
|
-
expect(exitSpy).toHaveBeenCalled()
|
|
461
|
-
exitSpy.mockRestore()
|
|
462
|
-
})
|
|
405
|
+
describe('InteractiveMap — hideApp and showApp', () => {
|
|
406
|
+
beforeEach(setupBeforeEach)
|
|
407
|
+
afterEach(() => jest.restoreAllMocks())
|
|
463
408
|
|
|
464
|
-
it('hideApp
|
|
409
|
+
it('hideApp: hides element, focuses button, restores title, emits APP_CLOSED', () => {
|
|
410
|
+
document.title = 'Map View: Original Page Title'
|
|
465
411
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
466
412
|
map._openButton = mockButtonInstance
|
|
467
|
-
|
|
468
413
|
map.hideApp()
|
|
469
|
-
|
|
470
414
|
expect(map._isHidden).toBe(true)
|
|
471
415
|
expect(map.rootEl.style.display).toBe('none')
|
|
472
416
|
expect(mockButtonInstance.removeAttribute).toHaveBeenCalledWith('style')
|
|
473
417
|
expect(mockButtonInstance.focus).toHaveBeenCalled()
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
it('hideApp restores document title when it contains a map prefix', () => {
|
|
477
|
-
document.title = 'Map View: Original Page Title'
|
|
478
|
-
|
|
479
|
-
const map = new InteractiveMap('map', {
|
|
480
|
-
behaviour: 'buttonFirst',
|
|
481
|
-
mapProvider: mapProviderMock
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
map._openButton = mockButtonInstance
|
|
485
|
-
map.hideApp()
|
|
486
|
-
|
|
487
418
|
expect(document.title).toBe('Original Page Title')
|
|
419
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:closed', { statePreserved: true })
|
|
488
420
|
})
|
|
489
421
|
|
|
490
|
-
it('hideApp works
|
|
491
|
-
const map = new InteractiveMap('map', {
|
|
492
|
-
behaviour: 'buttonFirst',
|
|
493
|
-
mapProvider: mapProviderMock
|
|
494
|
-
})
|
|
495
|
-
|
|
422
|
+
it('hideApp: works without _openButton', () => {
|
|
423
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
496
424
|
map._openButton = null
|
|
497
|
-
map.hideApp()
|
|
498
|
-
|
|
425
|
+
expect(() => map.hideApp()).not.toThrow()
|
|
499
426
|
expect(map._isHidden).toBe(true)
|
|
500
|
-
expect(map.rootEl.style.display).toBe('none')
|
|
501
427
|
})
|
|
502
428
|
|
|
503
|
-
it('showApp
|
|
429
|
+
it('showApp: shows element, clears _isHidden, hides button, updates DOM, emits APP_OPENED', () => {
|
|
504
430
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
505
431
|
map._isHidden = true
|
|
506
432
|
map._openButton = mockButtonInstance
|
|
507
433
|
map.rootEl.style.display = 'none'
|
|
508
|
-
|
|
509
434
|
map.showApp()
|
|
510
|
-
|
|
511
435
|
expect(map._isHidden).toBe(false)
|
|
512
436
|
expect(map.rootEl.style.display).toBe('')
|
|
513
437
|
expect(mockButtonInstance.style.display).toBe('none')
|
|
514
438
|
expect(updateDOMState).toHaveBeenCalledWith(map)
|
|
439
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: true })
|
|
515
440
|
})
|
|
516
441
|
|
|
517
|
-
it('showApp works
|
|
518
|
-
const map = new InteractiveMap('map', {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
442
|
+
it('showApp: works without _openButton', () => {
|
|
443
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
444
|
+
map._openButton = null
|
|
445
|
+
expect(() => map.showApp()).not.toThrow()
|
|
446
|
+
expect(map._isHidden).toBe(false)
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
// --- open / close ---
|
|
451
|
+
|
|
452
|
+
describe('InteractiveMap — open and close', () => {
|
|
453
|
+
beforeEach(setupBeforeEach)
|
|
454
|
+
afterEach(() => jest.restoreAllMocks())
|
|
455
|
+
|
|
456
|
+
it('open(): shows hidden map, loads if uninitialised, no-op if already open', () => {
|
|
457
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
458
|
+
jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
459
|
+
jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
522
460
|
|
|
523
461
|
map._isHidden = true
|
|
524
|
-
map.
|
|
525
|
-
map.
|
|
462
|
+
map.open()
|
|
463
|
+
expect(map.showApp).toHaveBeenCalledTimes(1)
|
|
464
|
+
expect(map.loadApp).not.toHaveBeenCalled()
|
|
526
465
|
|
|
527
|
-
map.
|
|
466
|
+
map._isHidden = false
|
|
467
|
+
map._root = {}
|
|
468
|
+
map.open()
|
|
469
|
+
expect(map.showApp).toHaveBeenCalledTimes(1)
|
|
470
|
+
expect(map.loadApp).not.toHaveBeenCalled()
|
|
528
471
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
expect(
|
|
472
|
+
map._root = null
|
|
473
|
+
map.open()
|
|
474
|
+
expect(map.loadApp).toHaveBeenCalledTimes(1)
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('close(): delegates to _handleExitClick', () => {
|
|
478
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
479
|
+
jest.spyOn(map, '_handleExitClick').mockImplementation(() => {})
|
|
480
|
+
map.close()
|
|
481
|
+
expect(map._handleExitClick).toHaveBeenCalled()
|
|
532
482
|
})
|
|
533
483
|
})
|
|
534
484
|
|
|
485
|
+
// --- _removeMapParamFromUrl ---
|
|
486
|
+
|
|
535
487
|
describe('_removeMapParamFromUrl', () => {
|
|
488
|
+
const PARAM = 'mv'
|
|
489
|
+
const BASE = 'https://example.com/page'
|
|
536
490
|
let map
|
|
537
491
|
|
|
538
492
|
beforeEach(() => {
|
|
539
|
-
document.body.innerHTML =
|
|
540
|
-
map = new InteractiveMap('map', {
|
|
541
|
-
mapProvider: { load: jest.fn() },
|
|
542
|
-
mapViewParamKey: 'mv'
|
|
543
|
-
})
|
|
493
|
+
document.body.innerHTML = MAP_ROOT_HTML
|
|
494
|
+
map = new InteractiveMap('map', { mapProvider: { load: jest.fn() }, mapViewParamKey: PARAM })
|
|
544
495
|
})
|
|
545
496
|
|
|
546
|
-
it('removes param when followed by another param
|
|
547
|
-
|
|
548
|
-
const result = map._removeMapParamFromUrl(href, 'mv')
|
|
549
|
-
expect(result).toBe('https://example.com/page?foo=1')
|
|
497
|
+
it('removes param when followed by another param', () => {
|
|
498
|
+
expect(map._removeMapParamFromUrl(`${BASE}?${PARAM}=map&foo=1`, PARAM)).toBe(`${BASE}?foo=1`)
|
|
550
499
|
})
|
|
551
500
|
|
|
552
|
-
it('removes param when it is the last param
|
|
553
|
-
|
|
554
|
-
const result = map._removeMapParamFromUrl(href, 'mv')
|
|
555
|
-
expect(result).toBe('https://example.com/page?foo=1')
|
|
501
|
+
it('removes param when it is the last param', () => {
|
|
502
|
+
expect(map._removeMapParamFromUrl(`${BASE}?foo=1&${PARAM}=map`, PARAM)).toBe(`${BASE}?foo=1`)
|
|
556
503
|
})
|
|
557
504
|
|
|
558
505
|
it('removes lone param and trailing ?', () => {
|
|
559
|
-
|
|
560
|
-
const result = map._removeMapParamFromUrl(href, 'mv')
|
|
561
|
-
expect(result).toBe('https://example.com/page')
|
|
506
|
+
expect(map._removeMapParamFromUrl(`${BASE}?${PARAM}=map`, PARAM)).toBe(BASE)
|
|
562
507
|
})
|
|
563
508
|
|
|
564
|
-
it('returns href unchanged when param does not exist
|
|
565
|
-
const href =
|
|
566
|
-
|
|
567
|
-
expect(result).toBe(href)
|
|
509
|
+
it('returns href unchanged when param does not exist', () => {
|
|
510
|
+
const href = `${BASE}?foo=1`
|
|
511
|
+
expect(map._removeMapParamFromUrl(href, PARAM)).toBe(href)
|
|
568
512
|
})
|
|
569
513
|
})
|
|
570
514
|
|
|
571
|
-
|
|
515
|
+
// --- Public API ---
|
|
516
|
+
|
|
517
|
+
describe('InteractiveMap — Public API Methods', () => {
|
|
572
518
|
let map
|
|
573
519
|
|
|
574
520
|
beforeEach(() => {
|
|
575
521
|
jest.clearAllMocks()
|
|
576
|
-
document.body.innerHTML =
|
|
522
|
+
document.body.innerHTML = MAP_ROOT_HTML
|
|
577
523
|
map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
578
524
|
})
|
|
579
525
|
|
|
580
|
-
it('delegates
|
|
526
|
+
it('delegates EventBus and Marker API calls', () => {
|
|
581
527
|
const cb = jest.fn()
|
|
582
528
|
const coords = [10.5, 20.5]
|
|
583
529
|
const options = { color: 'red' }
|
|
584
|
-
|
|
585
530
|
map.on('testEvent', cb)
|
|
586
531
|
map.off('testEvent', cb)
|
|
587
532
|
map.emit('customEvent', 123)
|
|
@@ -589,7 +534,6 @@ describe('InteractiveMap Public API Methods', () => {
|
|
|
589
534
|
map.updateMarker('marker-1', options)
|
|
590
535
|
map.removeMarker('marker-1')
|
|
591
536
|
map.setMode('test-mode')
|
|
592
|
-
|
|
593
537
|
expect(map.eventBus.on).toHaveBeenCalledWith('testEvent', cb)
|
|
594
538
|
expect(map.eventBus.off).toHaveBeenCalledWith('testEvent', cb)
|
|
595
539
|
expect(map.eventBus.emit).toHaveBeenCalledWith('customEvent', 123)
|
|
@@ -599,50 +543,32 @@ describe('InteractiveMap Public API Methods', () => {
|
|
|
599
543
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:setmode', 'test-mode')
|
|
600
544
|
})
|
|
601
545
|
|
|
602
|
-
it('delegates
|
|
546
|
+
it('delegates Panel, Button, and Control API calls', () => {
|
|
603
547
|
const buttonConfig = { label: 'MyButton' }
|
|
604
548
|
const panelConfig = { title: 'MyPanel' }
|
|
605
549
|
const controlConfig = { type: 'zoom' }
|
|
606
|
-
|
|
607
|
-
// Existing API calls
|
|
608
550
|
map.addButton('btn1', buttonConfig)
|
|
609
551
|
map.addPanel('panel1', panelConfig)
|
|
610
552
|
map.addControl('ctrl1', controlConfig)
|
|
611
553
|
map.removePanel('panel1')
|
|
612
|
-
|
|
613
|
-
// New API calls to cover missing lines
|
|
614
554
|
map.showPanel('panel2')
|
|
615
555
|
map.hidePanel('panel3')
|
|
616
|
-
|
|
617
|
-
// Existing assertions
|
|
556
|
+
map.toggleButtonState('btn-1', 'disabled', true)
|
|
618
557
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:addbutton', { id: 'btn1', config: buttonConfig })
|
|
619
558
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:addpanel', { id: 'panel1', config: panelConfig })
|
|
620
559
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:addcontrol', { id: 'ctrl1', config: controlConfig })
|
|
621
560
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:removepanel', 'panel1')
|
|
622
|
-
|
|
623
|
-
// New assertions for coverage
|
|
624
561
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', { id: 'panel2', focus: true })
|
|
625
562
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:hidepanel', 'panel3')
|
|
563
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:togglebuttonstate', { id: 'btn-1', prop: 'disabled', value: true })
|
|
626
564
|
})
|
|
627
565
|
|
|
628
|
-
it('
|
|
629
|
-
map.toggleButtonState('btn-1', 'disabled', true)
|
|
630
|
-
|
|
631
|
-
expect(map.eventBus.emit).toHaveBeenCalledWith(
|
|
632
|
-
'app:togglebuttonstate',
|
|
633
|
-
{ id: 'btn-1', prop: 'disabled', value: true }
|
|
634
|
-
)
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
it('fitToBounds emits MAP_FIT_TO_BOUNDS with bbox', () => {
|
|
566
|
+
it('fitToBounds and setView emit correct events', () => {
|
|
638
567
|
const bbox = [-0.489, 51.28, 0.236, 51.686]
|
|
568
|
+
const center = [-0.1276, 51.5074]
|
|
639
569
|
map.fitToBounds(bbox)
|
|
570
|
+
map.setView({ center, zoom: 12 })
|
|
640
571
|
expect(map.eventBus.emitWhenReady).toHaveBeenCalledWith('map:fittobounds', bbox)
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
it('setView emits MAP_SET_VIEW with opts', () => {
|
|
644
|
-
const opts = { center: [-0.1276, 51.5074], zoom: 12 }
|
|
645
|
-
map.setView(opts)
|
|
646
|
-
expect(map.eventBus.emitWhenReady).toHaveBeenCalledWith('map:setview', opts)
|
|
572
|
+
expect(map.eventBus.emitWhenReady).toHaveBeenCalledWith('map:setview', { center, zoom: 12 })
|
|
647
573
|
})
|
|
648
574
|
})
|