@defra/interactive-map 0.0.43-fmp-experimental → 0.0.44-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 (275) hide show
  1. package/assets/templates/add-polygons.njk +5 -1
  2. package/assets/templates/add-symbols.njk +5 -1
  3. package/dist/css/index.css +1 -1
  4. package/dist/esm/im-core.js +1 -1
  5. package/dist/esm/im-shell.js +1 -1
  6. package/dist/umd/im-core.js +1 -1
  7. package/dist/umd/index.js +1 -1
  8. package/docs/api/context.md +24 -0
  9. package/docs/api/keyboard-shortcut-definition.md +102 -0
  10. package/docs/api.md +18 -8
  11. package/docs/building-a-plugin.md +1 -0
  12. package/docs/plugins/draw.md +2 -2
  13. package/docs/plugins/plugin-manifest.md +9 -0
  14. package/package.json +4 -2
  15. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  16. package/plugins/beta/draw-es/src/api/newPolygon.js +1 -0
  17. package/plugins/beta/draw-es/src/events.js +17 -8
  18. package/plugins/beta/draw-es/src/reducer.js +8 -1
  19. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  20. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  21. package/plugins/beta/draw-ol/dist/css/index.css +1 -13
  22. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  23. package/plugins/beta/frame/dist/css/index.css +1 -11
  24. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  25. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  26. package/plugins/beta/frame/src/Frame.jsx +28 -2
  27. package/plugins/beta/map-styles/dist/css/index.css +1 -1
  28. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  29. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  30. package/plugins/beta/scale-bar/dist/css/index.css +1 -8
  31. package/plugins/datasets/dist/css/5648.index.css +1 -5
  32. package/plugins/datasets/dist/css/index.css +1 -1
  33. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  34. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  35. package/plugins/datasets/src/components/LayersMenu/Layers.module.scss +1 -0
  36. package/plugins/datasets/src/initialise/DatasetsInit.jsx +2 -2
  37. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +6 -5
  38. package/plugins/datasets/src/registry/dataset.js +8 -12
  39. package/plugins/datasets/src/registry/dataset.test.js +17 -0
  40. package/plugins/datasets/src/registry/datasetRegistry.js +4 -5
  41. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  42. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  43. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  44. package/plugins/draw/dist/esm/index.js +1 -1
  45. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -1
  46. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  47. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  48. package/plugins/draw/dist/umd/index.js +1 -1
  49. package/plugins/draw/src/DrawInit.jsx +41 -26
  50. package/plugins/draw/src/DrawInit.test.jsx +55 -12
  51. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +113 -114
  52. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +82 -9
  53. package/plugins/draw/src/adapters/maplibre/drawEvents.js +3 -1
  54. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +1 -1
  55. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +18 -9
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +9 -1
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +11 -1
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +23 -2
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +53 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +29 -19
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +17 -2
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +1 -1
  63. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +35 -29
  64. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +24 -6
  65. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +22 -20
  66. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +1 -1
  67. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +2 -2
  68. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +3 -3
  69. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +31 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +8 -1
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +38 -30
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +20 -3
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +5 -5
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +2 -2
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +2 -2
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +3 -3
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -7
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -4
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +14 -14
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +12 -32
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +10 -10
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +48 -22
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +74 -6
  84. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +2 -2
  85. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +6 -0
  86. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +9 -0
  87. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +15 -2
  88. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +27 -1
  89. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +1 -1
  90. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +40 -17
  91. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +55 -1
  92. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +13 -7
  93. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +7 -15
  94. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +36 -4
  95. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +41 -1
  96. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +35 -25
  97. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +40 -2
  98. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +2 -2
  99. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +2 -2
  100. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +40 -54
  101. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +32 -13
  102. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +1 -1
  103. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +17 -1
  104. package/plugins/draw/src/events.js +4 -4
  105. package/plugins/draw/src/events.test.js +1 -1
  106. package/plugins/draw/src/hooks/useSpatialList.js +180 -0
  107. package/plugins/draw/src/hooks/useSpatialList.test.js +339 -0
  108. package/plugins/draw/src/manifest.js +4 -1
  109. package/plugins/draw/src/manifest.test.js +19 -7
  110. package/plugins/draw/src/utils/spatial.js +1 -31
  111. package/plugins/draw/src/utils/spatial.test.js +1 -24
  112. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  113. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  114. package/plugins/interact/dist/umd/index.js +1 -1
  115. package/plugins/interact/src/InteractInit.jsx +2 -2
  116. package/plugins/interact/src/InteractInit.test.js +3 -3
  117. package/plugins/interact/src/events.js +11 -2
  118. package/plugins/interact/src/events.test.js +32 -0
  119. package/plugins/interact/src/hooks/useAttachEvents.js +20 -0
  120. package/plugins/interact/src/hooks/useAttachEvents.test.js +44 -1
  121. package/plugins/interact/src/hooks/useCrossHairVisibility.js +9 -4
  122. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
  123. package/plugins/interact/src/hooks/useSpatialList.js +251 -0
  124. package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +216 -70
  125. package/plugins/interact/src/manifest.js +12 -1
  126. package/plugins/interact/src/manifest.test.js +20 -0
  127. package/plugins/interact/src/utils/spatial.js +24 -1
  128. package/plugins/interact/src/utils/spatial.test.js +26 -1
  129. package/plugins/map-key/dist/css/index.css +1 -106
  130. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
  131. package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
  132. package/plugins/map-key/dist/umd/index.js +1 -1
  133. package/plugins/map-key/src/components/Key/Key.jsx +3 -4
  134. package/plugins/map-key/src/components/Key/Key.module.scss +10 -8
  135. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +2 -2
  136. package/plugins/map-key/src/components/Key/KeyItem.jsx +2 -2
  137. package/plugins/map-key/src/components/Key/KeySvg.jsx +9 -7
  138. package/plugins/map-key/src/components/Key/KeySvg.test.jsx +4 -4
  139. package/plugins/map-key/src/components/Key/MapKey.jsx +5 -3
  140. package/plugins/map-key/src/manifest.js +1 -1
  141. package/plugins/map-key/src/utils/groupStyles.js +7 -0
  142. package/plugins/map-key/src/utils/mergeKeyGroupItems.js +12 -0
  143. package/plugins/map-key/src/utils/mergeKeyGroupItems.test.js +53 -0
  144. package/plugins/menu/dist/css/index.css +1 -80
  145. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -1
  146. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -1
  147. package/plugins/menu/dist/umd/index.js +1 -1
  148. package/plugins/menu/src/initialise/MenuInit.jsx +15 -3
  149. package/plugins/menu/src/initialise/MenuInit.test.jsx +25 -0
  150. package/plugins/menu/src/initialise/createMenu.js +3 -1
  151. package/plugins/menu/src/initialise/createMenu.test.js +20 -5
  152. package/plugins/menu/src/initialise/updateUrl.js +44 -0
  153. package/plugins/menu/src/initialise/updateUrl.test.js +91 -0
  154. package/plugins/menu/src/reducers/menuStateReducer.js +19 -1
  155. package/plugins/menu/src/reducers/menuStateReducer.test.js +53 -20
  156. package/plugins/menu/src/reducers/pluginState.js +1 -6
  157. package/plugins/menu/src/reducers/pluginState.test.js +0 -25
  158. package/plugins/search/dist/css/index.css +1 -1
  159. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  160. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  161. package/plugins/search/src/manifest.js +5 -9
  162. package/plugins/search/src/manifest.test.js +5 -0
  163. package/providers/beta/esri/dist/css/index.css +1 -34
  164. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  165. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  166. package/providers/beta/openlayers/src/openlayersProvider.js +3 -3
  167. package/providers/beta/openlayers/src/openlayersProvider.test.js +6 -3
  168. package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -0
  169. package/providers/beta/openlayers/src/utils/queryFeatures.test.js +122 -1
  170. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  171. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  172. package/providers/maplibre/dist/umd/index.js +1 -1
  173. package/providers/maplibre/src/defaults.test.js +26 -0
  174. package/providers/maplibre/src/utils/labels.js +2 -2
  175. package/providers/maplibre/src/utils/labels.test.js +5 -5
  176. package/providers/maplibre/src/utils/spatial.js +0 -57
  177. package/providers/maplibre/src/utils/spatial.test.js +0 -27
  178. package/rollup.esm.mjs +2 -1
  179. package/scripts/publish-package.sh +36 -0
  180. package/src/App/App.jsx +1 -1
  181. package/src/App/App.test.jsx +6 -1
  182. package/src/App/components/CrossHair/CrossHair.jsx +25 -11
  183. package/src/App/components/CrossHair/CrossHair.module.scss +37 -12
  184. package/src/App/components/CrossHair/CrossHair.test.jsx +40 -11
  185. package/src/App/components/Hints/Hints.jsx +3 -3
  186. package/src/App/components/Hints/Hints.test.jsx +3 -3
  187. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +4 -1
  188. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +17 -0
  189. package/src/App/components/MapButton/MapButton.jsx +19 -7
  190. package/src/App/components/MapButton/MapButton.module.scss +1 -2
  191. package/src/App/components/MapButton/MapButton.test.jsx +42 -4
  192. package/src/App/components/{MoveControls/MoveControls.jsx → MapControls/MapControls.jsx} +10 -10
  193. package/src/App/components/{MoveControls/MoveControls.module.scss → MapControls/MapControls.module.scss} +25 -25
  194. package/src/App/components/{MoveControls/MoveControls.test.jsx → MapControls/MapControls.test.jsx} +48 -48
  195. package/src/App/components/Markers/Markers.jsx +2 -2
  196. package/src/App/components/Markers/Markers.test.jsx +3 -3
  197. package/src/App/components/Markers/SymbolMarker.jsx +2 -3
  198. package/src/App/components/Markers/SymbolMarker.test.jsx +8 -9
  199. package/src/App/components/Panel/Panel.jsx +19 -20
  200. package/src/App/components/Panel/Panel.module.scss +5 -0
  201. package/src/App/components/Panel/Panel.test.jsx +69 -4
  202. package/src/App/components/PopupMenu/PopupMenu.jsx +7 -3
  203. package/src/App/components/PopupMenu/PopupMenu.test.jsx +71 -0
  204. package/src/App/components/PopupMenu/usePopupMenu.js +10 -9
  205. package/src/App/components/Viewport/MapStatus.jsx +1 -1
  206. package/src/App/components/Viewport/SpatialList.jsx +47 -0
  207. package/src/App/components/Viewport/{Features.module.scss → SpatialList.module.scss} +6 -6
  208. package/src/App/components/Viewport/SpatialList.test.jsx +285 -0
  209. package/src/App/components/Viewport/Viewport.jsx +21 -11
  210. package/src/App/components/Viewport/Viewport.test.jsx +18 -2
  211. package/src/App/controls/keyboardMappings.js +7 -2
  212. package/src/App/controls/keyboardMappings.test.js +21 -0
  213. package/src/App/controls/keyboardShortcuts.js +16 -2
  214. package/src/App/controls/keyboardShortcuts.test.js +21 -0
  215. package/src/App/hooks/useCrossHairAPI.js +29 -1
  216. package/src/App/hooks/useCrossHairAPI.test.js +17 -1
  217. package/src/App/hooks/useHintsAPI.js +5 -5
  218. package/src/App/hooks/useInterfaceAPI.js +2 -1
  219. package/src/App/hooks/useInterfaceAPI.test.js +9 -0
  220. package/src/App/hooks/useKeyboardShortcuts.js +66 -35
  221. package/src/App/hooks/useKeyboardShortcuts.test.js +52 -0
  222. package/src/App/hooks/useModalPanelBehaviour.js +48 -34
  223. package/src/App/hooks/useModalPanelBehaviour.test.js +59 -0
  224. package/src/App/hooks/useSpatialListFocus.js +296 -0
  225. package/src/App/hooks/useSpatialListFocus.test.js +957 -0
  226. package/src/App/hooks/useSpatialListItems.js +51 -0
  227. package/src/App/hooks/useSpatialListItems.test.js +217 -0
  228. package/src/App/initialiseApp.js +6 -3
  229. package/src/App/initialiseApp.test.js +11 -0
  230. package/src/App/layout/Layout.jsx +4 -2
  231. package/src/App/layout/Layout.test.jsx +21 -0
  232. package/src/App/layout/layout.module.scss +7 -1
  233. package/src/App/registry/keyboardShortcutRegistry.js +2 -2
  234. package/src/App/registry/keyboardShortcutRegistry.test.js +16 -0
  235. package/src/App/registry/pluginRegistry.js +13 -0
  236. package/src/App/registry/pluginRegistry.test.js +15 -0
  237. package/src/App/registry/spatialListRegistry.js +104 -0
  238. package/src/App/registry/spatialListRegistry.test.js +218 -0
  239. package/src/App/renderer/HtmlElementHost.jsx +12 -32
  240. package/src/App/renderer/mapButtons.js +4 -0
  241. package/src/App/renderer/mapButtons.test.js +20 -0
  242. package/src/App/renderer/mapPanels.js +33 -53
  243. package/src/App/renderer/mapPanels.test.js +29 -4
  244. package/src/App/renderer/slotHelpers.js +33 -0
  245. package/src/App/renderer/slotHelpers.test.js +79 -2
  246. package/src/App/store/AppProvider.jsx +3 -2
  247. package/src/App/store/ServiceProvider.jsx +5 -3
  248. package/src/App/store/ServiceProvider.test.jsx +20 -1
  249. package/src/InteractiveMap/InteractiveMap.js +13 -2
  250. package/src/InteractiveMap/InteractiveMap.test.js +33 -0
  251. package/src/config/appConfig.js +16 -16
  252. package/src/config/appConfig.test.js +19 -19
  253. package/src/config/defaults.js +2 -2
  254. package/src/config/events.js +8 -8
  255. package/src/scss/main.scss +2 -2
  256. package/src/test-utils.js +8 -0
  257. package/src/types.js +51 -9
  258. package/src/utils/findNearestItemInDirection.js +29 -0
  259. package/src/utils/findNearestItemInDirection.test.js +67 -0
  260. package/src/utils/getPanelElementId.js +4 -0
  261. package/src/utils/getPanelElementId.test.js +11 -0
  262. package/src/utils/getPanelRole.js +22 -0
  263. package/src/utils/getPanelRole.test.js +56 -0
  264. package/src/utils/globalAltShortcuts.js +12 -0
  265. package/src/utils/globalAltShortcuts.test.js +27 -0
  266. package/src/utils/spatialNavigate.js +49 -0
  267. package/src/utils/spatialNavigate.test.js +42 -0
  268. package/webpack.umd.mjs +6 -1
  269. package/plugins/interact/src/hooks/useMapItemList.js +0 -187
  270. package/src/App/components/Viewport/Features.jsx +0 -35
  271. package/src/App/components/Viewport/Features.test.jsx +0 -124
  272. package/src/App/hooks/useFeatureFocus.js +0 -199
  273. package/src/App/hooks/useFeatureFocus.test.js +0 -635
  274. package/src/App/hooks/useFeatureItems.js +0 -39
  275. package/src/App/hooks/useFeatureItems.test.js +0 -155
