@defra/interactive-map 0.0.9-alpha → 0.0.11-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/DOCS_README.md +39 -0
- package/README.md +1 -1
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +21 -3
- package/docs/api/panel-definition.md +10 -12
- package/docs/api.md +81 -8
- package/docs/architecture/architecture-diagrams.md +1 -3
- package/docs/architecture/diagrams-viewer.mdx +12 -0
- package/docs/demo.mdx +70 -0
- package/docs/govuk-prototype.md +23 -0
- package/docs/index.md +19 -0
- package/docs/plugins/plugin-context.md +3 -3
- package/docs/plugins/plugin-manifest.md +1 -1
- package/docusaurus.config.cjs +136 -0
- package/mise.toml +2 -0
- package/package.json +27 -5
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/src/manifest.js +3 -3
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/api/newPolygon.js +1 -3
- package/plugins/beta/draw-es/src/events.js +2 -2
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/src/api/newLine.js +2 -2
- package/plugins/beta/draw-ml/src/api/newPolygon.js +2 -2
- package/plugins/beta/draw-ml/src/events.js +18 -10
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/src/manifest.js +3 -3
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +7 -7
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/esm/index.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 +9 -3
- package/plugins/search/src/Search.test.jsx +190 -0
- package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +67 -0
- package/plugins/search/src/components/Form/Form.jsx +35 -7
- package/plugins/search/src/components/Form/Form.module.scss +27 -0
- package/plugins/search/src/components/Form/Form.test.jsx +255 -0
- package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +47 -0
- package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +28 -0
- package/plugins/search/src/components/SubmitButton/SubmitButton.module.scss +8 -0
- package/plugins/search/src/components/SubmitButton/SubmitButton.test.jsx +33 -0
- package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +79 -0
- package/plugins/search/src/datasets.js +15 -11
- package/plugins/search/src/datasets.test.js +61 -0
- package/plugins/search/src/events/fetchSuggestions.js +1 -1
- package/plugins/search/src/events/fetchSuggestions.test.js +212 -0
- package/plugins/search/src/events/formHandlers.test.js +232 -0
- package/plugins/search/src/events/index.test.js +118 -0
- package/plugins/search/src/events/inputHandlers.test.js +104 -0
- package/plugins/search/src/events/suggestionHandlers.test.js +166 -0
- package/plugins/search/src/index.js +1 -1
- package/plugins/search/src/index.test.js +47 -0
- package/plugins/search/src/reducer.js +9 -4
- package/plugins/search/src/reducer.test.js +85 -0
- package/plugins/search/src/search.scss +5 -1
- package/plugins/search/src/utils/parseOsNamesResults.js +20 -3
- package/plugins/search/src/utils/parseOsNamesResults.test.js +158 -0
- package/plugins/search/src/utils/updateMap.test.js +52 -0
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/appEvents.js +8 -2
- package/providers/beta/esri/src/esriProvider.js +6 -14
- package/providers/beta/esri/src/mapEvents.js +7 -1
- package/providers/beta/esri/src/utils/coords.js +33 -1
- package/providers/beta/esri/src/utils/coords.test.js +126 -0
- 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-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/appEvents.js +10 -1
- package/providers/maplibre/src/appEvents.test.js +13 -4
- package/providers/maplibre/src/index.js +5 -13
- package/providers/maplibre/src/index.test.js +34 -15
- package/providers/maplibre/src/mapEvents.js +9 -1
- package/providers/maplibre/src/maplibreProvider.js +14 -15
- package/providers/maplibre/src/maplibreProvider.test.js +14 -1
- package/providers/maplibre/src/utils/spatial.js +11 -0
- package/providers/maplibre/src/utils/spatial.test.js +12 -0
- package/src/App/components/Actions/Actions.module.scss +5 -4
- package/src/App/components/MapButton/MapButton.jsx +4 -16
- package/src/App/components/MapButton/MapButton.module.scss +12 -12
- package/src/App/components/MapButton/MapButton.test.jsx +0 -9
- package/src/App/components/Panel/Panel.jsx +6 -6
- package/src/App/components/Panel/Panel.test.jsx +14 -15
- package/src/App/components/Viewport/MapController.jsx +2 -1
- package/src/App/hooks/useLayoutMeasurements.js +1 -1
- package/src/App/hooks/useLayoutMeasurements.test.js +1 -1
- package/src/App/hooks/useMapProviderOverrides.js +21 -1
- package/src/App/hooks/useMapProviderOverrides.test.js +51 -2
- package/src/App/layout/Layout.jsx +4 -4
- package/src/App/layout/layout.module.scss +1 -0
- package/src/App/registry/panelRegistry.js +1 -10
- package/src/App/registry/panelRegistry.test.js +6 -11
- package/src/App/renderer/HtmlElementHost.jsx +11 -3
- package/src/App/renderer/HtmlElementHost.test.jsx +89 -0
- package/src/App/renderer/mapButtons.js +128 -28
- package/src/App/renderer/mapButtons.test.js +119 -19
- package/src/App/store/MapProvider.jsx +18 -5
- package/src/App/store/MapProvider.test.jsx +56 -1
- package/src/App/store/appActionsMap.js +17 -9
- package/src/App/store/appActionsMap.test.js +33 -7
- package/src/App/store/mapActionsMap.js +4 -7
- package/src/InteractiveMap/InteractiveMap.js +18 -0
- package/src/InteractiveMap/InteractiveMap.test.js +12 -0
- package/src/config/appConfig.js +17 -15
- package/src/config/events.js +41 -4
- package/src/config/getInitialOpenPanels.js +2 -2
- package/src/config/getInitialOpenPanels.test.js +7 -7
- package/src/types.js +13 -11
- package/src/utils/getValueForStyle.js +1 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { fetchSuggestions, sanitiseQuery } from './fetchSuggestions.js'
|
|
5
|
+
|
|
6
|
+
describe('fetchSuggestions', () => {
|
|
7
|
+
const dispatch = jest.fn()
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
dispatch.mockClear()
|
|
11
|
+
global.fetch = jest.fn()
|
|
12
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
jest.restoreAllMocks()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('sanitiseQuery strips invalid chars and trims', () => {
|
|
20
|
+
expect(sanitiseQuery(' he!!llo@ ')).toBe('hello')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('fetches results, applies parsing, and dispatches', async () => {
|
|
24
|
+
global.fetch.mockResolvedValueOnce({
|
|
25
|
+
ok: true,
|
|
26
|
+
json: async () => ({ items: ['a', 'b'] })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const datasets = [
|
|
30
|
+
{
|
|
31
|
+
urlTemplate: '/api?q={query}',
|
|
32
|
+
parseResults: (json) => json.items
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const result = await fetchSuggestions('test', datasets, dispatch)
|
|
37
|
+
|
|
38
|
+
expect(fetch).toHaveBeenCalledWith('/api?q=test', { method: 'GET' })
|
|
39
|
+
expect(result.results).toEqual(['a', 'b'])
|
|
40
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
41
|
+
type: 'UPDATE_SUGGESTIONS',
|
|
42
|
+
payload: ['a', 'b']
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('respects includeRegex and excludeRegex', async () => {
|
|
47
|
+
const datasets = [
|
|
48
|
+
{
|
|
49
|
+
includeRegex: /^ok/,
|
|
50
|
+
excludeRegex: /bad/,
|
|
51
|
+
urlTemplate: '/x?q={query}',
|
|
52
|
+
parseResults: () => ['x']
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const result = await fetchSuggestions('bad', datasets, dispatch)
|
|
57
|
+
|
|
58
|
+
expect(fetch).not.toHaveBeenCalled()
|
|
59
|
+
expect(result.results).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('uses buildRequest when provided', async () => {
|
|
63
|
+
global.fetch.mockResolvedValueOnce({
|
|
64
|
+
ok: true,
|
|
65
|
+
json: async () => ({})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const datasets = [
|
|
69
|
+
{
|
|
70
|
+
buildRequest: (query) => ({
|
|
71
|
+
url: `/custom/${query}`,
|
|
72
|
+
options: { method: 'POST' }
|
|
73
|
+
}),
|
|
74
|
+
parseResults: () => ['y']
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
const result = await fetchSuggestions('abc', datasets, dispatch)
|
|
79
|
+
|
|
80
|
+
expect(fetch).toHaveBeenCalledWith('/custom/abc', { method: 'POST' })
|
|
81
|
+
expect(result.results).toEqual(['y'])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('uses transformRequest when provided', async () => {
|
|
85
|
+
global.fetch.mockResolvedValueOnce({
|
|
86
|
+
ok: true,
|
|
87
|
+
json: async () => ({})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const transformRequest = (req) => ({
|
|
91
|
+
...req,
|
|
92
|
+
options: { method: 'PUT' }
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const datasets = [
|
|
96
|
+
{
|
|
97
|
+
urlTemplate: '/t?q={query}',
|
|
98
|
+
parseResults: () => ['z']
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
await fetchSuggestions('x', datasets, dispatch, transformRequest)
|
|
103
|
+
|
|
104
|
+
expect(fetch).toHaveBeenCalledWith('/t?q=x', { method: 'PUT' })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('handles fetch HTTP error', async () => {
|
|
108
|
+
global.fetch.mockResolvedValueOnce({ ok: false, status: 500 })
|
|
109
|
+
|
|
110
|
+
const datasets = [
|
|
111
|
+
{
|
|
112
|
+
label: 'test-ds',
|
|
113
|
+
urlTemplate: '/fail?q={query}',
|
|
114
|
+
parseResults: () => ['nope']
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
const result = await fetchSuggestions('err', datasets, dispatch)
|
|
119
|
+
|
|
120
|
+
expect(result.results).toEqual([])
|
|
121
|
+
expect(console.error).toHaveBeenCalled()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('uses fallback dataset label on fetch HTTP error', async () => {
|
|
125
|
+
global.fetch.mockResolvedValueOnce({
|
|
126
|
+
ok: false,
|
|
127
|
+
status: 404
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const datasets = [
|
|
131
|
+
{
|
|
132
|
+
// no label on purpose
|
|
133
|
+
urlTemplate: '/missing?q={query}',
|
|
134
|
+
parseResults: () => []
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
await fetchSuggestions('oops', datasets, dispatch)
|
|
139
|
+
|
|
140
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
141
|
+
'Fetch error for dataset: 404'
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('handles network error', async () => {
|
|
146
|
+
global.fetch.mockRejectedValueOnce(new Error('network'))
|
|
147
|
+
|
|
148
|
+
const datasets = [
|
|
149
|
+
{
|
|
150
|
+
urlTemplate: '/net?q={query}',
|
|
151
|
+
parseResults: () => ['nope']
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
const result = await fetchSuggestions('err', datasets, dispatch)
|
|
156
|
+
|
|
157
|
+
expect(result.results).toEqual([])
|
|
158
|
+
expect(console.error).toHaveBeenCalled()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('stops processing when exclusive dataset returns results', async () => {
|
|
162
|
+
global.fetch
|
|
163
|
+
.mockResolvedValueOnce({
|
|
164
|
+
ok: true,
|
|
165
|
+
json: async () => ({})
|
|
166
|
+
})
|
|
167
|
+
.mockResolvedValueOnce({
|
|
168
|
+
ok: true,
|
|
169
|
+
json: async () => ({})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const datasets = [
|
|
173
|
+
{
|
|
174
|
+
exclusive: true,
|
|
175
|
+
urlTemplate: '/first?q={query}',
|
|
176
|
+
parseResults: () => ['first']
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
urlTemplate: '/second?q={query}',
|
|
180
|
+
parseResults: () => ['second']
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
const result = await fetchSuggestions('go', datasets, dispatch)
|
|
185
|
+
|
|
186
|
+
expect(result.results).toEqual(['first'])
|
|
187
|
+
expect(fetch).toHaveBeenCalledTimes(1)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('buildRequest can call default request builder', async () => {
|
|
191
|
+
global.fetch.mockResolvedValueOnce({
|
|
192
|
+
ok: true,
|
|
193
|
+
json: async () => ({})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const datasets = [
|
|
197
|
+
{
|
|
198
|
+
buildRequest: (query, getDefault) => {
|
|
199
|
+
// 👇 THIS is the missing function call
|
|
200
|
+
return getDefault()
|
|
201
|
+
},
|
|
202
|
+
urlTemplate: '/default?q={query}',
|
|
203
|
+
parseResults: () => ['ok']
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
const result = await fetchSuggestions('hi', datasets, dispatch)
|
|
208
|
+
|
|
209
|
+
expect(fetch).toHaveBeenCalledWith('/default?q=hi', { method: 'GET' })
|
|
210
|
+
expect(result.results).toEqual(['ok'])
|
|
211
|
+
})
|
|
212
|
+
})
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { createFormHandlers } from './formHandlers.js'
|
|
5
|
+
import { fetchSuggestions } from './fetchSuggestions.js'
|
|
6
|
+
import { updateMap } from '../utils/updateMap.js'
|
|
7
|
+
import { DEFAULTS } from '../defaults.js'
|
|
8
|
+
|
|
9
|
+
jest.mock('./fetchSuggestions.js')
|
|
10
|
+
jest.mock('../utils/updateMap.js')
|
|
11
|
+
|
|
12
|
+
describe('createFormHandlers', () => {
|
|
13
|
+
let dispatch
|
|
14
|
+
let services
|
|
15
|
+
let viewportRef
|
|
16
|
+
let markers
|
|
17
|
+
let handlers
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
dispatch = jest.fn()
|
|
21
|
+
|
|
22
|
+
services = {
|
|
23
|
+
eventBus: { emit: jest.fn() }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
viewportRef = {
|
|
27
|
+
current: { focus: jest.fn() }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
markers = {
|
|
31
|
+
remove: jest.fn()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
handlers = createFormHandlers({
|
|
35
|
+
dispatch,
|
|
36
|
+
services,
|
|
37
|
+
viewportRef,
|
|
38
|
+
mapProvider: 'map',
|
|
39
|
+
markers,
|
|
40
|
+
datasets: [],
|
|
41
|
+
transformRequest: jest.fn(),
|
|
42
|
+
showMarker: true,
|
|
43
|
+
markerColor: 'red'
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
jest.clearAllMocks()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('handleOpenClick dispatches and emits', () => {
|
|
50
|
+
handlers.handleOpenClick()
|
|
51
|
+
|
|
52
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
53
|
+
type: 'TOGGLE_EXPANDED',
|
|
54
|
+
payload: true
|
|
55
|
+
})
|
|
56
|
+
expect(services.eventBus.emit).toHaveBeenCalledWith('search:open')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('handleCloseClick resets state and focuses button', () => {
|
|
60
|
+
jest.useFakeTimers()
|
|
61
|
+
const buttonRef = { current: { focus: jest.fn() } }
|
|
62
|
+
|
|
63
|
+
handlers.handleCloseClick(null, buttonRef)
|
|
64
|
+
|
|
65
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_EXPANDED', payload: false })
|
|
66
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_SUGGESTIONS', payload: [] })
|
|
67
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_VALUE', payload: '' })
|
|
68
|
+
expect(markers.remove).toHaveBeenCalledWith('search')
|
|
69
|
+
|
|
70
|
+
jest.runAllTimers()
|
|
71
|
+
expect(buttonRef.current.focus).toHaveBeenCalled()
|
|
72
|
+
|
|
73
|
+
expect(services.eventBus.emit).toHaveBeenCalledWith('search:clear')
|
|
74
|
+
expect(services.eventBus.emit).toHaveBeenCalledWith('search:close')
|
|
75
|
+
|
|
76
|
+
jest.useRealTimers()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('handleSubmit uses selected suggestion when selectedIndex >= 0', async () => {
|
|
80
|
+
const suggestion = {
|
|
81
|
+
text: 'Paris',
|
|
82
|
+
bounds: 'b',
|
|
83
|
+
point: 'p'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await handlers.handleSubmit(
|
|
87
|
+
{ preventDefault: jest.fn() },
|
|
88
|
+
{},
|
|
89
|
+
{
|
|
90
|
+
suggestions: [suggestion],
|
|
91
|
+
selectedIndex: 0,
|
|
92
|
+
value: 'x'
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: -1 })
|
|
97
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'HIDE_SUGGESTIONS' })
|
|
98
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_VALUE', payload: 'Paris' })
|
|
99
|
+
|
|
100
|
+
expect(updateMap).toHaveBeenCalledWith(
|
|
101
|
+
expect.objectContaining({ bounds: 'b', point: 'p' })
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
expect(services.eventBus.emit).toHaveBeenCalledWith(
|
|
105
|
+
'search:match',
|
|
106
|
+
expect.objectContaining({ query: 'Paris' })
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('handleSubmit returns early for short input', async () => {
|
|
111
|
+
await handlers.handleSubmit(
|
|
112
|
+
{ preventDefault: jest.fn() },
|
|
113
|
+
{},
|
|
114
|
+
{
|
|
115
|
+
suggestions: [],
|
|
116
|
+
selectedIndex: -1,
|
|
117
|
+
value: 'a'.repeat(DEFAULTS.minSearchLength - 1)
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
expect(fetchSuggestions).not.toHaveBeenCalled()
|
|
122
|
+
expect(updateMap).not.toHaveBeenCalled()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('handleSubmit fetches suggestions and updates map (keyboard)', async () => {
|
|
126
|
+
fetchSuggestions.mockResolvedValueOnce({
|
|
127
|
+
sanitisedValue: 'rome',
|
|
128
|
+
results: [
|
|
129
|
+
{ text: 'Rome', bounds: 'b', point: 'p' }
|
|
130
|
+
]
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
await handlers.handleSubmit(
|
|
134
|
+
{ preventDefault: jest.fn() },
|
|
135
|
+
{ interfaceType: 'keyboard' },
|
|
136
|
+
{
|
|
137
|
+
suggestions: [],
|
|
138
|
+
selectedIndex: -1,
|
|
139
|
+
value: 'rome'
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
expect(fetchSuggestions).toHaveBeenCalled()
|
|
144
|
+
expect(viewportRef.current.focus).toHaveBeenCalled()
|
|
145
|
+
expect(updateMap).toHaveBeenCalled()
|
|
146
|
+
expect(services.eventBus.emit).toHaveBeenCalledWith(
|
|
147
|
+
'search:match',
|
|
148
|
+
expect.objectContaining({ query: 'rome' })
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('handleSubmit mobile closes search', async () => {
|
|
153
|
+
fetchSuggestions.mockResolvedValueOnce({
|
|
154
|
+
sanitisedValue: 'berlin',
|
|
155
|
+
results: [
|
|
156
|
+
{ text: 'Berlin', bounds: 'b', point: 'p' }
|
|
157
|
+
]
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
await handlers.handleSubmit(
|
|
161
|
+
{ preventDefault: jest.fn() },
|
|
162
|
+
{ breakpoint: 'mobile' },
|
|
163
|
+
{
|
|
164
|
+
suggestions: [],
|
|
165
|
+
selectedIndex: -1,
|
|
166
|
+
value: 'berlin'
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
171
|
+
type: 'TOGGLE_EXPANDED',
|
|
172
|
+
payload: false
|
|
173
|
+
})
|
|
174
|
+
expect(services.eventBus.emit).toHaveBeenCalledWith('search:close')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('handleSubmit does nothing when no suggestions are returned', async () => {
|
|
178
|
+
fetchSuggestions.mockResolvedValueOnce({
|
|
179
|
+
sanitisedValue: 'none',
|
|
180
|
+
results: []
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
await handlers.handleSubmit(
|
|
184
|
+
{ preventDefault: jest.fn() },
|
|
185
|
+
{},
|
|
186
|
+
{
|
|
187
|
+
suggestions: [],
|
|
188
|
+
selectedIndex: -1,
|
|
189
|
+
value: 'none'
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// Fetch happens
|
|
194
|
+
expect(fetchSuggestions).toHaveBeenCalled()
|
|
195
|
+
|
|
196
|
+
// But nothing downstream runs
|
|
197
|
+
expect(updateMap).not.toHaveBeenCalled()
|
|
198
|
+
expect(services.eventBus.emit).not.toHaveBeenCalledWith(
|
|
199
|
+
'search:match',
|
|
200
|
+
expect.anything()
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('does not refetch when value matches lastFetchedValue', async () => {
|
|
205
|
+
fetchSuggestions.mockResolvedValueOnce({
|
|
206
|
+
sanitisedValue: 'same',
|
|
207
|
+
results: [{ text: 'Same', bounds: 'b', point: 'p' }]
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
await handlers.handleSubmit(
|
|
211
|
+
{ preventDefault: jest.fn() },
|
|
212
|
+
{},
|
|
213
|
+
{
|
|
214
|
+
suggestions: [],
|
|
215
|
+
selectedIndex: -1,
|
|
216
|
+
value: 'same'
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
await handlers.handleSubmit(
|
|
221
|
+
{ preventDefault: jest.fn() },
|
|
222
|
+
{},
|
|
223
|
+
{
|
|
224
|
+
suggestions: [{ text: 'Same', bounds: 'b', point: 'p' }],
|
|
225
|
+
selectedIndex: -1,
|
|
226
|
+
value: 'same'
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
expect(fetchSuggestions).toHaveBeenCalledTimes(1)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { attachEvents } from './index.js'
|
|
5
|
+
import { fetchSuggestions } from './fetchSuggestions.js'
|
|
6
|
+
import { createFormHandlers } from './formHandlers.js'
|
|
7
|
+
import { createInputHandlers } from './inputHandlers.js'
|
|
8
|
+
import { createSuggestionHandlers } from './suggestionHandlers.js'
|
|
9
|
+
import { debounce } from '../../../../src/utils/debounce.js'
|
|
10
|
+
|
|
11
|
+
jest.mock('./fetchSuggestions.js')
|
|
12
|
+
jest.mock('./formHandlers.js')
|
|
13
|
+
jest.mock('./inputHandlers.js')
|
|
14
|
+
jest.mock('./suggestionHandlers.js')
|
|
15
|
+
jest.mock('../../../../src/utils/debounce.js')
|
|
16
|
+
|
|
17
|
+
describe('attachEvents', () => {
|
|
18
|
+
let dispatch
|
|
19
|
+
let services
|
|
20
|
+
let searchContainerRef
|
|
21
|
+
let args
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
dispatch = jest.fn()
|
|
25
|
+
|
|
26
|
+
services = {
|
|
27
|
+
eventBus: { emit: jest.fn() }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
searchContainerRef = {
|
|
31
|
+
current: {
|
|
32
|
+
contains: jest.fn()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Mock debounce to return the function immediately
|
|
37
|
+
debounce.mockImplementation(fn => fn)
|
|
38
|
+
|
|
39
|
+
createFormHandlers.mockReturnValue({ formHandler: jest.fn() })
|
|
40
|
+
createInputHandlers.mockReturnValue({ inputHandler: jest.fn() })
|
|
41
|
+
createSuggestionHandlers.mockReturnValue({ suggestionHandler: jest.fn() })
|
|
42
|
+
|
|
43
|
+
args = {
|
|
44
|
+
dispatch,
|
|
45
|
+
services,
|
|
46
|
+
searchContainerRef,
|
|
47
|
+
datasets: [],
|
|
48
|
+
transformRequest: jest.fn()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
jest.clearAllMocks()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('composes handlers from all handler factories', () => {
|
|
55
|
+
const handlers = attachEvents(args)
|
|
56
|
+
|
|
57
|
+
expect(createFormHandlers).toHaveBeenCalledWith(args)
|
|
58
|
+
expect(createSuggestionHandlers).toHaveBeenCalledWith(args)
|
|
59
|
+
|
|
60
|
+
expect(createInputHandlers).toHaveBeenCalledWith(
|
|
61
|
+
expect.objectContaining({
|
|
62
|
+
debouncedFetchSuggestions: expect.any(Function)
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
expect(handlers.formHandler).toBeDefined()
|
|
67
|
+
expect(handlers.inputHandler).toBeDefined()
|
|
68
|
+
expect(handlers.suggestionHandler).toBeDefined()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('debouncedFetchSuggestions calls fetchSuggestions with correct args', () => {
|
|
72
|
+
const handlers = attachEvents(args)
|
|
73
|
+
|
|
74
|
+
// grab the debounced function passed to input handlers
|
|
75
|
+
const { debouncedFetchSuggestions } =
|
|
76
|
+
createInputHandlers.mock.calls[0][0]
|
|
77
|
+
|
|
78
|
+
debouncedFetchSuggestions('query')
|
|
79
|
+
|
|
80
|
+
expect(fetchSuggestions).toHaveBeenCalledWith(
|
|
81
|
+
'query',
|
|
82
|
+
args.datasets,
|
|
83
|
+
dispatch,
|
|
84
|
+
args.transformRequest
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
expect(debounce).toHaveBeenCalledWith(
|
|
88
|
+
expect.any(Function),
|
|
89
|
+
350
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('handleOutside does nothing when click is inside container', () => {
|
|
94
|
+
searchContainerRef.current.contains.mockReturnValue(true)
|
|
95
|
+
|
|
96
|
+
const handlers = attachEvents(args)
|
|
97
|
+
|
|
98
|
+
handlers.handleOutside({ target: 'inside' })
|
|
99
|
+
|
|
100
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
101
|
+
expect(services.eventBus.emit).not.toHaveBeenCalled()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('handleOutside collapses search when click is outside container', () => {
|
|
105
|
+
searchContainerRef.current.contains.mockReturnValue(false)
|
|
106
|
+
|
|
107
|
+
const handlers = attachEvents(args)
|
|
108
|
+
|
|
109
|
+
handlers.handleOutside({ target: 'outside' })
|
|
110
|
+
|
|
111
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
112
|
+
type: 'TOGGLE_EXPANDED',
|
|
113
|
+
payload: false
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(services.eventBus.emit).toHaveBeenCalledWith('search:close')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { createInputHandlers } from './inputHandlers.js'
|
|
5
|
+
import { DEFAULTS } from '../defaults.js'
|
|
6
|
+
|
|
7
|
+
describe('createInputHandlers', () => {
|
|
8
|
+
let dispatch
|
|
9
|
+
let debouncedFetchSuggestions
|
|
10
|
+
let handlers
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
dispatch = jest.fn()
|
|
14
|
+
|
|
15
|
+
debouncedFetchSuggestions = jest.fn()
|
|
16
|
+
debouncedFetchSuggestions.cancel = jest.fn()
|
|
17
|
+
|
|
18
|
+
handlers = createInputHandlers({
|
|
19
|
+
dispatch,
|
|
20
|
+
debouncedFetchSuggestions
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('handleInputClick shows suggestions', () => {
|
|
25
|
+
handlers.handleInputClick()
|
|
26
|
+
|
|
27
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
28
|
+
type: 'SHOW_SUGGESTIONS'
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('handleInputFocus sets keyboard focus when interface is keyboard', () => {
|
|
33
|
+
handlers.handleInputFocus('keyboard')
|
|
34
|
+
|
|
35
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
36
|
+
type: 'SET_KEYBOARD_FOCUS_WITHIN',
|
|
37
|
+
payload: true
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('handleInputFocus clears keyboard focus when interface is not keyboard', () => {
|
|
42
|
+
handlers.handleInputFocus('mouse')
|
|
43
|
+
|
|
44
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
45
|
+
type: 'SET_KEYBOARD_FOCUS_WITHIN',
|
|
46
|
+
payload: false
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('handleInputBlur dispatches blur event', () => {
|
|
51
|
+
handlers.handleInputBlur('keyboard')
|
|
52
|
+
|
|
53
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
54
|
+
type: 'INPUT_BLUR',
|
|
55
|
+
payload: 'keyboard'
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('handleInputChange below min length cancels debounce and hides suggestions', () => {
|
|
60
|
+
const value = 'a'.repeat(DEFAULTS.minSearchLength - 1)
|
|
61
|
+
|
|
62
|
+
handlers.handleInputChange({
|
|
63
|
+
target: { value }
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
67
|
+
type: 'SET_VALUE',
|
|
68
|
+
payload: value
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
expect(debouncedFetchSuggestions.cancel).toHaveBeenCalled()
|
|
72
|
+
|
|
73
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
74
|
+
type: 'UPDATE_SUGGESTIONS',
|
|
75
|
+
payload: []
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
79
|
+
type: 'HIDE_SUGGESTIONS'
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
expect(debouncedFetchSuggestions).not.toHaveBeenCalled()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('handleInputChange at or above min length shows suggestions and fetches', () => {
|
|
86
|
+
const value = 'a'.repeat(DEFAULTS.minSearchLength)
|
|
87
|
+
|
|
88
|
+
handlers.handleInputChange({
|
|
89
|
+
target: { value }
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
93
|
+
type: 'SET_VALUE',
|
|
94
|
+
payload: value
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
98
|
+
type: 'SHOW_SUGGESTIONS'
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
expect(debouncedFetchSuggestions).toHaveBeenCalledWith(value)
|
|
102
|
+
expect(debouncedFetchSuggestions.cancel).not.toHaveBeenCalled()
|
|
103
|
+
})
|
|
104
|
+
})
|