@defra/interactive-map 0.0.9-alpha → 0.0.10-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/docs/api.md +1 -1
- package/docs/architecture/architecture-diagrams.md +1 -3
- package/docs/architecture/diagrams-viewer.mdx +12 -0
- package/docs/govuk-prototype.md +23 -0
- package/docs/index.md +23 -0
- package/docusaurus.config.cjs +106 -0
- package/mise.toml +2 -0
- package/package.json +21 -4
- 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/search/src/Search.test.jsx +170 -0
- package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +67 -0
- package/plugins/search/src/components/Form/Form.test.jsx +158 -0
- package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +47 -0
- package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +79 -0
- package/plugins/search/src/datasets.test.js +46 -0
- 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.test.js +47 -0
- package/plugins/search/src/reducer.test.js +80 -0
- package/plugins/search/src/utils/parseOsNamesResults.js +2 -1
- package/plugins/search/src/utils/parseOsNamesResults.test.js +140 -0
- package/plugins/search/src/utils/updateMap.test.js +52 -0
|
@@ -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
|
+
})
|