@@ -0,0 +1,56 @@
1
+ import { classifyPanel, getPanelRole } from './getPanelRole.js'
2
+
3
+ describe('classifyPanel', () => {
4
+ it('is a dialog when dismissible and not a persistent side aside', () => {
5
+ expect(classifyPanel({ slot: 'right-top', open: false })).toMatchObject({
6
+ isModal: false,
7
+ isAside: false,
8
+ isDismissible: true,
9
+ isDialog: true
10
+ })
11
+ })
12
+
13
+ it('is an aside (not a dialog) for a persistently open side panel', () => {
14
+ expect(classifyPanel({ slot: 'side', open: true })).toMatchObject({
15
+ isAside: true,
16
+ isDialog: false
17
+ })
18
+ })
19
+
20
+ it('is never an aside when modal, even in the side slot', () => {
21
+ expect(classifyPanel({ slot: 'side', open: true, modal: true })).toMatchObject({
22
+ isModal: true,
23
+ isAside: false,
24
+ isDismissible: true,
25
+ isDialog: true
26
+ })
27
+ })
28
+
29
+ it('treats dismissible: false as non-dismissible unless modal forces it', () => {
30
+ expect(classifyPanel({ slot: 'right-top', open: false, dismissible: false })).toMatchObject({
31
+ isDismissible: false,
32
+ isDialog: false
33
+ })
34
+ })
35
+
36
+ it('modal forces dismissible regardless of an explicit dismissible: false', () => {
37
+ expect(classifyPanel({ slot: 'right-top', open: false, dismissible: false, modal: true })).toMatchObject({
38
+ isDismissible: true,
39
+ isDialog: true
40
+ })
41
+ })
42
+ })
43
+
44
+ describe('getPanelRole', () => {
45
+ it('returns dialog when isDialog', () => {
46
+ expect(getPanelRole({ isDialog: true, isDismissible: true })).toBe('dialog')
47
+ })
48
+
49
+ it('returns complementary when dismissible but not a dialog', () => {
50
+ expect(getPanelRole({ isDialog: false, isDismissible: true })).toBe('complementary')
51
+ })
52
+
53
+ it('returns region when neither a dialog nor dismissible', () => {
54
+ expect(getPanelRole({ isDialog: false, isDismissible: false })).toBe('region')
55
+ })
56
+ })
@@ -0,0 +1,12 @@
1
+ // Alt+<key> keyup combos useKeyboardShortcuts.js binds globally (see keyboardMappings.js's
2
+ // `keyup` section) — a locally-focused context using the same combo must shadow it, or both
3
+ // fire. Single source of truth so a new global mapping is a visible prompt to check for that.
4
+ export const GLOBAL_ALT_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter'])
5
+
6
+ // Call unconditionally from a local keyup handler while its scope is active — it only acts
7
+ // when the key is actually shadowed, so it's safe to call regardless of which key was pressed.
8
+ export const stopIfGlobalAltKey = (event) => {
9
+ if (event.altKey && GLOBAL_ALT_KEYS.has(event.key)) {
10
+ event.stopPropagation()
11
+ }
12
+ }
@@ -0,0 +1,27 @@
1
+ import { GLOBAL_ALT_KEYS, stopIfGlobalAltKey } from './globalAltShortcuts.js'
2
+
3
+ describe('GLOBAL_ALT_KEYS', () => {
4
+ test('lists the arrow keys and Enter — the combos keyboardMappings.js binds globally under Alt', () => {
5
+ expect([...GLOBAL_ALT_KEYS].sort()).toEqual(['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'Enter'].sort())
6
+ })
7
+ })
8
+
9
+ describe('stopIfGlobalAltKey', () => {
10
+ test.each(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter'])('stops propagation for Alt+%s', (key) => {
11
+ const event = { altKey: true, key, stopPropagation: jest.fn() }
12
+ stopIfGlobalAltKey(event)
13
+ expect(event.stopPropagation).toHaveBeenCalled()
14
+ })
15
+
16
+ test('leaves the same keys alone without Alt held', () => {
17
+ const event = { altKey: false, key: 'ArrowUp', stopPropagation: jest.fn() }
18
+ stopIfGlobalAltKey(event)
19
+ expect(event.stopPropagation).not.toHaveBeenCalled()
20
+ })
21
+
22
+ test('leaves an Alt+<other key> alone — nothing global uses this combo', () => {
23
+ const event = { altKey: true, key: 'i', stopPropagation: jest.fn() }
24
+ stopIfGlobalAltKey(event)
25
+ expect(event.stopPropagation).not.toHaveBeenCalled()
26
+ })
27
+ })
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Given a starting screen pixel and a set of candidate pixels, finds the index of the
3
+ * candidate that's nearest to `start` in the given cardinal direction — the shared primitive
4
+ * behind every directional "move to the next spatially-adjacent thing" interaction in this
5
+ * codebase (map label navigation, draw/edit vertex navigation, and the map features listbox).
6
+ *
7
+ * Candidates are first filtered to a ~90° cone centred on the pressed direction (whichever
8
+ * axis dominates the offset decides up/down vs left/right), then the nearest of those by
9
+ * Euclidean distance wins. If nothing falls in that cone, `start`'s own index in `pixels` is
10
+ * returned (a no-op move) rather than jumping somewhere the user didn't ask for.
11
+ *
12
+ * Previously duplicated (with subtly different tie-breaking/equality logic) between
13
+ * providers/maplibre/src/utils/spatial.js and plugins/draw/src/utils/spatial.js —
14
+ * consolidated here as the one canonical implementation both now import.
15
+ *
16
+ * @param {[number, number]} start - Current pixel [x, y].
17
+ * @param {Array<[number, number]>} pixels - All candidate pixels, including `start` itself.
18
+ * @param {'ArrowUp'|'ArrowDown'|'ArrowLeft'|'ArrowRight'|undefined} direction - undefined matches every candidate (nearest overall, any direction).
19
+ * @returns {number} Index into `pixels` of the chosen candidate.
20
+ */
21
+ export const spatialNavigate = (start, pixels, direction) => {
22
+ const isSamePixel = (a, b) => a[0] === b[0] && a[1] === b[1]
23
+
24
+ const quadrant = pixels.filter((p) => {
25
+ const offsetX = Math.abs(p[0] - start[0])
26
+ const offsetY = Math.abs(p[1] - start[1])
27
+ let isQuadrant = false
28
+ if (direction === 'ArrowUp') {
29
+ isQuadrant = p[1] <= start[1] && offsetY >= offsetX
30
+ } else if (direction === 'ArrowDown') {
31
+ isQuadrant = p[1] > start[1] && offsetY >= offsetX
32
+ } else if (direction === 'ArrowLeft') {
33
+ isQuadrant = p[0] <= start[0] && offsetY < offsetX
34
+ } else if (direction === 'ArrowRight') {
35
+ isQuadrant = p[0] > start[0] && offsetY < offsetX
36
+ } else {
37
+ isQuadrant = true
38
+ }
39
+ return isQuadrant && !isSamePixel(p, start)
40
+ })
41
+
42
+ if (!quadrant.length) {
43
+ quadrant.push(start)
44
+ }
45
+
46
+ const distance = (p) => Math.hypot(start[0] - p[0], start[1] - p[1])
47
+ const closest = quadrant.reduce((best, p) => (distance(p) < distance(best) ? p : best), quadrant[0])
48
+ return pixels.findIndex((p) => isSamePixel(p, closest))
49
+ }
@@ -0,0 +1,42 @@
1
+ import { spatialNavigate } from './spatialNavigate.js'
2
+
3
+ describe('spatialNavigate', () => {
4
+ const start = [0, 0]
5
+ const pixels = [[0, 0], [0, -10], [0, 10], [-10, 0], [10, 0]]
6
+
7
+ test.each([
8
+ ['ArrowUp', 1],
9
+ ['ArrowDown', 2],
10
+ ['ArrowLeft', 3],
11
+ ['ArrowRight', 4]
12
+ ])('finds the nearest pixel for %s', (direction, expectedIndex) => {
13
+ expect(spatialNavigate(start, pixels, direction)).toBe(expectedIndex)
14
+ })
15
+
16
+ test('considers all pixels for an unrecognised direction', () => {
17
+ expect(spatialNavigate(start, pixels, 'Tab')).toBe(1)
18
+ })
19
+
20
+ test('considers all pixels when no direction is given (nearest overall)', () => {
21
+ expect(spatialNavigate(start, pixels, undefined)).toBe(1)
22
+ })
23
+
24
+ test('falls back to the start point when no pixel is in the quadrant', () => {
25
+ expect(spatialNavigate(start, [[0, 0]], 'ArrowUp')).toBe(0)
26
+ })
27
+
28
+ test('picks the closer of two candidates in the same direction', () => {
29
+ const pixels = [[0, 0], [10, 0], [2, 0]]
30
+ expect(spatialNavigate(start, pixels, 'ArrowRight')).toBe(2)
31
+ })
32
+
33
+ test('keeps the first-seen closer candidate rather than a later farther one', () => {
34
+ const pixels = [[0, 0], [2, 0], [10, 0]]
35
+ expect(spatialNavigate(start, pixels, 'ArrowRight')).toBe(1)
36
+ })
37
+
38
+ test('a diagonal candidate with dx > dy counts as ArrowRight, not ArrowUp/Down', () => {
39
+ const pixels = [[0, 0], [3, 1], [1, 0]]
40
+ expect(spatialNavigate(start, pixels, 'ArrowRight')).toBe(2)
41
+ })
42
+ })
package/webpack.umd.mjs CHANGED
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url'
3
3
  import fs from 'fs'
4
4
 
5
5
  import MiniCssExtractPlugin from 'mini-css-extract-plugin'
6
+ import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
6
7
  import RemoveEmptyScriptsPlugin from 'webpack-remove-empty-scripts'
7
8
  import RemoveFilesPlugin from 'remove-files-webpack-plugin'
8
9
 
@@ -124,7 +125,11 @@ const createUMDConfig = (entryName, entryPath, libraryPath, outDir, isCore = fal
124
125
 
125
126
  optimization: {
126
127
  splitChunks: { chunks: () => false },
127
- removeEmptyChunks: true
128
+ removeEmptyChunks: true,
129
+ // '...' keeps webpack's default JS minimizer (Terser); CssMinimizerPlugin
130
+ // is added alongside it since setting `minimizer` explicitly replaces the
131
+ // defaults otherwise.
132
+ minimizer: ['...', new CssMinimizerPlugin()]
128
133
  }
129
134
  }
130
135
  }
