@defra/interactive-map 0.0.37-alpha → 0.0.38-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 (244) hide show
  1. package/assets/templates/draw-tools.njk +4 -3
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api.md +32 -0
  8. package/docs/examples/draw-tools.mdx +0 -4
  9. package/docs/plugins/draw.md +482 -0
  10. package/docs/plugins.md +2 -6
  11. package/govuk-prototype-kit.config.json +3 -2
  12. package/package.json +2 -4
  13. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  14. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  15. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  16. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  17. package/plugins/beta/map-styles/dist/css/index.css +1 -97
  18. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  19. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  20. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  21. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  22. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  23. package/plugins/draw/dist/esm/index.js +1 -0
  24. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  25. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  26. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  27. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  28. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  29. package/plugins/draw/dist/umd/index.js +2 -0
  30. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  31. package/plugins/draw/src/DrawInit.jsx +89 -0
  32. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  33. package/plugins/draw/src/adapterEvents.js +127 -0
  34. package/plugins/draw/src/adapterEvents.test.js +20 -0
  35. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  36. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  37. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  38. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  39. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  40. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  41. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  42. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  43. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  44. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  45. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  46. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  47. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  48. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  49. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  86. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  87. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  88. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  89. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  95. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  96. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  97. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  98. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  99. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  100. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  101. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  102. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  103. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  104. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  105. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  106. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  107. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  108. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  109. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  110. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  111. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  112. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  113. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  114. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  115. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  118. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  119. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  120. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  121. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  144. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  145. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  146. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  147. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  148. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  149. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  156. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  157. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  158. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  159. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  163. package/plugins/draw/src/api/addFeature.js +22 -0
  164. package/plugins/draw/src/api/addFeature.test.js +41 -0
  165. package/plugins/draw/src/api/deleteFeature.js +11 -0
  166. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  167. package/plugins/draw/src/api/editFeature.js +54 -0
  168. package/plugins/draw/src/api/editFeature.test.js +134 -0
  169. package/plugins/draw/src/api/merge.js +30 -0
  170. package/plugins/draw/src/api/merge.test.js +57 -0
  171. package/plugins/draw/src/api/newLine.js +41 -0
  172. package/plugins/draw/src/api/newLine.test.js +93 -0
  173. package/plugins/draw/src/api/newPolygon.js +41 -0
  174. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  175. package/plugins/draw/src/api/split.js +117 -0
  176. package/plugins/draw/src/api/split.test.js +204 -0
  177. package/plugins/draw/src/defaults.js +52 -0
  178. package/plugins/draw/src/defaults.test.js +18 -0
  179. package/plugins/draw/src/draw.scss +43 -0
  180. package/plugins/draw/src/events.js +237 -0
  181. package/plugins/draw/src/events.test.js +369 -0
  182. package/plugins/draw/src/index.js +10 -0
  183. package/plugins/draw/src/index.test.js +24 -0
  184. package/plugins/draw/src/manifest.js +170 -0
  185. package/plugins/draw/src/manifest.test.js +147 -0
  186. package/plugins/draw/src/reducer.js +69 -0
  187. package/plugins/draw/src/reducer.test.js +85 -0
  188. package/plugins/draw/src/utils/debounce.js +16 -0
  189. package/plugins/draw/src/utils/debounce.test.js +40 -0
  190. package/plugins/draw/src/utils/eventBus.js +36 -0
  191. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  192. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  193. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  194. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  195. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  196. package/plugins/draw/src/utils/resolveColors.js +39 -0
  197. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  198. package/plugins/draw/src/utils/spatial.js +243 -0
  199. package/plugins/draw/src/utils/spatial.test.js +243 -0
  200. package/plugins/draw/src/utils/touchTarget.js +95 -0
  201. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  202. package/plugins/draw/src/utils/undoStack.js +31 -0
  203. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  204. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  205. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  206. package/plugins/draw/src/validation/liveStroke.js +85 -0
  207. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  208. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  209. package/plugins/draw/src/validation/rules.js +186 -0
  210. package/plugins/draw/src/validation/rules.test.js +120 -0
  211. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  212. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  213. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  214. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  215. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  216. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  217. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  218. package/plugins/search/dist/css/index.css +1 -317
  219. package/rollup.esm.mjs +35 -28
  220. package/sonar-project.properties +2 -2
  221. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  222. package/src/App/components/Hints/Hints.module.scss +5 -4
  223. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  224. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  225. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  226. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  227. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  228. package/src/App/hooks/useHintsAPI.js +67 -0
  229. package/src/App/hooks/useHintsAPI.test.js +145 -0
  230. package/src/App/renderer/PluginInits.jsx +4 -0
  231. package/src/App/renderer/PluginInits.test.jsx +7 -1
  232. package/src/InteractiveMap/InteractiveMap.js +20 -0
  233. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  234. package/src/config/events.js +4 -0
  235. package/src/scss/settings/_dimensions.scss +3 -0
  236. package/src/services/announcer.js +38 -3
  237. package/src/services/announcer.test.js +133 -1
  238. package/src/services/hints.js +3 -0
  239. package/src/services/hints.test.js +19 -0
  240. package/src/types.js +12 -0
  241. package/src/utils/detectInterfaceType.js +5 -1
  242. package/src/utils/isMac.js +15 -0
  243. package/src/utils/isMac.test.js +37 -0
  244. package/webpack.umd.mjs +1 -0
