@defra/interactive-map 0.0.26-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.
Files changed (92) hide show
  1. package/dist/esm/im-core.js +1 -1
  2. package/dist/esm/im-shell.js +1 -1
  3. package/dist/umd/im-core.js +1 -1
  4. package/dist/umd/index.js +1 -1
  5. package/docs/assets/css/docusaurus.scss +4 -0
  6. package/package.json +1 -1
  7. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  8. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  9. package/plugins/beta/draw-ml/src/defaults.js +14 -14
  10. package/plugins/beta/draw-ml/src/mapboxDraw.js +5 -0
  11. package/plugins/beta/draw-ml/src/mapboxSnap.js +1 -1
  12. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +10 -2
  13. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +2 -0
  14. package/plugins/beta/draw-ml/src/reducer.js +4 -1
  15. package/plugins/beta/draw-ml/src/styles.js +42 -39
  16. package/plugins/beta/draw-ol/dist/css/index.css +13 -0
  17. package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -0
  18. package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -0
  19. package/plugins/beta/draw-ol/dist/esm/geometryHelpers.js +1 -0
  20. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -0
  21. package/plugins/beta/draw-ol/dist/esm/index.js +1 -0
  22. package/plugins/beta/draw-ol/src/DrawInit.jsx +67 -0
  23. package/plugins/beta/draw-ol/src/api/addFeature.js +29 -0
  24. package/plugins/beta/draw-ol/src/api/deleteFeature.js +11 -0
  25. package/plugins/beta/draw-ol/src/api/editFeature.js +50 -0
  26. package/plugins/beta/draw-ol/src/api/newLine.js +44 -0
  27. package/plugins/beta/draw-ol/src/api/newPolygon.js +44 -0
  28. package/plugins/beta/draw-ol/src/core/OLDrawManager.js +150 -0
  29. package/plugins/beta/draw-ol/src/core/featureStore.js +65 -0
  30. package/plugins/beta/draw-ol/src/core/styles.js +98 -0
  31. package/plugins/beta/draw-ol/src/core/undoStack.js +31 -0
  32. package/plugins/beta/draw-ol/src/defaults.js +16 -0
  33. package/plugins/beta/draw-ol/src/draw/DrawMode.js +113 -0
  34. package/plugins/beta/draw-ol/src/draw/drawInput.js +199 -0
  35. package/plugins/beta/draw-ol/src/draw.scss +17 -0
  36. package/plugins/beta/draw-ol/src/edit/EditMode.js +424 -0
  37. package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +227 -0
  38. package/plugins/beta/draw-ol/src/edit/midpointLayer.js +57 -0
  39. package/plugins/beta/draw-ol/src/edit/touchHandler.js +171 -0
  40. package/plugins/beta/draw-ol/src/edit/undoOps.js +95 -0
  41. package/plugins/beta/draw-ol/src/edit/vertexHitTest.js +72 -0
  42. package/plugins/beta/draw-ol/src/edit/vertexLayer.js +49 -0
  43. package/plugins/beta/draw-ol/src/edit/vertexOps.js +99 -0
  44. package/plugins/beta/draw-ol/src/events.js +114 -0
  45. package/plugins/beta/draw-ol/src/index.js +12 -0
  46. package/plugins/beta/draw-ol/src/manifest.js +121 -0
  47. package/plugins/beta/draw-ol/src/olDraw.js +38 -0
  48. package/plugins/beta/draw-ol/src/reducer.js +41 -0
  49. package/plugins/beta/draw-ol/src/snap/snapEngine.js +109 -0
  50. package/plugins/beta/draw-ol/src/snap/snapGeometry.js +200 -0
  51. package/plugins/beta/draw-ol/src/snap/snapIndicator.js +81 -0
  52. package/plugins/beta/draw-ol/src/snap/snapInteraction.js +56 -0
  53. package/plugins/beta/draw-ol/src/snap/snapManager.js +90 -0
  54. package/plugins/beta/draw-ol/src/utils/flattenStyleProperties.js +47 -0
  55. package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +97 -0
  56. package/plugins/beta/draw-ol/src/utils/olCoords.js +35 -0
  57. package/plugins/beta/draw-ol/src/utils/resolveColors.js +39 -0
  58. package/plugins/beta/draw-ol/src/utils/spatial.js +28 -0
  59. package/plugins/beta/draw-ol/src/utils/touchTarget.js +61 -0
  60. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  61. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  62. package/plugins/interact/src/defaults.js +1 -0
  63. package/plugins/interact/src/hooks/useInteractionHandlers.js +150 -43
  64. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +240 -8
  65. package/plugins/interact/src/reducer.js +7 -0
  66. package/plugins/interact/src/reducer.test.js +16 -0
  67. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  68. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  69. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  70. package/providers/beta/openlayers/src/appEvents.js +9 -13
  71. package/providers/beta/openlayers/src/appEvents.test.js +25 -17
  72. package/providers/beta/openlayers/src/mapEvents.js +32 -16
  73. package/providers/beta/openlayers/src/mapEvents.test.js +43 -10
  74. package/providers/beta/openlayers/src/openlayersProvider.js +45 -26
  75. package/providers/beta/openlayers/src/openlayersProvider.test.js +12 -34
  76. package/providers/beta/openlayers/src/utils/highlightFeatures.js +245 -0
  77. package/providers/beta/openlayers/src/utils/hoverCursor.js +65 -0
  78. package/providers/beta/openlayers/src/utils/hoverCursor.test.js +235 -0
  79. package/providers/beta/openlayers/src/utils/openLayersFixes.js +11 -0
  80. package/providers/beta/openlayers/src/utils/openLayersFixes.test.js +53 -0
  81. package/providers/beta/openlayers/src/utils/queryFeatures.js +77 -0
  82. package/providers/beta/openlayers/src/utils/queryFeatures.test.js +181 -0
  83. package/providers/beta/openlayers/src/utils/tileLayers.js +19 -1
  84. package/providers/beta/openlayers/src/utils/tileLayers.test.js +74 -3
  85. package/providers/beta/openlayers/src/utils/vtTileFragments.js +86 -0
  86. package/rollup.esm.mjs +6 -0
  87. package/src/App/controls/keyboardActions.js +2 -2
  88. package/src/InteractiveMap/InteractiveMap.js +38 -10
  89. package/src/InteractiveMap/InteractiveMap.test.js +282 -356
  90. package/src/InteractiveMap/historyManager.js +7 -0
  91. package/src/InteractiveMap/historyManager.test.js +11 -0
  92. 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