@@ -1,187 +0,0 @@
1
- import { useEffect, useRef } from 'react'
2
- import { EVENTS } from '../../../../src/config/events.js'
3
- import { isStandaloneLabel } from '../../../../src/utils/symbolUtils.js'
4
- import { buildLayerConfigMap } from '../utils/featureQueries.js'
5
-
6
- const getFeatureId = (feature, config) =>
7
- config ? (feature.properties?.[config.idProperty] ?? feature.id) : null
8
-
9
- const isInViewport = (el) => {
10
- const container = el.closest('.im-c-viewport__markers')
11
- if (!container) {
12
- return false
13
- }
14
- const containerRect = container.getBoundingClientRect()
15
- const { left, right, top, bottom } = el.getBoundingClientRect()
16
- return right > containerRect.left && left < containerRect.right &&
17
- bottom > containerRect.top && top < containerRect.bottom
18
- }
19
-
20
- const collectVisibleMarkers = (markers) => {
21
- const items = []
22
- for (const marker of markers.items) {
23
- if (!marker.label) { continue }
24
- const el = markers.markerRefs?.get(marker.id)
25
- if (!isStandaloneLabel(marker) && el && isInViewport(el)) {
26
- items.push({ id: marker.id, label: marker.label })
27
- }
28
- }
29
- return items
30
- }
31
-
32
- const toFeatureItem = (feature, layerConfigMap, seenIds) => {
33
- const config = layerConfigMap[feature.layer?.id]
34
- if (!config?.labelProperty) {
35
- return null
36
- }
37
- const id = getFeatureId(feature, config)
38
- const stringId = id == null ? null : String(id)
39
- if (stringId == null || seenIds.has(stringId)) {
40
- return null
41
- }
42
- seenIds.add(stringId)
43
- const label = feature.properties?.[config.labelProperty] ?? stringId
44
- return { id: stringId, label }
45
- }
46
-
47
- const collectVisibleFeatures = (mapProvider, layers) => {
48
- const items = []
49
- const seenIds = new Set()
50
- const layerIds = layers.map(layer => layer.layerId)
51
- const layerConfigMap = buildLayerConfigMap(layers)
52
- const features = mapProvider.getVisibleFeatures(layerIds)
53
- for (const feature of features) {
54
- const item = toFeatureItem(feature, layerConfigMap, seenIds)
55
- if (item) {
56
- items.push(item)
57
- }
58
- }
59
- return items
60
- }
61
-
62
- const findFeatureById = (features, layerConfigMap, targetId) => {
63
- for (const feature of features) {
64
- const config = layerConfigMap[feature.layer?.id]
65
- const rawId = getFeatureId(feature, config)
66
- if (rawId != null && String(rawId) === String(targetId)) {
67
- return { feature, config, rawId }
68
- }
69
- }
70
- return null
71
- }
72
-
73
- /**
74
- * Rebuilds the keyboard-navigable item list whenever the map moves or data changes.
75
- * Collects visible markers (by DOM visibility) and visible features (by viewport query),
76
- * then emits MAP_SET_FEATURES so the listbox stays in sync with what the user can see.
77
- */
78
- function useItemListSync ({ markers, interactionModes, layers, mapProvider, multiSelect, eventBus }) {
79
- useEffect(() => {
80
- const handleMoveEnd = () => {
81
- const items = []
82
- if (interactionModes?.includes('selectMarker')) {
83
- items.push(...collectVisibleMarkers(markers))
84
- }
85
- if (interactionModes?.includes('selectFeature') && layers.length > 0) {
86
- items.push(...collectVisibleFeatures(mapProvider, layers))
87
- }
88
- eventBus.emit(EVENTS.MAP_SET_FEATURES, { items, multiselectable: multiSelect })
89
- }
90
- handleMoveEnd()
91
- eventBus.on(EVENTS.MAP_MOVE_END, handleMoveEnd)
92
- eventBus.on(EVENTS.MAP_DATA_CHANGE, handleMoveEnd)
93
- return () => {
94
- eventBus.off(EVENTS.MAP_MOVE_END, handleMoveEnd)
95
- eventBus.off(EVENTS.MAP_DATA_CHANGE, handleMoveEnd)
96
- }
97
- }, [markers, interactionModes, layers, mapProvider, multiSelect, eventBus])
98
- }
99
-
100
- /**
101
- * Listens for MAP_SET_ACTIVE_FEATURE and resolves the active item to its full feature/marker data,
102
- * storing it in both a ref (for synchronous access) and plugin state (for highlight rendering).
103
- * Shows the keyboard cursor ring without firing interact:selectionchange — committing the item
104
- * to the real selection only happens when the user presses Enter/Space.
105
- */
106
- function useActiveItemHandler ({ markers, interactionModes, layers, mapProvider, eventBus, dispatch, listboxActiveItemRef }) {
107
- useEffect(() => {
108
- const handle = ({ id }) => {
109
- if (id === null) {
110
- listboxActiveItemRef.current = null
111
- dispatch({ type: 'SET_LISTBOX_ACTIVE', payload: null })
112
- return
113
- }
114
- const hasMarkerMatch = markers.items.some(m => m.id === id)
115
- if (hasMarkerMatch) {
116
- listboxActiveItemRef.current = { id, isMarker: true }
117
- dispatch({ type: 'SET_LISTBOX_ACTIVE', payload: null })
118
- return
119
- }
120
- if (interactionModes?.includes('selectFeature') && layers.length > 0) {
121
- const layerIds = layers.map(layer => layer.layerId)
122
- const layerConfigMap = buildLayerConfigMap(layers)
123
- const features = mapProvider.getVisibleFeatures(layerIds)
124
- const match = findFeatureById(features, layerConfigMap, id)
125
- if (match) {
126
- const payload = {
127
- featureId: match.rawId,
128
- layerId: match.config.layerId,
129
- idProperty: match.config.idProperty,
130
- geometry: match.feature.geometry
131
- }
132
- listboxActiveItemRef.current = { id, isMarker: false, ...payload, properties: match.feature.properties }
133
- dispatch({ type: 'SET_LISTBOX_ACTIVE', payload })
134
- }
135
- }
136
- }
137
- eventBus.on(EVENTS.MAP_SET_ACTIVE_FEATURE, handle)
138
- return () => { eventBus.off(EVENTS.MAP_SET_ACTIVE_FEATURE, handle) }
139
- }, [markers, interactionModes, layers, mapProvider, eventBus, dispatch, listboxActiveItemRef])
140
- }
141
-
142
- /**
143
- * Handles MAP_SELECT_FEATURE (Enter/Space keypress) by promoting the currently active
144
- * listbox item to a confirmed selection, dispatching TOGGLE_SELECTED_FEATURES or
145
- * TOGGLE_SELECTED_MARKERS and triggering interact:selectionchange downstream.
146
- */
147
- function useSelectItemHandler ({ eventBus, dispatch, listboxActiveItemRef, multiSelect }) {
148
- useEffect(() => {
149
- const handleConfirm = () => {
150
- const item = listboxActiveItemRef.current
151
- if (!item) {
152
- return
153
- }
154
- if (item.isMarker) {
155
- dispatch({ type: 'TOGGLE_SELECTED_MARKERS', payload: { markerId: item.id, multiSelect } })
156
- } else {
157
- const { featureId, layerId, idProperty, geometry, properties } = item
158
- dispatch({
159
- type: 'TOGGLE_SELECTED_FEATURES',
160
- payload: { featureId, layerId, idProperty, geometry, properties, multiSelect, replaceAll: !multiSelect }
161
- })
162
- }
163
- }
164
- eventBus.on(EVENTS.MAP_SELECT_FEATURE, handleConfirm)
165
- return () => { eventBus.off(EVENTS.MAP_SELECT_FEATURE, handleConfirm) }
166
- }, [eventBus, dispatch, listboxActiveItemRef, multiSelect])
167
- }
168
-
169
- /**
170
- * Orchestrates the keyboard-accessible listbox for the interact plugin.
171
- *
172
- * Composes three concerns:
173
- * - Item list sync — keeps the listbox populated with currently visible markers and features
174
- * - Active item resolution — translates a listbox cursor position into full feature/marker data
175
- * - Selection confirmation — commits the active item to the selection on Enter/Space
176
- *
177
- * @param {{ mapState: object, pluginState: object, services: object, mapProvider: object }} params
178
- */
179
- export function useMapItemList ({ mapState, pluginState, services, mapProvider }) {
180
- const { markers } = mapState
181
- const { dispatch, interactionModes, layers, multiSelect } = pluginState
182
- const { eventBus } = services
183
- const listboxActiveItemRef = useRef(null)
184
- useItemListSync({ markers, interactionModes, layers, mapProvider, multiSelect, eventBus })
185
- useActiveItemHandler({ markers, interactionModes, layers, mapProvider, eventBus, dispatch, listboxActiveItemRef })
186
- useSelectItemHandler({ eventBus, dispatch, listboxActiveItemRef, multiSelect })
187
- }
@@ -1,35 +0,0 @@
1
- import React, { forwardRef } from 'react'
2
- import { useConfig } from '../../store/configContext.js'
3
-
4
- export const Features = forwardRef(({ activeFeatureId, selectedIds = [], multiselectable = false, items = [], onFocus, onBlur }, ref) => {
5
- const { id } = useConfig()
6
- const hasItems = items.length > 0
7
- return (
8
- <ul // NOSONAR: role='listbox' is correct for custom composite widget; native <select> cannot host SVG marker elements
9
- id={`${id}-features`}
10
- ref={ref}
11
- role='listbox' // NOSONAR
12
- tabIndex={hasItems ? '0' : '-1'}
13
- aria-hidden={hasItems ? undefined : true}
14
- aria-label='Map features'
15
- aria-describedby={`${id}-keyboard-desc`}
16
- aria-multiselectable={multiselectable || undefined}
17
- aria-activedescendant={activeFeatureId ? `${id}-feature-${activeFeatureId}` : undefined}
18
- className='im-c-features'
19
- onFocus={onFocus}
20
- onBlur={onBlur}
21
- >
22
- {items.map(item => (
23
- <li // NOSONAR: role='option' overrides implicit listitem; this is the correct ARIA listbox child pattern
24
- key={item.id} id={`${id}-feature-${item.id}`} role='option' // NOSONAR
25
- data-id={item.id}
26
- aria-selected={selectedIds.includes(item.id)}
27
- >
28
- {item.label}
29
- </li>
30
- ))}
31
- </ul>
32
- )
33
- })
34
-
35
- Features.displayName = 'Features'
@@ -1,124 +0,0 @@
1
- import React from 'react'
2
- import { render, fireEvent } from '@testing-library/react'
3
- import { Features } from './Features.jsx'
4
- import { useConfig } from '../../store/configContext.js'
5
-
6
- jest.mock('../../store/configContext.js', () => ({ useConfig: jest.fn() }))
7
-
8
- const APP_ID = 'test-app'
9
- const LISTBOX = '[role="listbox"]' // NOSONAR
10
- const OPTION = '[role="option"]' // NOSONAR
11
- const ARIA_SELECTED = 'aria-selected'
12
- const ITEMS = [
13
- { id: 'f1', label: 'Feature One' },
14
- { id: 'f2', label: 'Feature Two' }
15
- ]
16
-
17
- beforeEach(() => {
18
- useConfig.mockReturnValue({ id: APP_ID })
19
- })
20
-
21
- // ─── Features — rendering ─────────────────────────────────────────────────────
22
-
23
- describe('Features — rendering', () => {
24
- it('renders a listbox with the correct id', () => {
25
- const { container } = render(<Features />)
26
- expect(container.querySelector(`#${APP_ID}-features`)).toBeTruthy()
27
- expect(container.querySelector(LISTBOX)).toBeTruthy() // NOSONAR
28
- })
29
-
30
- it('renders no options when items is empty', () => {
31
- const { container } = render(<Features />)
32
- expect(container.querySelectorAll('[role="option"]')).toHaveLength(0) // NOSONAR
33
- })
34
-
35
- it('renders one option per item with correct id, data-id and label', () => {
36
- const { container } = render(<Features items={ITEMS} />)
37
- const options = container.querySelectorAll(OPTION)
38
- expect(options).toHaveLength(2)
39
- expect(options[0].getAttribute('id')).toBe(`${APP_ID}-feature-f1`)
40
- expect(options[0].dataset.id).toBe('f1')
41
- expect(options[0].textContent).toBe('Feature One')
42
- expect(options[1].getAttribute('id')).toBe(`${APP_ID}-feature-f2`)
43
- expect(options[1].dataset.id).toBe('f2')
44
- expect(options[1].textContent).toBe('Feature Two')
45
- })
46
-
47
- it('sets aria-selected on items present in selectedIds', () => {
48
- const { container } = render(<Features items={ITEMS} selectedIds={['f1']} />)
49
- const options = container.querySelectorAll(OPTION)
50
- expect(options[0]).toHaveAttribute(ARIA_SELECTED, 'true')
51
- expect(options[1]).toHaveAttribute(ARIA_SELECTED, 'false')
52
- })
53
-
54
- it('sets aria-selected on multiple items when selectedIds has multiple entries', () => {
55
- const { container } = render(<Features items={ITEMS} selectedIds={['f1', 'f2']} />)
56
- const options = container.querySelectorAll(OPTION)
57
- expect(options[0]).toHaveAttribute(ARIA_SELECTED, 'true')
58
- expect(options[1]).toHaveAttribute(ARIA_SELECTED, 'true')
59
- })
60
-
61
- it('does not set aria-selected from activeFeatureId alone', () => {
62
- const { container } = render(<Features items={ITEMS} activeFeatureId='f1' />)
63
- const options = container.querySelectorAll(OPTION)
64
- expect(options[0]).toHaveAttribute(ARIA_SELECTED, 'false')
65
- expect(options[1]).toHaveAttribute(ARIA_SELECTED, 'false')
66
- })
67
-
68
- it('sets aria-activedescendant to the option element id when activeFeatureId is provided', () => {
69
- const { container } = render(<Features items={ITEMS} activeFeatureId='f2' />)
70
- expect(container.querySelector(LISTBOX).getAttribute('aria-activedescendant')).toBe('test-app-feature-f2') // NOSONAR
71
- })
72
-
73
- it('omits aria-activedescendant when activeFeatureId is absent', () => {
74
- const { container } = render(<Features items={ITEMS} />)
75
- expect(container.querySelector(LISTBOX).getAttribute('aria-activedescendant')).toBeNull() // NOSONAR
76
- })
77
-
78
- it('sets aria-multiselectable when multiselectable is true', () => {
79
- const { container } = render(<Features items={ITEMS} multiselectable />)
80
- expect(container.querySelector(LISTBOX).getAttribute('aria-multiselectable')).toBe('true') // NOSONAR
81
- })
82
-
83
- it('omits aria-multiselectable when multiselectable is false', () => {
84
- const { container } = render(<Features items={ITEMS} />)
85
- expect(container.querySelector(LISTBOX).getAttribute('aria-multiselectable')).toBeNull() // NOSONAR
86
- })
87
-
88
- it('is tabIndex 0 and not aria-hidden when items are present', () => {
89
- const { container } = render(<Features items={ITEMS} />)
90
- const ul = container.querySelector(LISTBOX) // NOSONAR
91
- expect(ul.getAttribute('tabIndex')).toBe('0')
92
- expect(ul.getAttribute('aria-hidden')).toBeNull()
93
- })
94
-
95
- it('is tabIndex -1 and aria-hidden when items is empty', () => {
96
- const { container } = render(<Features />)
97
- const ul = container.querySelector(LISTBOX) // NOSONAR
98
- expect(ul.getAttribute('tabIndex')).toBe('-1')
99
- expect(ul.getAttribute('aria-hidden')).toBe('true')
100
- })
101
-
102
- it('sets aria-describedby to the shared hints container id', () => {
103
- const { container } = render(<Features />)
104
- expect(container.querySelector(LISTBOX).getAttribute('aria-describedby')).toBe(`${APP_ID}-keyboard-desc`) // NOSONAR
105
- })
106
- })
107
-
108
- // ─── Features — interactions ──────────────────────────────────────────────────
109
-
110
- describe('Features — interactions', () => {
111
- it('calls onFocus when the listbox receives focus', () => {
112
- const onFocus = jest.fn()
113
- const { container } = render(<Features onFocus={onFocus} />)
114
- fireEvent.focus(container.querySelector(LISTBOX)) // NOSONAR
115
- expect(onFocus).toHaveBeenCalled()
116
- })
117
-
118
- it('calls onBlur when the listbox loses focus', () => {
119
- const onBlur = jest.fn()
120
- const { container } = render(<Features onBlur={onBlur} />)
121
- fireEvent.blur(container.querySelector(LISTBOX)) // NOSONAR
122
- expect(onBlur).toHaveBeenCalled()
123
- })
124
- })