@@ -0,0 +1,147 @@
1
+ import { manifest } from './manifest.js'
2
+
3
+ const findButton = (id) => manifest.buttons.find((b) => b.id === id)
4
+ const findMenuItem = (menuId, itemId) => findButton(menuId).menuItems.find((m) => m.id === itemId)
5
+
6
+ describe('manifest structure', () => {
7
+ test('exposes the reducer, init component and api surface', () => {
8
+ expect(manifest.reducer).toHaveProperty('initialState')
9
+ expect(manifest.reducer).toHaveProperty('actions')
10
+ expect(manifest.InitComponent).toBeDefined()
11
+ expect(Object.keys(manifest.api)).toEqual(expect.arrayContaining([
12
+ 'newPolygon', 'newLine', 'editFeature', 'addFeature', 'deleteFeature', 'split', 'merge'
13
+ ]))
14
+ })
15
+ })
16
+
17
+ describe('drawCancel', () => {
18
+ test('is hidden only when there is no active mode', () => {
19
+ expect(findButton('drawCancel').hiddenWhen({ pluginState: { mode: null } })).toBe(true)
20
+ expect(findButton('drawCancel').hiddenWhen({ pluginState: { mode: 'draw_line' } })).toBe(false)
21
+ })
22
+ })
23
+
24
+ describe('drawAddPoint', () => {
25
+ const hidden = (interfaceType, mode) =>
26
+ findButton('drawAddPoint').hiddenWhen({ appState: { interfaceType }, pluginState: { mode } })
27
+
28
+ test('is shown only while drawing on a touch interface', () => {
29
+ expect(hidden('touch', 'draw_polygon')).toBe(false)
30
+ expect(hidden('mouse', 'draw_polygon')).toBe(true)
31
+ expect(hidden('touch', 'edit_vertex')).toBe(true)
32
+ })
33
+
34
+ test('is disabled while placing at the crosshair would be vetoed', () => {
35
+ const btn = findButton('drawAddPoint')
36
+ expect(btn.enableWhen({ pluginState: { canAddPoint: true } })).toBe(true)
37
+ expect(btn.enableWhen({ pluginState: { canAddPoint: false } })).toBe(false)
38
+ })
39
+ })
40
+
41
+ describe('drawDone', () => {
42
+ const btn = () => findButton('drawDone')
43
+
44
+ test('is hidden outside draw/edit modes', () => {
45
+ expect(btn().hiddenWhen({ pluginState: { mode: null } })).toBe(true)
46
+ expect(btn().hiddenWhen({ pluginState: { mode: 'draw_polygon' } })).toBe(false)
47
+ })
48
+
49
+ test('enables in a draw/edit mode only when the geometry is valid', () => {
50
+ expect(btn().enableWhen({ pluginState: { mode: 'draw_polygon', geometryValid: true } })).toBe(true)
51
+ expect(btn().enableWhen({ pluginState: { mode: 'draw_line', geometryValid: true } })).toBe(true)
52
+ expect(btn().enableWhen({ pluginState: { mode: 'edit_vertex', geometryValid: true } })).toBe(true)
53
+ expect(btn().enableWhen({ pluginState: { mode: 'disabled', geometryValid: true } })).toBe(false)
54
+ })
55
+
56
+ test('stays disabled while the geometry is invalid', () => {
57
+ expect(btn().enableWhen({ pluginState: { mode: 'draw_polygon', geometryValid: false } })).toBe(false)
58
+ expect(btn().enableWhen({ pluginState: { mode: 'draw_line', geometryValid: false } })).toBe(false)
59
+ expect(btn().enableWhen({ pluginState: { mode: 'edit_vertex', geometryValid: false } })).toBe(false)
60
+ })
61
+ })
62
+
63
+ describe('drawMenu', () => {
64
+ test('is hidden outside draw/edit modes', () => {
65
+ expect(findButton('drawMenu').hiddenWhen({ pluginState: { mode: null } })).toBe(true)
66
+ expect(findButton('drawMenu').hiddenWhen({ pluginState: { mode: 'edit_vertex' } })).toBe(false)
67
+ })
68
+
69
+ test('labels "Edit actions" in edit mode, "Draw actions" otherwise', () => {
70
+ expect(findButton('drawMenu').label({ pluginState: { mode: 'edit_vertex' } })).toBe('Edit actions')
71
+ expect(findButton('drawMenu').label({ pluginState: { mode: 'draw_polygon' } })).toBe('Draw actions')
72
+ expect(findButton('drawMenu').label({ pluginState: { mode: 'draw_line' } })).toBe('Draw actions')
73
+ })
74
+
75
+ describe('drawUndo', () => {
76
+ const item = () => findMenuItem('drawMenu', 'drawUndo')
77
+
78
+ test('is hidden outside draw/edit modes', () => {
79
+ expect(item().hiddenWhen({ pluginState: { mode: null } })).toBe(true)
80
+ expect(item().hiddenWhen({ pluginState: { mode: 'draw_line' } })).toBe(false)
81
+ })
82
+
83
+ test('enables from vertex count while drawing and from the undo stack while editing', () => {
84
+ expect(item().enableWhen({ pluginState: { mode: 'draw_polygon', numVertices: 1 } })).toBe(true)
85
+ expect(item().enableWhen({ pluginState: { mode: 'draw_polygon', numVertices: 0 } })).toBe(false)
86
+ expect(item().enableWhen({ pluginState: { mode: 'edit_vertex', undoStackLength: 2 } })).toBe(true)
87
+ expect(item().enableWhen({ pluginState: { mode: 'edit_vertex', undoStackLength: 0 } })).toBe(false)
88
+ })
89
+ })
90
+
91
+ describe('drawSnap', () => {
92
+ const item = () => findMenuItem('drawMenu', 'drawSnap')
93
+
94
+ test('is hidden without a mode or snap layers', () => {
95
+ expect(item().hiddenWhen({ pluginState: { mode: null, hasSnapLayers: true } })).toBe(true)
96
+ expect(item().hiddenWhen({ pluginState: { mode: 'draw_line', hasSnapLayers: false } })).toBe(true)
97
+ expect(item().hiddenWhen({ pluginState: { mode: 'draw_line', hasSnapLayers: true } })).toBe(false)
98
+ })
99
+
100
+ test('is pressed when snapping is enabled', () => {
101
+ expect(item().pressedWhen({ pluginState: { snap: true } })).toBe(true)
102
+ expect(item().pressedWhen({ pluginState: { snap: false } })).toBe(false)
103
+ })
104
+ })
105
+
106
+ describe('drawDeletePoint', () => {
107
+ const item = () => findMenuItem('drawMenu', 'drawDeletePoint')
108
+
109
+ test('is hidden outside edit mode', () => {
110
+ expect(item().hiddenWhen({ pluginState: { mode: 'draw_polygon' } })).toBe(true)
111
+ expect(item().hiddenWhen({ pluginState: { mode: 'edit_vertex' } })).toBe(false)
112
+ })
113
+
114
+ test('enables only with a selection and enough vertices (polygon vs line)', () => {
115
+ expect(item().enableWhen({ pluginState: { selectedVertexIndex: -1 } })).toBe(false)
116
+ expect(item().enableWhen({ pluginState: { selectedVertexIndex: 0, feature: { geometry: { type: 'Polygon' } }, numVertices: 4 } })).toBe(true)
117
+ expect(item().enableWhen({ pluginState: { selectedVertexIndex: 0, feature: { geometry: { type: 'Polygon' } }, numVertices: 3 } })).toBe(false)
118
+ expect(item().enableWhen({ pluginState: { selectedVertexIndex: 0, feature: { geometry: { type: 'LineString' } }, numVertices: 3 } })).toBe(true)
119
+ expect(item().enableWhen({ pluginState: { selectedVertexIndex: 0, feature: { geometry: { type: 'LineString' } }, numVertices: 2 } })).toBe(false)
120
+ })
121
+ })
122
+ })
123
+
124
+ describe('drawUndo keyboard shortcut (platform-specific command)', () => {
125
+ const loadUndoCommand = (mac) => {
126
+ let command
127
+ jest.isolateModules(() => {
128
+ jest.doMock('../../../src/utils/isMac.js', () => ({ isMac: () => mac }))
129
+ const { manifest: reloaded } = require('./manifest.js')
130
+ command = reloaded.keyboardShortcuts.find((s) => s.id === 'drawUndo').command
131
+ })
132
+ return command
133
+ }
134
+
135
+ afterEach(() => {
136
+ jest.dontMock('../../../src/utils/isMac.js')
137
+ jest.resetModules()
138
+ })
139
+
140
+ test('uses Command on macOS', () => {
141
+ expect(loadUndoCommand(true)).toBe('<kbd>Command</kbd> + <kbd>Z</kbd>')
142
+ })
143
+
144
+ test('uses Ctrl on non-mac platforms', () => {
145
+ expect(loadUndoCommand(false)).toBe('<kbd>Ctrl</kbd> + <kbd>Z</kbd>')
146
+ })
147
+ })
@@ -0,0 +1,69 @@
1
+ const initialState = {
2
+ mode: null,
3
+ action: null,
4
+ actionValid: false,
5
+ feature: null,
6
+ tempFeature: null,
7
+ selectedVertexIndex: -1,
8
+ numVertices: null,
9
+ geometryValid: true,
10
+ canAddPoint: true,
11
+ snap: false,
12
+ hasSnapLayers: false,
13
+ undoStackLength: 0
14
+ }
15
+
16
+ const DRAW_MODES = new Set(['draw_polygon', 'draw_line'])
17
+
18
+ const setMode = (state, payload) => ({
19
+ ...state,
20
+ mode: payload,
21
+ numVertices: DRAW_MODES.has(payload) ? 0 : state.numVertices,
22
+ // A new/empty shape is never valid; validation flips this true once the geometry
23
+ // passes all soft rules (edit mode seeds it explicitly in api/editFeature).
24
+ geometryValid: false,
25
+ // A fresh mode can always place; the live placement gate flips this on veto.
26
+ canAddPoint: true
27
+ })
28
+
29
+ const setAction = (state, payload) => ({
30
+ ...state,
31
+ action: payload.name,
32
+ actionValid: payload.isValid
33
+ })
34
+
35
+ const setSelectedVertexIndex = (state, payload) => ({
36
+ ...state,
37
+ selectedVertexIndex: payload.index,
38
+ numVertices: payload.numVertices
39
+ })
40
+
41
+ const setFeature = (state, payload) => ({
42
+ ...state,
43
+ feature: payload.feature === undefined ? state.feature : payload.feature,
44
+ tempFeature: payload.tempFeature === undefined ? state.tempFeature : payload.tempFeature
45
+ })
46
+
47
+ const toggleSnap = (state) => ({ ...state, snap: !state.snap })
48
+
49
+ const setHasSnapLayers = (state, payload) => ({ ...state, hasSnapLayers: !!payload })
50
+
51
+ const setUndoStackLength = (state, payload) => ({ ...state, undoStackLength: payload })
52
+
53
+ const setGeometryValid = (state, payload) => ({ ...state, geometryValid: !!payload })
54
+
55
+ const setCanAddPoint = (state, payload) => ({ ...state, canAddPoint: !!payload })
56
+
57
+ const actions = {
58
+ SET_MODE: setMode,
59
+ SET_ACTION: setAction,
60
+ SET_FEATURE: setFeature,
61
+ SET_SELECTED_VERTEX_INDEX: setSelectedVertexIndex,
62
+ TOGGLE_SNAP: toggleSnap,
63
+ SET_HAS_SNAP_LAYERS: setHasSnapLayers,
64
+ SET_UNDO_STACK_LENGTH: setUndoStackLength,
65
+ SET_GEOMETRY_VALID: setGeometryValid,
66
+ SET_CAN_ADD_POINT: setCanAddPoint
67
+ }
68
+
69
+ export { initialState, actions }
@@ -0,0 +1,85 @@
1
+ import { initialState, actions } from './reducer.js'
2
+
3
+ describe('initialState', () => {
4
+ test('has sensible defaults', () => {
5
+ expect(initialState).toMatchObject({
6
+ mode: null,
7
+ action: null,
8
+ actionValid: false,
9
+ feature: null,
10
+ tempFeature: null,
11
+ selectedVertexIndex: -1,
12
+ numVertices: null,
13
+ snap: false,
14
+ hasSnapLayers: false,
15
+ undoStackLength: 0,
16
+ canAddPoint: true
17
+ })
18
+ })
19
+ })
20
+
21
+ describe('SET_MODE', () => {
22
+ test('resets numVertices to 0 for draw modes', () => {
23
+ expect(actions.SET_MODE(initialState, 'draw_polygon')).toMatchObject({ mode: 'draw_polygon', numVertices: 0 })
24
+ expect(actions.SET_MODE(initialState, 'draw_line')).toMatchObject({ mode: 'draw_line', numVertices: 0 })
25
+ })
26
+
27
+ test('a fresh mode can always place a point', () => {
28
+ const state = { ...initialState, canAddPoint: false }
29
+ expect(actions.SET_MODE(state, 'draw_polygon')).toMatchObject({ canAddPoint: true })
30
+ })
31
+
32
+ test('preserves numVertices for non-draw modes', () => {
33
+ const state = { ...initialState, numVertices: 5 }
34
+ expect(actions.SET_MODE(state, 'edit_vertex')).toMatchObject({ mode: 'edit_vertex', numVertices: 5 })
35
+ expect(actions.SET_MODE(state, null)).toMatchObject({ mode: null, numVertices: 5 })
36
+ })
37
+ })
38
+
39
+ describe('SET_ACTION', () => {
40
+ test('sets the action name and validity', () => {
41
+ expect(actions.SET_ACTION(initialState, { name: 'split', isValid: true }))
42
+ .toMatchObject({ action: 'split', actionValid: true })
43
+ })
44
+ })
45
+
46
+ describe('SET_FEATURE', () => {
47
+ test('updates provided fields and preserves undefined ones', () => {
48
+ const state = { ...initialState, feature: 'F', tempFeature: 'T' }
49
+ expect(actions.SET_FEATURE(state, { feature: 'F2' })).toMatchObject({ feature: 'F2', tempFeature: 'T' })
50
+ expect(actions.SET_FEATURE(state, { tempFeature: null })).toMatchObject({ feature: 'F', tempFeature: null })
51
+ expect(actions.SET_FEATURE(state, { feature: null, tempFeature: null })).toMatchObject({ feature: null, tempFeature: null })
52
+ })
53
+ })
54
+
55
+ describe('SET_SELECTED_VERTEX_INDEX', () => {
56
+ test('sets the index and numVertices', () => {
57
+ expect(actions.SET_SELECTED_VERTEX_INDEX(initialState, { index: 2, numVertices: 4 }))
58
+ .toMatchObject({ selectedVertexIndex: 2, numVertices: 4 })
59
+ })
60
+ })
61
+
62
+ describe('snap actions', () => {
63
+ test('TOGGLE_SNAP flips the snap flag', () => {
64
+ expect(actions.TOGGLE_SNAP({ ...initialState, snap: false }).snap).toBe(true)
65
+ expect(actions.TOGGLE_SNAP({ ...initialState, snap: true }).snap).toBe(false)
66
+ })
67
+
68
+ test('SET_HAS_SNAP_LAYERS coerces the payload to a boolean', () => {
69
+ expect(actions.SET_HAS_SNAP_LAYERS(initialState, ['x']).hasSnapLayers).toBe(true)
70
+ expect(actions.SET_HAS_SNAP_LAYERS(initialState, null).hasSnapLayers).toBe(false)
71
+ })
72
+ })
73
+
74
+ describe('SET_UNDO_STACK_LENGTH', () => {
75
+ test('sets the undo stack length', () => {
76
+ expect(actions.SET_UNDO_STACK_LENGTH(initialState, 3).undoStackLength).toBe(3)
77
+ })
78
+ })
79
+
80
+ describe('SET_CAN_ADD_POINT', () => {
81
+ test('coerces the payload to a boolean', () => {
82
+ expect(actions.SET_CAN_ADD_POINT(initialState, false).canAddPoint).toBe(false)
83
+ expect(actions.SET_CAN_ADD_POINT(initialState, 1).canAddPoint).toBe(true)
84
+ })
85
+ })
@@ -0,0 +1,16 @@
1
+ // --- Debounce Helper ---
2
+ export const debounce = (fn, delay) => {
3
+ let timeout
4
+
5
+ const debounced = (...args) => {
6
+ clearTimeout(timeout)
7
+ timeout = setTimeout(() => fn(...args), delay)
8
+ }
9
+
10
+ debounced.cancel = () => {
11
+ clearTimeout(timeout)
12
+ timeout = null
13
+ }
14
+
15
+ return debounced
16
+ }
@@ -0,0 +1,40 @@
1
+ import { debounce } from './debounce.js'
2
+
3
+ jest.useFakeTimers()
4
+
5
+ describe('debounce', () => {
6
+ test('invokes the function after the delay with the latest arguments', () => {
7
+ const fn = jest.fn()
8
+ const debounced = debounce(fn, 100)
9
+
10
+ debounced('a')
11
+ expect(fn).not.toHaveBeenCalled()
12
+
13
+ jest.advanceTimersByTime(100)
14
+ expect(fn).toHaveBeenCalledWith('a')
15
+ })
16
+
17
+ test('collapses rapid calls into a single trailing invocation', () => {
18
+ const fn = jest.fn()
19
+ const debounced = debounce(fn, 100)
20
+
21
+ debounced(1)
22
+ debounced(2)
23
+ debounced(3)
24
+ jest.advanceTimersByTime(100)
25
+
26
+ expect(fn).toHaveBeenCalledTimes(1)
27
+ expect(fn).toHaveBeenCalledWith(3)
28
+ })
29
+
30
+ test('cancel prevents a pending invocation', () => {
31
+ const fn = jest.fn()
32
+ const debounced = debounce(fn, 100)
33
+
34
+ debounced('x')
35
+ debounced.cancel()
36
+ jest.advanceTimersByTime(100)
37
+
38
+ expect(fn).not.toHaveBeenCalled()
39
+ })
40
+ })
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Simple pub-sub event bus for plugin-internal communication.
3
+ * Used to decouple components that need to communicate events.
4
+ *
5
+ * @returns {{on: Function, off: Function, emit: Function}}
6
+ */
7
+ export const createEventBus = () => {
8
+ const listeners = new Map()
9
+
10
+ return {
11
+ on (type, handler) {
12
+ if (!listeners.has(type)) {
13
+ listeners.set(type, new Set())
14
+ }
15
+ listeners.get(type).add(handler)
16
+ },
17
+
18
+ off (type, handler) {
19
+ listeners.get(type)?.delete(handler)
20
+ },
21
+
22
+ emit (type, ...args) {
23
+ const handlers = listeners.get(type)
24
+ if (handlers) {
25
+ // Array.from, not [...handlers] — under Docusaurus's docs build, Babel
26
+ // compiles with `loose: true` (@docusaurus/babel/preset.js), which turns
27
+ // spread-of-non-array-iterable into `[].concat(handlers)`. concat() only
28
+ // flattens real arrays; for a Set it appends the whole Set as a single
29
+ // element, so every handler call becomes `set.apply(...)` and throws
30
+ // "h.apply is not a function". Array.from() is a plain function call, so
31
+ // it's untouched by that transform and always iterates correctly.
32
+ Array.from(handlers).forEach(h => h(...args))
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,47 @@
1
+ import { createEventBus } from './eventBus.js'
2
+
3
+ describe('createEventBus', () => {
4
+ test('emits to a registered handler with the supplied arguments', () => {
5
+ const bus = createEventBus()
6
+ const handler = jest.fn()
7
+ bus.on('thing', handler)
8
+
9
+ bus.emit('thing', 1, 2)
10
+
11
+ expect(handler).toHaveBeenCalledWith(1, 2)
12
+ })
13
+
14
+ test('supports multiple handlers for the same type', () => {
15
+ const bus = createEventBus()
16
+ const a = jest.fn()
17
+ const b = jest.fn()
18
+ bus.on('thing', a)
19
+ bus.on('thing', b)
20
+
21
+ bus.emit('thing')
22
+
23
+ expect(a).toHaveBeenCalled()
24
+ expect(b).toHaveBeenCalled()
25
+ })
26
+
27
+ test('off removes a handler so it no longer fires', () => {
28
+ const bus = createEventBus()
29
+ const handler = jest.fn()
30
+ bus.on('thing', handler)
31
+ bus.off('thing', handler)
32
+
33
+ bus.emit('thing')
34
+
35
+ expect(handler).not.toHaveBeenCalled()
36
+ })
37
+
38
+ test('emitting a type with no listeners is a no-op', () => {
39
+ const bus = createEventBus()
40
+ expect(() => bus.emit('nothing')).not.toThrow()
41
+ })
42
+
43
+ test('off on an unknown type does not throw', () => {
44
+ const bus = createEventBus()
45
+ expect(() => bus.off('unknown', jest.fn())).not.toThrow()
46
+ })
47
+ })
@@ -0,0 +1,25 @@
1
+ const STYLE_PROPS = new Set(['stroke', 'fill', 'strokeWidth'])
2
+
3
+ export const flattenStyleProperties = (props) => {
4
+ if (!props) {
5
+ return {}
6
+ }
7
+
8
+ const result = {}
9
+
10
+ for (const [key, value] of Object.entries(props)) {
11
+ if (STYLE_PROPS.has(key) && typeof value === 'object' && value !== null) {
12
+ const entries = Object.entries(value)
13
+ if (entries.length > 0) {
14
+ result[key] = entries[0][1]
15
+ }
16
+ for (const [styleId, styleValue] of entries) {
17
+ result[`${key}${styleId.charAt(0).toUpperCase() + styleId.slice(1)}`] = styleValue
18
+ }
19
+ } else {
20
+ result[key] = value
21
+ }
22
+ }
23
+
24
+ return result
25
+ }
@@ -0,0 +1,33 @@
1
+ import { flattenStyleProperties } from './flattenStyleProperties.js'
2
+
3
+ describe('flattenStyleProperties', () => {
4
+ test('returns an empty object for nullish input', () => {
5
+ expect(flattenStyleProperties(null)).toEqual({})
6
+ expect(flattenStyleProperties(undefined)).toEqual({})
7
+ })
8
+
9
+ test('flattens a style-variant object into base + per-style keys', () => {
10
+ const result = flattenStyleProperties({ stroke: { light: 'red', dark: 'blue' } })
11
+ expect(result).toEqual({
12
+ stroke: 'red', // base = first variant value
13
+ strokeLight: 'red',
14
+ strokeDark: 'blue'
15
+ })
16
+ })
17
+
18
+ test('passes through a scalar style value unchanged', () => {
19
+ expect(flattenStyleProperties({ stroke: 'red' })).toEqual({ stroke: 'red' })
20
+ })
21
+
22
+ test('passes through a null style value unchanged', () => {
23
+ expect(flattenStyleProperties({ fill: null })).toEqual({ fill: null })
24
+ })
25
+
26
+ test('passes through non-style properties untouched', () => {
27
+ expect(flattenStyleProperties({ name: 'field', count: 3 })).toEqual({ name: 'field', count: 3 })
28
+ })
29
+
30
+ test('ignores an empty style-variant object (no base, no variants)', () => {
31
+ expect(flattenStyleProperties({ fill: {} })).toEqual({})
32
+ })
33
+ })
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Resolve a configuration value which may be either a simple value (same for all styles)
3
+ * or an object with scheme/style variants.
4
+ *
5
+ * Useful for resolving any value (colors, sizes, etc.) across different map styles and color schemes.
6
+ * Can resolve based on both style ID and color scheme.
7
+ *
8
+ * Resolution order for objects:
9
+ * 1. Exact style ID match (e.g., { outdoor: '...', dark: '...' })
10
+ * 2. Scheme match (e.g., { light: '...', dark: '...' })
11
+ * 3. Fallback to 'light' property
12
+ * 4. First value in object
13
+ *
14
+ * @param {any} value - Simple value or variant object
15
+ * @param {string} scheme - Current color scheme ('light' or 'dark')
16
+ * @param {string|null} styleId - Map style ID for per-style customization
17
+ * @returns {any} Resolved value
18
+ */
19
+ export const getValueForStyle = (value, scheme, styleId = null) => {
20
+ if (typeof value !== 'object' || value === null) {
21
+ return value
22
+ }
23
+ if (styleId && value[styleId] !== undefined) {
24
+ return value[styleId]
25
+ }
26
+ if (value[scheme] !== undefined) {
27
+ return value[scheme]
28
+ }
29
+ if (value.light !== undefined) {
30
+ return value.light
31
+ }
32
+ return Object.values(value)[0]
33
+ }
@@ -0,0 +1,32 @@
1
+ import { getValueForStyle } from './getValueForStyle.js'
2
+
3
+ describe('getValueForStyle', () => {
4
+ test('returns primitive values unchanged', () => {
5
+ expect(getValueForStyle('red', 'light')).toBe('red')
6
+ expect(getValueForStyle(5, 'light')).toBe(5)
7
+ })
8
+
9
+ test('returns null unchanged', () => {
10
+ expect(getValueForStyle(null, 'light')).toBeNull()
11
+ })
12
+
13
+ test('prefers an exact style-id match', () => {
14
+ expect(getValueForStyle({ outdoor: 'green', light: 'red' }, 'light', 'outdoor')).toBe('green')
15
+ })
16
+
17
+ test('falls back to the scheme match when the style id is absent', () => {
18
+ expect(getValueForStyle({ light: 'red', dark: 'blue' }, 'dark', 'outdoor')).toBe('blue')
19
+ })
20
+
21
+ test('uses the scheme match when no style id is provided', () => {
22
+ expect(getValueForStyle({ light: 'red', dark: 'blue' }, 'dark')).toBe('blue')
23
+ })
24
+
25
+ test('falls back to the light property when the scheme is missing', () => {
26
+ expect(getValueForStyle({ light: 'red', dark: 'blue' }, 'sepia')).toBe('red')
27
+ })
28
+
29
+ test('falls back to the first value when neither scheme nor light exist', () => {
30
+ expect(getValueForStyle({ outdoor: 'green', satellite: 'grey' }, 'dark')).toBe('green')
31
+ })
32
+ })
@@ -0,0 +1,39 @@
1
+ import { COLORS, SIZES } from '../defaults.js'
2
+ import { getValueForStyle } from './getValueForStyle.js'
3
+
4
+ /**
5
+ * Resolve all draw colors for the given map style and plugin config overrides.
6
+ * Shared by both adapters (MapLibre and OpenLayers) — they draw from the same
7
+ * COLORS/SIZES palette (defaults.js), so a plugin-level override behaves
8
+ * identically regardless of which map provider is in use.
9
+ *
10
+ * Values in pluginConfig may be plain strings or variant objects (e.g. { light: '...', dark: '...' }).
11
+ * Variant resolution order: exact style ID match → color scheme → 'light' fallback → first value.
12
+ *
13
+ * @param {object|null} mapStyle - Current map style object (has .id and .mapColorScheme)
14
+ * @param {object} pluginConfig - Plugin-level user overrides (may override any COLORS key)
15
+ * @returns {object} Flat color values ready for use in each adapter's styles module
16
+ */
17
+ export const resolveColors = (mapStyle, pluginConfig = {}) => {
18
+ const scheme = mapStyle?.mapColorScheme ?? 'light'
19
+ const styleId = mapStyle?.id ?? null
20
+ const resolveColor = (key) => getValueForStyle(pluginConfig[key] ?? COLORS[key], scheme, styleId)
21
+
22
+ return {
23
+ editStroke: resolveColor('editStroke'),
24
+ editFill: resolveColor('editFill'),
25
+ editVertex: resolveColor('editVertex'),
26
+ editMidpoint: resolveColor('editMidpoint'),
27
+ editActive: resolveColor('editActive'),
28
+ editHalo: resolveColor('editHalo'),
29
+ invalidStroke: resolveColor('invalidStroke'),
30
+ splitValid: resolveColor('splitValid'),
31
+ splitInvalid: resolveColor('splitInvalid'),
32
+ shapeStroke: resolveColor('shapeStroke'),
33
+ strokeWidth: pluginConfig.strokeWidth ?? SIZES.strokeWidth,
34
+ shapeFill: resolveColor('shapeFill'),
35
+ snapVertex: resolveColor('snapVertex'),
36
+ snapEdge: resolveColor('snapEdge'),
37
+ mapStyleId: styleId
38
+ }
39
+ }
@@ -0,0 +1,34 @@
1
+ import { resolveColors } from './resolveColors.js'
2
+ import { COLORS, SIZES } from '../defaults.js'
3
+
4
+ test('without a map style, colours resolve to their light variants and defaults', () => {
5
+ const colors = resolveColors(null)
6
+ expect(colors.editStroke).toBe(COLORS.editStroke.light)
7
+ expect(colors.shapeStroke).toBe(COLORS.shapeStroke) // plain value — no variants
8
+ expect(colors.strokeWidth).toBe(SIZES.strokeWidth)
9
+ expect(colors.mapStyleId).toBeNull()
10
+ })
11
+
12
+ test('splitter colours are resolved', () => {
13
+ const colors = resolveColors(null)
14
+ expect(colors.splitValid).toBe(COLORS.splitValid.light)
15
+ expect(colors.splitInvalid).toBe(COLORS.splitInvalid.light)
16
+ })
17
+
18
+ test('a dark map style resolves dark variants and carries its id through', () => {
19
+ const colors = resolveColors({ id: 'dark', mapColorScheme: 'dark' })
20
+ expect(colors.editStroke).toBe(COLORS.editStroke.dark)
21
+ expect(colors.mapStyleId).toBe('dark')
22
+ })
23
+
24
+ test('plugin config overrides beat the defaults, as strings or scheme variants', () => {
25
+ const colors = resolveColors({ id: 'road', mapColorScheme: 'dark' }, {
26
+ editStroke: '#custom',
27
+ editFill: { light: '#l', dark: '#d' },
28
+ strokeWidth: 9
29
+ })
30
+ expect(colors.editStroke).toBe('#custom')
31
+ expect(colors.editFill).toBe('#d')
32
+ expect(colors.strokeWidth).toBe(9)
33
+ expect(colors.editVertex).toBe(COLORS.editVertex.dark) // untouched keys still resolve
34
+ })