- describe('InteractiveMap Core Functionality', () => {
59
- let rootEl, openButtonCallback
59
+ // --- Shared setup ---
60
60
 
61
- beforeEach(() => {
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
- it('throws error if root element not found', () => {
73
- expect(() => new InteractiveMap('nonexistent')).toThrow(/not found/)
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
- it('binds eventBus methods (on/off/emit)', () => {
77
- const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
78
- expect(typeof map.on).toBe('function')
79
- expect(typeof map.off).toBe('function')
80
- expect(typeof map.emit).toBe('function')
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 from constructor if device not supported', () => {
85
+ it('returns early if device not supported', () => {
84
86
  checkDeviceSupport.mockReturnValue(false)
85
- const createMap = () => new InteractiveMap('map', { mapProvider: mapProviderMock })
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
- checkDeviceSupport.mockReturnValue(true)
94
- const createMap = () => new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
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('calls breakpoint & interface detectors', () => {
100
- const createMap = () => new InteractiveMap('map', { mapProvider: mapProviderMock })
101
- expect(createMap).not.toThrow()
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('builds config from dataset and props', () => {
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 and sets up behavior', () => {
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
- it('open button click calls _handleButtonClick / loadApp', async () => {
120
- const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: true, mapProvider: mapProviderMock })
121
- const loadSpy = jest.spyOn(map, 'loadApp').mockResolvedValue()
122
- const pushStateSpy = jest.spyOn(history, 'pushState').mockImplementation(() => {})
123
- const fakeEvent = { currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } }
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
- await openButtonCallback(fakeEvent)
135
+ map._root = null
136
+ map._isLoading = true
137
+ await map.loadApp()
138
+ expect(initialiseApp).not.toHaveBeenCalled()
139
+ })
126
140
 
127
- expect(loadSpy).toHaveBeenCalled()
128
- expect(pushStateSpy).toHaveBeenCalledWith({ isBack: true }, '', '/?mv=map')
129
- loadSpy.mockRestore()
130
- pushStateSpy.mockRestore()
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('calls loadApp if shouldLoadComponent returns true', async () => {
152
- shouldLoadComponent.mockReturnValue(true)
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 when merging appInstance', async () => {
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
- it('removes component and resets DOM/button', () => {
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 if _root is falsy or unmount is not a function', () => {
202
- const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
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 breakpoint detector on removeApp (persists for history navigation)', () => {
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('destroys breakpoint detector and unregisters from historyManager on destroy', () => {
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
- it('_handleExitClick removes app when preserveStateOnClose is false', () => {
272
- const replaceStateSpy = jest.spyOn(history, 'replaceState').mockImplementation(() => {})
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: false
323
+ preserveStateOnClose: preserve
280
324
  })
281
-
282
- const removeAppSpy = jest.spyOn(map, 'removeApp').mockImplementation(() => {})
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(removeAppSpy).toHaveBeenCalled()
288
- expect(hideAppSpy).not.toHaveBeenCalled()
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('_handleExitClick hides app when preserveStateOnClose is true', () => {
301
- const replaceStateSpy = jest.spyOn(history, 'replaceState').mockImplementation(() => {})
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
- expect(hideAppSpy).toHaveBeenCalled()
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('_handleButtonClick skips pushState when manageHistoryState is false', async () => {
345
- const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: false, mapProvider: mapProviderMock })
346
- expect(map.config.manageHistoryState).toBe(false)
347
- const pushStateSpy = jest.spyOn(history, 'pushState').mockImplementation(() => {})
348
- const fakeEvent = { currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } }
349
-
350
- await openButtonCallback(fakeEvent)
351
-
352
- expect(pushStateSpy).not.toHaveBeenCalled()
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('_handleExitClick calls history.back() when history.state.isBack is true', () => {
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
- mapViewQueryParam: 'mv',
364
- manageHistoryState: true,
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(backSpy).toHaveBeenCalled()
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('_handleExitClick skips history when manageHistoryState is false', () => {
377
- const backSpy = jest.spyOn(history, 'back').mockImplementation(() => {})
378
- const replaceStateSpy = jest.spyOn(history, 'replaceState').mockImplementation(() => {})
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: false,
385
- preserveStateOnClose: false
379
+ manageHistoryState: true
386
380
  })
387
- jest.spyOn(map, 'removeApp').mockImplementation(() => {})
388
-
381
+ map._isClosingViaBack = true
389
382
  map._handleExitClick()
390
-
391
- expect(backSpy).not.toHaveBeenCalled()
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('open() loads map when not yet initialised', () => {
434
- const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
435
- const loadSpy = jest.spyOn(map, 'loadApp').mockResolvedValue()
436
- map._root = null
437
- map._isHidden = false
438
- map.open()
439
- expect(loadSpy).toHaveBeenCalled()
440
- loadSpy.mockRestore()
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
- it('open() is a no-op when already open', () => {
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
- it('close() delegates to _handleExitClick', () => {
457
- const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
458
- const exitSpy = jest.spyOn(map, '_handleExitClick').mockImplementation(() => {})
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 sets _isHidden and hides element', () => {
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 when _openButton is null', () => {
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 sets _isHidden false and shows element', () => {
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 when _openButton is null', () => {
518
- const map = new InteractiveMap('map', {
519
- behaviour: 'buttonFirst',
520
- mapProvider: mapProviderMock
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._openButton = null
525
- map.rootEl.style.display = 'none'
462
+ map.open()
463
+ expect(map.showApp).toHaveBeenCalledTimes(1)
464
+ expect(map.loadApp).not.toHaveBeenCalled()
526
465
 
527
- map.showApp()
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
- expect(map._isHidden).toBe(false)
530
- expect(map.rootEl.style.display).toBe('')
531
- expect(updateDOMState).toHaveBeenCalledWith(map)
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 = '<div id="map"></div>'
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 (& branch)', () => {
547
- const href = 'https://example.com/page?mv=map&foo=1'
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 (end-of-string branch)', () => {
553
- const href = 'https://example.com/page?foo=1&mv=map'
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
- const href = 'https://example.com/page?mv=map'
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 (no-match branch)', () => {
565
- const href = 'https://example.com/page?foo=1'
566
- const result = map._removeMapParamFromUrl(href, 'mv')
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
- describe('InteractiveMap Public API Methods', () => {
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 = '<div id="map"></div>'
522
+ document.body.innerHTML = MAP_ROOT_HTML
577
523
  map = new InteractiveMap('map', { mapProvider: mapProviderMock })
578
524
  })
579
525
 
580
- it('delegates all EventBus and Marker API calls correctly', () => {
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 addButton, addPanel, addControl, removePanel, showPanel, hidePanel correctly', () => {
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('delegates toggleButtonState correctly', () => {
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
  })