@defra/interactive-map 0.0.8-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/dist/esm/im-core.js +1 -0
- package/dist/esm/im-shell.js +1 -0
- package/dist/esm/index.js +1 -2
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +104 -3
- package/docs/api.md +22 -2
- package/docs/architecture/architecture-diagrams.md +1 -3
- package/docs/architecture/diagrams-viewer.mdx +12 -0
- package/docs/getting-started.md +78 -8
- 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 +51 -27
- package/plugins/beta/datasets/dist/css/index.css +50 -1
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -0
- package/plugins/beta/datasets/dist/esm/index.js +1 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -0
- package/plugins/beta/draw-es/dist/esm/index.js +1 -2
- 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 -0
- package/plugins/beta/draw-ml/dist/esm/index.js +1 -2
- 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/frame/dist/css/index.css +11 -1
- package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -0
- package/plugins/beta/frame/dist/esm/index.js +1 -2
- package/plugins/beta/map-styles/dist/css/index.css +79 -1
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -0
- package/plugins/beta/map-styles/dist/esm/index.js +1 -2
- package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -0
- package/plugins/beta/scale-bar/dist/esm/index.js +1 -2
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -0
- package/plugins/beta/use-location/dist/esm/index.js +1 -2
- package/plugins/beta/use-location/dist/umd/index.js +1 -1
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -0
- package/plugins/interact/dist/esm/index.js +1 -2
- package/plugins/search/dist/esm/im-search-plugin.js +1 -0
- package/plugins/search/dist/esm/index.js +1 -2
- 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.module.scss +1 -1
- 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/search.scss +1 -1
- 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
- package/providers/beta/esri/dist/css/index.css +30 -1
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -0
- package/providers/beta/esri/dist/esm/index.js +1 -2
- package/providers/beta/open-names/dist/esm/im-reverse-geocode.js +1 -0
- package/providers/beta/open-names/dist/esm/index.js +1 -2
- package/providers/beta/open-names/src/utils/mapToLocationModel.test.js +61 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -0
- package/providers/maplibre/dist/esm/index.js +1 -2
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/src/appEvents.test.js +44 -0
- package/providers/maplibre/src/index.test.js +60 -0
- package/providers/maplibre/src/mapEvents.test.js +115 -0
- package/providers/maplibre/src/maplibreProvider.test.js +205 -0
- package/providers/maplibre/src/utils/calculateLinearTextSize.test.js +31 -0
- package/providers/maplibre/src/utils/detectWebgl.test.js +63 -0
- package/providers/maplibre/src/utils/highlightFeatures.test.js +126 -0
- package/providers/maplibre/src/utils/labels.js +1 -3
- package/providers/maplibre/src/utils/labels.test.js +231 -0
- package/providers/maplibre/src/utils/maplibreFixes.test.js +66 -0
- package/providers/maplibre/src/utils/queryFeatures.test.js +60 -0
- package/providers/maplibre/src/utils/spatial.js +5 -4
- package/providers/maplibre/src/utils/spatial.test.js +96 -0
- package/rollup.esm.mjs +288 -0
- package/src/App/store/appActionsMap.js +1 -1
- package/src/InteractiveMap/InteractiveMap.js +3 -2
- package/webpack.dev.mjs +9 -1
- package/webpack.prod.mjs +8 -1
- package/webpack.umd.mjs +1 -2
- package/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/datasets/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/draw-es/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/draw-ml/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/frame/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/map-styles/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/scale-bar/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/use-location/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/interact/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/search/dist/esm/index.js.LICENSE.txt +0 -1
- package/providers/beta/esri/dist/esm/index.js.LICENSE.txt +0 -1
- package/providers/beta/open-names/dist/esm/index.js.LICENSE.txt +0 -1
- package/providers/maplibre/dist/esm/index.js.LICENSE.txt +0 -6
- package/webpack.esm.mjs +0 -154
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/plugins/search/OpenButton.test.jsx
|
|
2
|
+
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
4
|
+
import { OpenButton } from './OpenButton'
|
|
5
|
+
|
|
6
|
+
describe('OpenButton', () => {
|
|
7
|
+
const baseProps = {
|
|
8
|
+
id: 'test',
|
|
9
|
+
isExpanded: false,
|
|
10
|
+
onClick: jest.fn(),
|
|
11
|
+
buttonRef: { current: null },
|
|
12
|
+
searchIcon: null,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.clearAllMocks()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('renders the button with correct ARIA attributes and calls onClick', () => {
|
|
20
|
+
render(<OpenButton {...baseProps} />)
|
|
21
|
+
const button = screen.getByRole('button', { name: /open search/i })
|
|
22
|
+
expect(button).toHaveAttribute('aria-controls', 'test-search-form')
|
|
23
|
+
fireEvent.click(button)
|
|
24
|
+
expect(baseProps.onClick).toHaveBeenCalledTimes(1)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('applies display:none when isExpanded is true', () => {
|
|
28
|
+
render(<OpenButton {...baseProps} isExpanded />)
|
|
29
|
+
const button = screen.getByLabelText('Open search')
|
|
30
|
+
expect(button).toHaveStyle({ display: 'none' })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('renders the search icon SVG when searchIcon is provided', () => {
|
|
34
|
+
const svgContent = '<path d="M1 1L23 23" />'
|
|
35
|
+
const { container } = render(
|
|
36
|
+
<OpenButton {...baseProps} searchIcon={svgContent} />
|
|
37
|
+
)
|
|
38
|
+
const svg = container.querySelector('svg')
|
|
39
|
+
expect(svg).toBeTruthy()
|
|
40
|
+
expect(svg.innerHTML).toContain('M1 1L23 23')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('does not render an SVG when searchIcon is not provided', () => {
|
|
44
|
+
const { container } = render(<OpenButton {...baseProps} />)
|
|
45
|
+
expect(container.querySelector('svg')).toBeNull()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/plugins/search/Suggestions.test.jsx
|
|
2
|
+
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
4
|
+
import { Suggestions } from './Suggestions'
|
|
5
|
+
|
|
6
|
+
describe('Suggestions', () => {
|
|
7
|
+
const baseProps = {
|
|
8
|
+
id: 'test',
|
|
9
|
+
pluginState: {
|
|
10
|
+
areSuggestionsVisible: true,
|
|
11
|
+
suggestions: [
|
|
12
|
+
{ id: '1', marked: 'First' },
|
|
13
|
+
{ id: '2', marked: 'Second' },
|
|
14
|
+
],
|
|
15
|
+
selectedIndex: 0,
|
|
16
|
+
},
|
|
17
|
+
handleSuggestionClick: jest.fn(),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('renders the listbox with correct attributes', () => {
|
|
25
|
+
render(<Suggestions {...baseProps} />)
|
|
26
|
+
const listbox = screen.getByRole('listbox')
|
|
27
|
+
expect(listbox).toHaveAttribute('id', 'test-search-suggestions')
|
|
28
|
+
expect(listbox).toHaveAttribute('aria-labelledby', 'test-search')
|
|
29
|
+
expect(listbox.className).toContain('im-c-search-suggestions')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('hides the listbox when suggestions are not visible', () => {
|
|
33
|
+
render(
|
|
34
|
+
<Suggestions
|
|
35
|
+
{...baseProps}
|
|
36
|
+
pluginState={{ ...baseProps.pluginState, areSuggestionsVisible: false }}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
const listbox = screen.getByRole('listbox', { hidden: true })
|
|
40
|
+
expect(listbox).toHaveStyle({ display: 'none' })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('hides the listbox when suggestions array is empty', () => {
|
|
44
|
+
render(
|
|
45
|
+
<Suggestions
|
|
46
|
+
{...baseProps}
|
|
47
|
+
pluginState={{ ...baseProps.pluginState, suggestions: [] }}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
const listbox = screen.getByRole('listbox', { hidden: true })
|
|
51
|
+
expect(listbox).toHaveStyle({ display: 'none' })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('renders all suggestion items with correct ARIA attributes', () => {
|
|
55
|
+
render(<Suggestions {...baseProps} />)
|
|
56
|
+
const items = screen.getAllByRole('option')
|
|
57
|
+
expect(items).toHaveLength(2)
|
|
58
|
+
|
|
59
|
+
items.forEach((item, i) => {
|
|
60
|
+
expect(item).toHaveAttribute('id', `test-search-suggestion-${i}`)
|
|
61
|
+
expect(item).toHaveClass('im-c-search-suggestions__item')
|
|
62
|
+
expect(item).toHaveAttribute('aria-setsize', '2')
|
|
63
|
+
expect(item).toHaveAttribute('aria-posinset', `${i + 1}`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// First item should be selected
|
|
67
|
+
expect(items[0]).toHaveAttribute('aria-selected', 'true')
|
|
68
|
+
expect(items[1]).toHaveAttribute('aria-selected', 'false')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('calls handleSuggestionClick when a suggestion is clicked', () => {
|
|
72
|
+
render(<Suggestions {...baseProps} />)
|
|
73
|
+
const items = screen.getAllByRole('option')
|
|
74
|
+
fireEvent.click(items[1])
|
|
75
|
+
expect(baseProps.handleSuggestionClick).toHaveBeenCalledWith(
|
|
76
|
+
baseProps.pluginState.suggestions[1]
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/plugins/search/datasets.test.js
|
|
2
|
+
|
|
3
|
+
import { createDatasets } from './datasets'
|
|
4
|
+
import * as parseModule from './utils/parseOsNamesResults.js'
|
|
5
|
+
|
|
6
|
+
describe('createDatasets', () => {
|
|
7
|
+
const osNamesURL = 'https://example.com/osnames'
|
|
8
|
+
const crs = 'EPSG:4326'
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('returns default dataset with correct properties', () => {
|
|
15
|
+
const datasets = createDatasets({ osNamesURL, crs })
|
|
16
|
+
expect(datasets).toHaveLength(1)
|
|
17
|
+
const ds = datasets[0]
|
|
18
|
+
|
|
19
|
+
expect(ds.name).toBe('osNames')
|
|
20
|
+
expect(ds.urlTemplate).toBe(osNamesURL)
|
|
21
|
+
expect(ds.includeRegex).toEqual(/^[a-zA-Z0-9\s,-]+$/)
|
|
22
|
+
expect(ds.excludeRegex).toEqual(
|
|
23
|
+
/^(?:[a-z]{2}\s*(?:\d{3}\s*\d{3}|\d{4}\s*\d{4}|\d{5}\s*\d{5})|\d+\s*,?\s*\d+)$/i
|
|
24
|
+
)
|
|
25
|
+
expect(typeof ds.parseResults).toBe('function')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('merge custom datasets with default dataset', () => {
|
|
29
|
+
const custom = [{ name: 'custom1', urlTemplate: 'https://custom.com' }]
|
|
30
|
+
const datasets = createDatasets({ osNamesURL, crs, customDatasets: custom })
|
|
31
|
+
expect(datasets).toHaveLength(2)
|
|
32
|
+
expect(datasets[1]).toEqual(custom[0])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('parseResults calls parseOsNamesResults with correct arguments', () => {
|
|
36
|
+
const parseMock = jest.spyOn(parseModule, 'parseOsNamesResults').mockReturnValue('parsed')
|
|
37
|
+
const datasets = createDatasets({ osNamesURL, crs })
|
|
38
|
+
const json = { some: 'data' }
|
|
39
|
+
const query = 'test query'
|
|
40
|
+
|
|
41
|
+
const result = datasets[0].parseResults(json, query)
|
|
42
|
+
expect(parseMock).toHaveBeenCalledWith(json, query, crs)
|
|
43
|
+
expect(result).toBe('parsed')
|
|
44
|
+
parseMock.mockRestore()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -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
|
+
})
|