@defra/interactive-map 0.0.4-alpha → 0.0.6-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/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api.md +8 -0
- package/package.json +3 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +2 -1
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js.LICENSE.txt +1 -0
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js.LICENSE.txt +1 -0
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/datasets.js +55 -2
- package/plugins/beta/datasets/src/fetch/createDynamicSource.js +206 -0
- package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +38 -0
- package/plugins/beta/datasets/src/handleSetMapStyle.js +8 -1
- package/plugins/beta/datasets/src/mapLayers.js +41 -3
- package/plugins/beta/datasets/src/utils/bbox.js +113 -2
- package/plugins/beta/draw-ml/dist/css/index.css +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/dist/umd/index.js +1 -1
- package/plugins/beta/draw-ml/src/api/addFeature.js +15 -3
- package/plugins/beta/draw-ml/src/api/editFeature.js +16 -5
- package/plugins/beta/draw-ml/src/api/newLine.js +32 -7
- package/plugins/beta/draw-ml/src/api/newPolygon.js +33 -8
- package/plugins/beta/draw-ml/src/defaults.js +1 -0
- package/plugins/beta/draw-ml/src/draw.scss +23 -0
- package/plugins/beta/draw-ml/src/events.js +55 -113
- package/plugins/beta/draw-ml/src/manifest.js +64 -38
- package/plugins/beta/draw-ml/src/mapboxSnap.js +16 -12
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +4 -3
- package/plugins/beta/draw-ml/src/modes/editVertex/geometryHelpers.js +135 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/helpers.js +2 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +134 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +133 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +141 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +121 -0
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +51 -406
- package/plugins/beta/draw-ml/src/utils/flattenStyleProperties.js +49 -0
- package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/src/Frame.jsx +1 -1
- package/plugins/beta/map-styles/dist/css/index.css +1 -1
- package/plugins/beta/map-styles/dist/esm/index.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/map-styles/src/mapStyles.scss +4 -1
- package/plugins/interact/dist/css/index.css +1 -1
- 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/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +1 -1
- package/plugins/interact/src/defaults.js +1 -0
- package/plugins/interact/src/events.js +37 -52
- package/plugins/interact/src/events.test.js +35 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.js +80 -48
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +64 -8
- package/plugins/interact/src/interact.scss +6 -0
- package/plugins/interact/src/reducer.js +25 -27
- package/plugins/interact/src/reducer.test.js +19 -6
- package/plugins/interact/src/utils/featureQueries.js +2 -2
- package/plugins/interact/src/utils/spatial.js +99 -0
- package/plugins/interact/src/utils/spatial.test.js +100 -0
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +2 -2
- package/plugins/search/src/components/Form/Form.jsx +1 -1
- package/plugins/search/src/components/Suggestions/Suggestions.jsx +5 -5
- package/plugins/search/src/datasets.js +1 -1
- package/plugins/search/src/defaults.js +3 -0
- package/plugins/search/src/events/fetchSuggestions.js +48 -53
- package/plugins/search/src/events/formHandlers.js +3 -2
- package/plugins/search/src/events/index.js +1 -1
- package/plugins/search/src/events/inputHandlers.js +3 -1
- package/plugins/search/src/events/suggestionHandlers.js +12 -3
- package/plugins/search/src/search.scss +4 -1
- package/plugins/search/src/utils/parseOsNamesResults.js +12 -12
- package/providers/beta/esri/src/esriProvider.js +1 -1
- package/providers/beta/esri/src/index.js +1 -1
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/defaults.js +3 -2
- package/providers/maplibre/src/index.js +22 -19
- package/providers/maplibre/src/maplibreProvider.js +13 -11
- package/providers/maplibre/src/utils/detectWebgl.js +1 -2
- package/providers/maplibre/src/utils/highlightFeatures.js +78 -67
- package/providers/maplibre/src/utils/labels.js +174 -120
- package/providers/maplibre/src/utils/maplibreFixes.js +2 -2
- package/providers/maplibre/src/utils/queryFeatures.js +146 -0
- package/providers/maplibre/src/utils/spatial.js +14 -17
- package/sonar-project.properties +39 -2
- package/src/App/components/Actions/Actions.jsx +2 -2
- package/src/App/components/Actions/Actions.module.scss +2 -2
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +3 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +9 -0
- package/src/App/components/Logo/Logo.jsx +1 -1
- package/src/App/components/MapButton/MapButton.jsx +217 -41
- package/src/App/components/MapButton/MapButton.module.scss +32 -21
- package/src/App/components/MapButton/MapButton.test.jsx +128 -84
- package/src/App/components/Markers/Markers.jsx +1 -1
- package/src/App/components/Markers/Markers.module.scss +0 -5
- package/src/App/components/Panel/Panel.jsx +75 -45
- package/src/App/components/Panel/Panel.module.scss +18 -24
- package/src/App/components/Panel/Panel.test.jsx +18 -0
- package/src/App/components/PopupMenu/PopupMenu.jsx +246 -0
- package/src/App/components/PopupMenu/PopupMenu.module.scss +70 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +304 -0
- package/src/App/components/Tooltip/Tooltip.jsx +4 -2
- package/src/App/components/Tooltip/getTooltipPosition.js +1 -1
- package/src/App/components/Viewport/MapController.jsx +1 -1
- package/src/App/components/Viewport/MapStatus.jsx +1 -1
- package/src/App/components/Viewport/Viewport.jsx +2 -2
- package/src/App/components/Viewport/Viewport.module.scss +2 -2
- package/src/App/controls/keyboardActions.js +3 -2
- package/src/App/controls/keyboardShortcuts.js +4 -2
- package/src/App/hooks/useButtonStateEvaluator.js +58 -45
- package/src/App/hooks/useButtonStateEvaluator.test.js +36 -3
- package/src/App/hooks/useCrossHairAPI.js +49 -47
- package/src/App/hooks/useFocusVisible.js +2 -2
- package/src/App/hooks/useInterfaceAPI.js +16 -4
- package/src/App/hooks/useKeyboardHint.js +1 -1
- package/src/App/hooks/useKeyboardShortcuts.js +4 -2
- package/src/App/hooks/useMapAnnouncements.js +34 -32
- package/src/App/hooks/useMapEvents.js +12 -1
- package/src/App/hooks/useMapProviderOverrides.js +3 -3
- package/src/App/hooks/useMapStateSync.js +7 -1
- package/src/App/hooks/useMapURLSync.js +6 -1
- package/src/App/hooks/useMarkersAPI.js +88 -50
- package/src/App/hooks/useMediaQueryDispatch.test.js +48 -0
- package/src/App/hooks/useModalPanelBehaviour.js +46 -40
- package/src/App/hooks/useResizeObserver.js +2 -2
- package/src/App/initialiseApp.js +45 -38
- package/src/App/layout/Layout.jsx +6 -4
- package/src/App/layout/layout.module.scss +13 -17
- package/src/App/registry/keyboardShortcutRegistry.js +12 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +28 -0
- package/src/App/registry/mergeManifests.js +1 -1
- package/src/App/registry/panelRegistry.js +1 -1
- package/src/App/registry/panelRegistry.test.js +30 -1
- package/src/App/registry/pluginRegistry.js +14 -51
- package/src/App/registry/pluginRegistry.test.js +54 -6
- package/src/App/renderer/HtmlElementHost.jsx +186 -0
- package/src/App/renderer/HtmlElementHost.test.jsx +231 -0
- package/src/App/renderer/mapButtons.js +11 -8
- package/src/App/renderer/mapButtons.test.js +6 -4
- package/src/App/renderer/mapControls.js +6 -0
- package/src/App/renderer/mapControls.test.js +10 -2
- package/src/App/renderer/mapPanels.js +39 -26
- package/src/App/renderer/mapPanels.test.js +29 -0
- package/src/App/renderer/pluginWrapper.test.js +28 -0
- package/src/App/renderer/slotHelpers.js +60 -0
- package/src/App/renderer/slotHelpers.test.js +82 -0
- package/src/App/store/AppProvider.jsx +21 -18
- package/src/App/store/AppProvider.test.jsx +40 -0
- package/src/App/store/PluginProvider.jsx +7 -5
- package/src/App/store/appActionsMap.js +28 -1
- package/src/App/store/appActionsMap.test.js +26 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +12 -24
- package/src/App/store/mapReducer.js +1 -1
- package/src/InteractiveMap/InteractiveMap.js +79 -6
- package/src/InteractiveMap/InteractiveMap.test.js +164 -2
- package/src/InteractiveMap/behaviourController.js +12 -2
- package/src/InteractiveMap/behaviourController.test.js +82 -1
- package/src/InteractiveMap/deviceChecker.js +1 -1
- package/src/InteractiveMap/deviceChecker.test.js +2 -2
- package/src/InteractiveMap/historyManager.js +41 -5
- package/src/InteractiveMap/historyManager.test.js +37 -6
- package/src/InteractiveMap/polyfills.js +39 -0
- package/src/InteractiveMap/polyfills.test.js +127 -0
- package/src/config/appConfig.js +5 -5
- package/src/config/appConfig.test.js +107 -42
- package/src/config/defaults.js +1 -0
- package/src/index.umd.js +1 -1
- package/src/scss/main.scss +1 -0
- package/src/scss/settings/_colors.scss +1 -1
- package/src/scss/settings/_dimensions.scss +4 -0
- package/src/services/eventBus.test.js +24 -1
- package/src/test-utils.js +1 -0
- package/src/types.js +5 -0
- package/src/utils/detectBreakpoint.js +78 -77
- package/src/utils/detectBreakpoint.test.js +50 -4
- package/src/utils/detectInterfaceType.js +6 -5
- package/src/utils/getIsFullscreen.js +3 -1
- package/src/utils/getSafeZoneInset.js +23 -7
- package/src/utils/getSafeZoneInset.test.js +18 -0
- package/src/utils/mapStateSync.js +3 -3
- package/src/utils/queryString.js +1 -1
- package/src/utils/stringToKebab.js +1 -1
- package/src/utils/toggleInertElements.js +37 -12
- package/webpack.dev.mjs +3 -4
- package/plugins/interact/src/utils/turfHelpers.js +0 -30
- package/plugins/interact/src/utils/turfHelpers.test.js +0 -30
- package/plugins/search/src/utils/fetchDataset.js +0 -23
- package/providers/maplibre/dist/esm/im-maplibre-legacy-framework.js +0 -1
- package/providers/maplibre/dist/umd/im-maplibre-legacy-framework.js +0 -1
|
@@ -13,19 +13,25 @@ describe('historyManager', () => {
|
|
|
13
13
|
beforeEach(() => {
|
|
14
14
|
component1 = {
|
|
15
15
|
id: 'map',
|
|
16
|
-
config: { behaviour: 'buttonFirst', hybridWidth: null, maxMobileWidth: 640 },
|
|
16
|
+
config: { behaviour: 'buttonFirst', hybridWidth: null, maxMobileWidth: 640, preserveStateOnClose: false },
|
|
17
17
|
rootEl: document.createElement('div'),
|
|
18
18
|
loadApp: jest.fn(),
|
|
19
19
|
removeApp: jest.fn(),
|
|
20
|
-
|
|
20
|
+
hideApp: jest.fn(),
|
|
21
|
+
showApp: jest.fn(),
|
|
22
|
+
openButton: { focus: jest.fn() },
|
|
23
|
+
_isHidden: false
|
|
21
24
|
}
|
|
22
25
|
component2 = {
|
|
23
26
|
id: 'list',
|
|
24
|
-
config: { behaviour: 'hybrid', hybridWidth: null, maxMobileWidth: 640 },
|
|
27
|
+
config: { behaviour: 'hybrid', hybridWidth: null, maxMobileWidth: 640, preserveStateOnClose: false },
|
|
25
28
|
rootEl: document.createElement('div'),
|
|
26
29
|
loadApp: jest.fn(),
|
|
27
30
|
removeApp: jest.fn(),
|
|
28
|
-
|
|
31
|
+
hideApp: jest.fn(),
|
|
32
|
+
showApp: jest.fn(),
|
|
33
|
+
openButton: { focus: jest.fn() },
|
|
34
|
+
_isHidden: false
|
|
29
35
|
}
|
|
30
36
|
popstateEvent = new PopStateEvent('popstate')
|
|
31
37
|
jest.clearAllMocks()
|
|
@@ -63,7 +69,8 @@ describe('historyManager', () => {
|
|
|
63
69
|
expect(component1.loadApp).not.toHaveBeenCalled()
|
|
64
70
|
})
|
|
65
71
|
|
|
66
|
-
it('removes component
|
|
72
|
+
it('removes component when view param does not match and preserveStateOnClose is false', () => {
|
|
73
|
+
component1.config.preserveStateOnClose = false
|
|
67
74
|
component1.rootEl.appendChild(document.createElement('div'))
|
|
68
75
|
historyManager.register(component1)
|
|
69
76
|
queryString.getQueryParam.mockReturnValue(null)
|
|
@@ -71,7 +78,19 @@ describe('historyManager', () => {
|
|
|
71
78
|
window.dispatchEvent(popstateEvent)
|
|
72
79
|
|
|
73
80
|
expect(component1.removeApp).toHaveBeenCalled()
|
|
74
|
-
expect(component1.
|
|
81
|
+
expect(component1.hideApp).not.toHaveBeenCalled()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('hides component when view param does not match and preserveStateOnClose is true', () => {
|
|
85
|
+
component1.config.preserveStateOnClose = true
|
|
86
|
+
component1.rootEl.appendChild(document.createElement('div'))
|
|
87
|
+
historyManager.register(component1)
|
|
88
|
+
queryString.getQueryParam.mockReturnValue(null)
|
|
89
|
+
|
|
90
|
+
window.dispatchEvent(popstateEvent)
|
|
91
|
+
|
|
92
|
+
expect(component1.hideApp).toHaveBeenCalled()
|
|
93
|
+
expect(component1.removeApp).not.toHaveBeenCalled()
|
|
75
94
|
})
|
|
76
95
|
|
|
77
96
|
it('does not remove hybrid component when viewport is wide (inline mode)', () => {
|
|
@@ -87,6 +106,7 @@ describe('historyManager', () => {
|
|
|
87
106
|
})
|
|
88
107
|
|
|
89
108
|
it('removes hybrid component when viewport is narrow and view does not match', () => {
|
|
109
|
+
component2.config.preserveStateOnClose = false
|
|
90
110
|
component2.rootEl.appendChild(document.createElement('div'))
|
|
91
111
|
historyManager.register(component2)
|
|
92
112
|
queryString.getQueryParam.mockReturnValue(null)
|
|
@@ -98,6 +118,17 @@ describe('historyManager', () => {
|
|
|
98
118
|
expect(component2.removeApp).toHaveBeenCalled()
|
|
99
119
|
})
|
|
100
120
|
|
|
121
|
+
it('calls showApp when view param matches and component is hidden', () => {
|
|
122
|
+
component1._isHidden = true
|
|
123
|
+
historyManager.register(component1)
|
|
124
|
+
queryString.getQueryParam.mockReturnValue('map')
|
|
125
|
+
|
|
126
|
+
window.dispatchEvent(popstateEvent)
|
|
127
|
+
|
|
128
|
+
expect(component1.showApp).toHaveBeenCalled()
|
|
129
|
+
expect(component1.loadApp).not.toHaveBeenCalled()
|
|
130
|
+
})
|
|
131
|
+
|
|
101
132
|
it('uses hybridWidth for media query when provided', () => {
|
|
102
133
|
component2.config.hybridWidth = 768
|
|
103
134
|
component2.rootEl.appendChild(document.createElement('div'))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// crypto.randomUUID
|
|
2
|
+
if (typeof crypto !== 'undefined' && !crypto.randomUUID) {
|
|
3
|
+
let last = 0
|
|
4
|
+
crypto.randomUUID = () => {
|
|
5
|
+
last = Math.max(Date.now(), last + 1)
|
|
6
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c, i) => {
|
|
7
|
+
const v = i < 12 ? Number.parseInt(last.toString(16).padStart(12, '0')[i], 16) : Math.random() * 16 | 0 // NOSONAR
|
|
8
|
+
return (c === 'x' ? v : (v & 0x3 | 0x8)).toString(16)
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// AbortSignal.throwIfAborted
|
|
14
|
+
const needsThrowIfAborted = typeof AbortController !== 'undefined' &&
|
|
15
|
+
!Object.getPrototypeOf(new AbortController().signal).throwIfAborted
|
|
16
|
+
|
|
17
|
+
if (needsThrowIfAborted) {
|
|
18
|
+
const signalProto = Object.getPrototypeOf(new AbortController().signal)
|
|
19
|
+
signalProto.throwIfAborted = function () {
|
|
20
|
+
if (this.aborted) {
|
|
21
|
+
const err = new Error('The operation was aborted.')
|
|
22
|
+
err.name = 'AbortError'
|
|
23
|
+
throw err
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Inject polyfill into web workers created from blob URLs (e.g. MapLibre GL)
|
|
29
|
+
// Workers have their own global scope so main thread polyfills don't apply
|
|
30
|
+
if (needsThrowIfAborted && typeof URL !== 'undefined' && URL.createObjectURL) {
|
|
31
|
+
const _createObjectURL = URL.createObjectURL.bind(URL)
|
|
32
|
+
URL.createObjectURL = (blob) => {
|
|
33
|
+
if (blob instanceof Blob && blob.type === 'text/javascript') {
|
|
34
|
+
const p = 'if(typeof AbortController!=="undefined"){var _p=Object.getPrototypeOf(new AbortController().signal);if(!_p.throwIfAborted){_p.throwIfAborted=function(){if(this.aborted){var e=new Error("The operation was aborted.");e.name="AbortError";throw e}}}}\n'
|
|
35
|
+
blob = new Blob([p, blob], { type: 'text/javascript' })
|
|
36
|
+
}
|
|
37
|
+
return _createObjectURL(blob)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
describe('Polyfills', () => {
|
|
2
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
|
|
3
|
+
|
|
4
|
+
const originalCryptoUUID = crypto.randomUUID
|
|
5
|
+
const originalCreateObjectURL = URL.createObjectURL
|
|
6
|
+
const signalProto = Object.getPrototypeOf(new AbortController().signal)
|
|
7
|
+
const originalThrowIfAborted = signalProto.throwIfAborted
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.resetModules()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
Object.defineProperty(crypto, 'randomUUID', {
|
|
15
|
+
value: originalCryptoUUID,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
URL.createObjectURL = originalCreateObjectURL
|
|
21
|
+
|
|
22
|
+
if (originalThrowIfAborted) {
|
|
23
|
+
signalProto.throwIfAborted = originalThrowIfAborted
|
|
24
|
+
} else {
|
|
25
|
+
delete signalProto.throwIfAborted
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const load = () => jest.isolateModules(() => require('./polyfills.js'))
|
|
30
|
+
|
|
31
|
+
// Helper to read Blob text in environments without blob.text()
|
|
32
|
+
const readBlobText = (blob) => new Promise((resolve) => {
|
|
33
|
+
const reader = new FileReader()
|
|
34
|
+
reader.onload = () => resolve(reader.result)
|
|
35
|
+
reader.readAsText(blob)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('crypto.randomUUID', () => {
|
|
39
|
+
test('polyfills crypto.randomUUID when missing (Lines 3-8)', () => {
|
|
40
|
+
Object.defineProperty(crypto, 'randomUUID', {
|
|
41
|
+
value: undefined,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
load()
|
|
47
|
+
|
|
48
|
+
expect(typeof crypto.randomUUID).toBe('function')
|
|
49
|
+
expect(crypto.randomUUID()).toMatch(UUID_RE)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('crypto.randomUUID generates unique UUIDs', () => {
|
|
53
|
+
Object.defineProperty(crypto, 'randomUUID', {
|
|
54
|
+
value: undefined,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true
|
|
57
|
+
})
|
|
58
|
+
load()
|
|
59
|
+
const ids = new Set(Array.from({ length: 100 }, () => crypto.randomUUID()))
|
|
60
|
+
expect(ids.size).toBe(100)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('does not overwrite existing crypto.randomUUID', () => {
|
|
64
|
+
const fake = jest.fn(() => 'fake')
|
|
65
|
+
Object.defineProperty(crypto, 'randomUUID', {
|
|
66
|
+
value: fake,
|
|
67
|
+
configurable: true,
|
|
68
|
+
writable: true
|
|
69
|
+
})
|
|
70
|
+
load()
|
|
71
|
+
expect(crypto.randomUUID).toBe(fake)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('AbortSignal.throwIfAborted', () => {
|
|
76
|
+
test('throws AbortError when aborted (True branch)', () => {
|
|
77
|
+
delete signalProto.throwIfAborted
|
|
78
|
+
load()
|
|
79
|
+
const ac = new AbortController()
|
|
80
|
+
ac.abort()
|
|
81
|
+
expect(() => ac.signal.throwIfAborted()).toThrow('The operation was aborted.')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('does nothing when not aborted (False branch)', () => {
|
|
85
|
+
delete signalProto.throwIfAborted
|
|
86
|
+
load()
|
|
87
|
+
const ac = new AbortController()
|
|
88
|
+
// This call should execute line 20, see that aborted is false, and return undefined
|
|
89
|
+
expect(() => ac.signal.throwIfAborted()).not.toThrow()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('wraps URL.createObjectURL for JS blobs', async () => {
|
|
93
|
+
delete signalProto.throwIfAborted
|
|
94
|
+
|
|
95
|
+
const mockCreate = jest.fn(() => 'blob:mock')
|
|
96
|
+
URL.createObjectURL = mockCreate
|
|
97
|
+
|
|
98
|
+
load()
|
|
99
|
+
|
|
100
|
+
const content = 'console.log(1)'
|
|
101
|
+
const blob = new Blob([content], { type: 'text/javascript' })
|
|
102
|
+
const result = URL.createObjectURL(blob)
|
|
103
|
+
|
|
104
|
+
expect(result).toBe('blob:mock')
|
|
105
|
+
expect(mockCreate).toHaveBeenCalled()
|
|
106
|
+
|
|
107
|
+
const passedBlob = mockCreate.mock.calls[0][0]
|
|
108
|
+
const text = await readBlobText(passedBlob)
|
|
109
|
+
|
|
110
|
+
expect(text).toContain('throwIfAborted')
|
|
111
|
+
expect(text).toContain(content)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('does not wrap URL.createObjectURL for non-JS blobs', () => {
|
|
115
|
+
delete signalProto.throwIfAborted
|
|
116
|
+
const mockCreate = jest.fn(() => 'blob:mock')
|
|
117
|
+
URL.createObjectURL = mockCreate
|
|
118
|
+
|
|
119
|
+
load()
|
|
120
|
+
|
|
121
|
+
const blob = new Blob(['{}'], { type: 'application/json' })
|
|
122
|
+
URL.createObjectURL(blob)
|
|
123
|
+
|
|
124
|
+
expect(mockCreate).toHaveBeenCalledWith(blob)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
})
|
package/src/config/appConfig.js
CHANGED
|
@@ -18,12 +18,12 @@ const exitButtonSlots = {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Default app buttons, panels and icons
|
|
21
|
-
export const
|
|
21
|
+
export const defaultAppConfig = {
|
|
22
22
|
buttons: [{
|
|
23
23
|
id: 'exit',
|
|
24
24
|
label: 'Exit',
|
|
25
25
|
iconId: 'close',
|
|
26
|
-
onClick: (
|
|
26
|
+
onClick: (_e, { services }) => services.closeApp(),
|
|
27
27
|
excludeWhen: ({ appConfig, appState }) => !appConfig.hasExitButton || !(appState.isFullscreen && (new URL(window.location.href)).searchParams.has(appConfig.mapViewParamKey)),
|
|
28
28
|
mobile: exitButtonSlots,
|
|
29
29
|
tablet: exitButtonSlots,
|
|
@@ -32,7 +32,7 @@ export const appConfig = {
|
|
|
32
32
|
id: 'fullscreen',
|
|
33
33
|
label: () => `${document.fullscreenElement ? 'Exit' : 'Enter'} fullscreen`,
|
|
34
34
|
iconId: () => document.fullscreenElement ? 'minimise' : 'maximise',
|
|
35
|
-
onClick: (
|
|
35
|
+
onClick: (_e, { appState }) => {
|
|
36
36
|
const container = appState.layoutRefs.appContainerRef.current
|
|
37
37
|
document.fullscreenElement ? document.exitFullscreen() : container.requestFullscreen()
|
|
38
38
|
},
|
|
@@ -45,7 +45,7 @@ export const appConfig = {
|
|
|
45
45
|
group: 'zoom',
|
|
46
46
|
label: 'Zoom in',
|
|
47
47
|
iconId: 'plus',
|
|
48
|
-
onClick: (
|
|
48
|
+
onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomIn(appConfig.zoomDelta),
|
|
49
49
|
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appState.interfaceType === 'touch',
|
|
50
50
|
enableWhen: ({ mapState }) => !mapState.isAtMaxZoom,
|
|
51
51
|
mobile: buttonSlots,
|
|
@@ -56,7 +56,7 @@ export const appConfig = {
|
|
|
56
56
|
group: 'zoom',
|
|
57
57
|
label: 'Zoom out',
|
|
58
58
|
iconId: 'minus',
|
|
59
|
-
onClick: (
|
|
59
|
+
onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomOut(appConfig.zoomDelta),
|
|
60
60
|
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appState.interfaceType === 'touch',
|
|
61
61
|
enableWhen: ({ mapState }) => !mapState.isAtMinZoom,
|
|
62
62
|
mobile: buttonSlots,
|
|
@@ -1,75 +1,140 @@
|
|
|
1
1
|
import { render } from '@testing-library/react'
|
|
2
|
-
import {
|
|
2
|
+
import { defaultAppConfig, defaultButtonConfig, scaleFactor, markerSvgPaths } from './appConfig'
|
|
3
3
|
|
|
4
|
-
describe('
|
|
5
|
-
const appState = {
|
|
6
|
-
|
|
4
|
+
describe('defaultAppConfig', () => {
|
|
5
|
+
const appState = {
|
|
6
|
+
layoutRefs: { appContainerRef: { current: document.createElement('div') } },
|
|
7
|
+
isFullscreen: false,
|
|
8
|
+
interfaceType: 'mouse'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const buttons = defaultAppConfig.buttons
|
|
7
12
|
const fullscreenBtn = buttons.find(b => b.id === 'fullscreen')
|
|
8
13
|
const exitBtn = buttons.find(b => b.id === 'exit')
|
|
14
|
+
const zoomInBtn = buttons.find(b => b.id === 'zoomIn')
|
|
15
|
+
const zoomOutBtn = buttons.find(b => b.id === 'zoomOut')
|
|
9
16
|
|
|
17
|
+
// --- UI RENDER TESTS ---
|
|
10
18
|
it('renders KeyboardHelp panel', () => {
|
|
11
|
-
const panel =
|
|
19
|
+
const panel = defaultAppConfig.panels.find(p => p.id === 'keyboardHelp')
|
|
12
20
|
const { container } = render(panel.render())
|
|
13
21
|
expect(container.querySelector('.im-c-keyboard-help')).toBeInTheDocument()
|
|
14
22
|
})
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
// --- EXIT BUTTON (Line 27 Coverage) ---
|
|
25
|
+
it('covers all branches of exitBtn excludeWhen', () => {
|
|
26
|
+
const config = { hasExitButton: true, mapViewParamKey: 'view' }
|
|
27
|
+
|
|
28
|
+
expect(exitBtn.excludeWhen({
|
|
29
|
+
appConfig: { ...config, hasExitButton: false },
|
|
30
|
+
appState: { isFullscreen: true }
|
|
31
|
+
})).toBe(true)
|
|
32
|
+
|
|
33
|
+
window.history.pushState({}, '', '?view=map')
|
|
34
|
+
expect(exitBtn.excludeWhen({
|
|
35
|
+
appConfig: config,
|
|
36
|
+
appState: { isFullscreen: false }
|
|
37
|
+
})).toBe(true)
|
|
38
|
+
|
|
39
|
+
window.history.pushState({}, '', '?wrong=param')
|
|
40
|
+
expect(exitBtn.excludeWhen({
|
|
41
|
+
appConfig: config,
|
|
42
|
+
appState: { isFullscreen: true }
|
|
43
|
+
})).toBe(true)
|
|
44
|
+
|
|
45
|
+
window.history.pushState({}, '', '?view=map')
|
|
46
|
+
expect(exitBtn.excludeWhen({
|
|
47
|
+
appConfig: config,
|
|
48
|
+
appState: { isFullscreen: true }
|
|
49
|
+
})).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('calls exit button onClick correctly', () => {
|
|
53
|
+
const servicesMock = { closeApp: jest.fn() }
|
|
54
|
+
exitBtn.onClick({}, { services: servicesMock })
|
|
55
|
+
expect(servicesMock.closeApp).toHaveBeenCalled()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// --- FULLSCREEN BUTTON (Line 39 Coverage) ---
|
|
59
|
+
it('evaluates fullscreen label and icon states', () => {
|
|
60
|
+
const containerMock = { requestFullscreen: jest.fn() }
|
|
61
|
+
Object.defineProperty(document, 'fullscreenElement', { value: null, writable: true, configurable: true })
|
|
20
62
|
|
|
21
|
-
|
|
22
|
-
expect(
|
|
23
|
-
expect(fullscreenBtn.iconId({ appState, appConfig })).toBe('maximise')
|
|
63
|
+
expect(fullscreenBtn.label({ appState })).toBe('Enter fullscreen')
|
|
64
|
+
expect(fullscreenBtn.iconId({ appState })).toBe('maximise')
|
|
24
65
|
|
|
25
|
-
|
|
26
|
-
expect(
|
|
27
|
-
expect(fullscreenBtn.
|
|
66
|
+
Object.defineProperty(document, 'fullscreenElement', { value: containerMock, writable: true, configurable: true })
|
|
67
|
+
expect(fullscreenBtn.label({ appState })).toBe('Exit fullscreen')
|
|
68
|
+
expect(fullscreenBtn.iconId({ appState })).toBe('minimise')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('covers all branches of fullscreen excludeWhen', () => {
|
|
72
|
+
expect(fullscreenBtn.excludeWhen({ appConfig: { enableFullscreen: false }, appState: { isFullscreen: false } })).toBe(true)
|
|
73
|
+
expect(fullscreenBtn.excludeWhen({ appConfig: { enableFullscreen: true }, appState: { isFullscreen: true } })).toBe(true)
|
|
74
|
+
expect(fullscreenBtn.excludeWhen({ appConfig: { enableFullscreen: true }, appState: { isFullscreen: false } })).toBe(false)
|
|
28
75
|
})
|
|
29
76
|
|
|
30
77
|
it('calls fullscreen onClick correctly', () => {
|
|
31
78
|
const containerMock = { requestFullscreen: jest.fn() }
|
|
32
|
-
const appStateMock = { layoutRefs: { appContainerRef: { current: containerMock } }
|
|
33
|
-
|
|
34
|
-
Object.defineProperty(document, 'fullscreenElement', { value: null, writable: true })
|
|
79
|
+
const appStateMock = { layoutRefs: { appContainerRef: { current: containerMock } } }
|
|
35
80
|
document.exitFullscreen = jest.fn()
|
|
36
81
|
|
|
37
|
-
|
|
82
|
+
Object.defineProperty(document, 'fullscreenElement', { value: null, writable: true, configurable: true })
|
|
38
83
|
fullscreenBtn.onClick({}, { appState: appStateMock })
|
|
39
84
|
expect(containerMock.requestFullscreen).toHaveBeenCalled()
|
|
40
|
-
expect(document.exitFullscreen).not.toHaveBeenCalled()
|
|
41
85
|
|
|
42
|
-
|
|
43
|
-
Object.defineProperty(document, 'fullscreenElement', { value: containerMock })
|
|
86
|
+
Object.defineProperty(document, 'fullscreenElement', { value: containerMock, writable: true, configurable: true })
|
|
44
87
|
fullscreenBtn.onClick({}, { appState: appStateMock })
|
|
45
88
|
expect(document.exitFullscreen).toHaveBeenCalled()
|
|
46
89
|
})
|
|
47
90
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
91
|
+
// --- ZOOM BUTTONS (Line 60 & 70 Coverage) ---
|
|
92
|
+
it('covers all branches of zoom excludeWhen for BOTH buttons', () => {
|
|
93
|
+
// We run this test for both ZoomIn and ZoomOut to ensure
|
|
94
|
+
// identical lines in both button configs are covered.
|
|
95
|
+
[zoomInBtn, zoomOutBtn].forEach((btn) => {
|
|
96
|
+
// Branch A: First part of OR is true (!enableZoomControls)
|
|
97
|
+
expect(btn.excludeWhen({
|
|
98
|
+
appConfig: { enableZoomControls: false },
|
|
99
|
+
appState: { interfaceType: 'mouse' }
|
|
100
|
+
})).toBe(true)
|
|
101
|
+
|
|
102
|
+
// Branch B: Second part of OR is true (interfaceType === 'touch')
|
|
103
|
+
expect(btn.excludeWhen({
|
|
104
|
+
appConfig: { enableZoomControls: true },
|
|
105
|
+
appState: { interfaceType: 'touch' }
|
|
106
|
+
})).toBe(true)
|
|
107
|
+
|
|
108
|
+
// Branch C: Both parts are false (Result: false)
|
|
109
|
+
expect(btn.excludeWhen({
|
|
110
|
+
appConfig: { enableZoomControls: true },
|
|
111
|
+
appState: { interfaceType: 'mouse' }
|
|
112
|
+
})).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
55
115
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(
|
|
59
|
-
expect(
|
|
116
|
+
it('evaluates zoom enableWhen logic', () => {
|
|
117
|
+
expect(zoomInBtn.enableWhen({ mapState: { isAtMaxZoom: true } })).toBe(false)
|
|
118
|
+
expect(zoomInBtn.enableWhen({ mapState: { isAtMaxZoom: false } })).toBe(true)
|
|
119
|
+
expect(zoomOutBtn.enableWhen({ mapState: { isAtMinZoom: true } })).toBe(false)
|
|
120
|
+
expect(zoomOutBtn.enableWhen({ mapState: { isAtMinZoom: false } })).toBe(true)
|
|
60
121
|
})
|
|
61
122
|
|
|
62
|
-
it('
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const handleExitClickMock = jest.fn()
|
|
123
|
+
it('triggers mapProvider zoom methods on click', () => {
|
|
124
|
+
const mapProviderMock = { zoomIn: jest.fn(), zoomOut: jest.fn() }
|
|
125
|
+
const appConfigMock = { zoomDelta: 2 }
|
|
66
126
|
|
|
67
|
-
|
|
127
|
+
zoomInBtn.onClick({}, { mapProvider: mapProviderMock, appConfig: appConfigMock })
|
|
128
|
+
expect(mapProviderMock.zoomIn).toHaveBeenCalledWith(2)
|
|
68
129
|
|
|
69
|
-
|
|
70
|
-
expect(
|
|
130
|
+
zoomOutBtn.onClick({}, { mapProvider: mapProviderMock, appConfig: appConfigMock })
|
|
131
|
+
expect(mapProviderMock.zoomOut).toHaveBeenCalledWith(2)
|
|
132
|
+
})
|
|
71
133
|
|
|
72
|
-
|
|
73
|
-
|
|
134
|
+
// --- SUPPLEMENTARY CONFIGS ---
|
|
135
|
+
it('exports supplementary configs and constants', () => {
|
|
136
|
+
expect(defaultButtonConfig.label).toBe('Button')
|
|
137
|
+
expect(scaleFactor.large).toBe(2)
|
|
138
|
+
expect(markerSvgPaths[0].shape).toBe('pin')
|
|
74
139
|
})
|
|
75
140
|
})
|
package/src/config/defaults.js
CHANGED
package/src/index.umd.js
CHANGED
|
@@ -6,7 +6,7 @@ import * as JSXRuntime from 'react/jsx-runtime'
|
|
|
6
6
|
|
|
7
7
|
import InteractiveMap from './index.js'
|
|
8
8
|
|
|
9
|
-
const g = typeof window
|
|
9
|
+
const g = typeof window === 'undefined' ? globalThis : window
|
|
10
10
|
|
|
11
11
|
// Create `defra` namespace if missing
|
|
12
12
|
g.defra = g.defra || {}
|
package/src/scss/main.scss
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
@use '../App/components/Panel/Panel.module';
|
|
22
22
|
@use '../App/components/Viewport/Viewport.module';
|
|
23
23
|
@use '../App/components/Tooltip/Tooltip.module';
|
|
24
|
+
@use '../App/components/PopupMenu/PopupMenu.module';
|
|
24
25
|
@use '../App/components/Attributions/Attributions.module';
|
|
25
26
|
@use '../App/components/Logo/Logo.module';
|
|
26
27
|
@use '../App/components/CrossHair/CrossHair.module';
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
--banner-max-desktop-width: 400px;
|
|
38
38
|
--banner-collapse-margin: 14px;
|
|
39
39
|
|
|
40
|
+
// Popup menu
|
|
41
|
+
--popup-menu-min-width: 200px;
|
|
42
|
+
--popup-menu-max-width: 300px;
|
|
43
|
+
|
|
40
44
|
// Action bar (Needs to fit evently if tools are in right hand column)
|
|
41
45
|
--action-bar-max-width: calc(100% - (var(--button-size) * 2) - (var(--primary-gap) * 4));
|
|
42
46
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/services/eventBus.test.js
|
|
2
|
-
import eventBus from './eventBus.js'
|
|
2
|
+
import eventBus, { createEventBus } from './eventBus.js'
|
|
3
3
|
|
|
4
4
|
describe('EventBus singleton', () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -93,3 +93,26 @@ describe('EventBus singleton', () => {
|
|
|
93
93
|
expect(eventBus.events).toEqual({})
|
|
94
94
|
})
|
|
95
95
|
})
|
|
96
|
+
|
|
97
|
+
describe('createEventBus factory', () => {
|
|
98
|
+
/**
|
|
99
|
+
* Test to ensure coverage for the factory function (Line 50).
|
|
100
|
+
* Validates that createEventBus returns a fresh, working EventBus instance.
|
|
101
|
+
*/
|
|
102
|
+
it('creates a new, independent EventBus instance', () => {
|
|
103
|
+
const newBus = createEventBus()
|
|
104
|
+
const handler = jest.fn()
|
|
105
|
+
|
|
106
|
+
// Verify it is an instance of the same logic
|
|
107
|
+
expect(newBus).toHaveProperty('on')
|
|
108
|
+
expect(newBus).toHaveProperty('emit')
|
|
109
|
+
|
|
110
|
+
// Verify it is independent of the singleton
|
|
111
|
+
newBus.on('instanceTest', handler)
|
|
112
|
+
eventBus.emit('instanceTest', 'data') // Emit on singleton
|
|
113
|
+
expect(handler).not.toHaveBeenCalled()
|
|
114
|
+
|
|
115
|
+
newBus.emit('instanceTest', 'data') // Emit on the new instance
|
|
116
|
+
expect(handler).toHaveBeenCalledWith('data')
|
|
117
|
+
})
|
|
118
|
+
})
|
package/src/test-utils.js
CHANGED
|
@@ -48,6 +48,7 @@ export const createMockAppState = (overrides = {}) => {
|
|
|
48
48
|
disabledButtons: new Set(),
|
|
49
49
|
hiddenButtons: new Set(),
|
|
50
50
|
pressedButtons: new Set(),
|
|
51
|
+
expandedButtons: new Set(),
|
|
51
52
|
buttonConfig: registries.buttonRegistry.getButtonConfig(),
|
|
52
53
|
panelConfig: registries.panelRegistry.getPanelConfig(),
|
|
53
54
|
controlConfig: registries.controlRegistry.getControlConfig(),
|
package/src/types.js
CHANGED
|
@@ -568,6 +568,11 @@
|
|
|
568
568
|
* @property {PluginDescriptor[]} [plugins]
|
|
569
569
|
* Plugins to load.
|
|
570
570
|
*
|
|
571
|
+
* @property {boolean} [preserveStateOnClose=false]
|
|
572
|
+
* Whether to preserve the map state when closed via back button or exit button.
|
|
573
|
+
* When true, the map is hidden but not destroyed, preserving markers, zoom, etc.
|
|
574
|
+
* Useful for list/map toggle scenarios. Only applies to 'hybrid' and 'buttonFirst' behaviours.
|
|
575
|
+
*
|
|
571
576
|
* @property {boolean} [readMapText=false]
|
|
572
577
|
* Whether map text labels can be selected and read aloud by assistive technologies.
|
|
573
578
|
*
|