Skip to content

Commit e53517a

Browse files
authored
Feature/search unit tests (#132)
* Search unit tests added
1 parent 16f8a8a commit e53517a

19 files changed

Lines changed: 1804 additions & 4 deletions

demo/js/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ const interactiveMap = new InteractiveMap('map', {
145145
mapStylesPlugin({
146146
mapStyles: vtsMapStyles3857
147147
}),
148-
// scaleBarPlugin({
149-
// units: 'metric'
150-
// }),
148+
scaleBarPlugin({
149+
units: 'metric'
150+
}),
151151
searchPlugin({
152152
transformRequest: transformGeocodeRequest,
153153
osNamesURL: process.env.OS_NAMES_URL,

package-lock.json

Lines changed: 80 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
"@rollup/plugin-node-resolve": "^16.0.3",
136136
"@rollup/plugin-replace": "^6.0.3",
137137
"@rollup/plugin-terser": "^0.4.4",
138+
"@testing-library/dom": "^10.4.1",
138139
"@testing-library/jest-dom": "^6.6.3",
139140
"@testing-library/react": "^16.3.0",
140141
"@types/geojson": "^7946.0.16",

plugins/search/src/Search.test.jsx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// /plugins/search/Search.test.jsx
2+
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
3+
import { Search } from './Search'
4+
import { attachEvents } from './events/index.js'
5+
import { createDatasets } from './datasets.js'
6+
7+
// Mock sub-components
8+
jest.mock('./components/OpenButton/OpenButton', () => ({
9+
OpenButton: ({ id, isExpanded, onClick }) => (
10+
<button data-testid="open-button" onClick={onClick}>
11+
OpenButton-{id}-{isExpanded ? 'expanded' : 'collapsed'}
12+
</button>
13+
),
14+
}))
15+
16+
jest.mock('./components/CloseButton/CloseButton', () => ({
17+
CloseButton: ({ defaultExpanded, onClick }) => (
18+
<button data-testid="close-button" onClick={onClick}>
19+
CloseButton-{defaultExpanded ? 'defaultExpanded' : 'collapsed'}
20+
</button>
21+
),
22+
}))
23+
24+
jest.mock('./components/Form/Form', () => ({
25+
Form: ({ children }) => <div data-testid="form">{children}</div>,
26+
}))
27+
28+
// Mock external logic
29+
jest.mock('./datasets.js', () => ({
30+
createDatasets: jest.fn(() => ['dataset1', 'dataset2']),
31+
}))
32+
33+
jest.mock('./events/index.js', () => ({
34+
attachEvents: jest.fn(() => ({
35+
handleOpenClick: jest.fn(),
36+
handleCloseClick: jest.fn(),
37+
handleOutside: jest.fn(),
38+
})),
39+
}))
40+
41+
describe('Search component', () => {
42+
let props
43+
let viewportRef
44+
45+
beforeEach(() => {
46+
// Clear mock history before every test to prevent call count accumulation
47+
jest.clearAllMocks()
48+
49+
viewportRef = { current: { style: { pointerEvents: 'auto' } } }
50+
51+
props = {
52+
appConfig: { id: 'search' },
53+
iconRegistry: { close: '<svg>close</svg>', search: '<svg>search</svg>' },
54+
pluginState: {
55+
dispatch: jest.fn(),
56+
isExpanded: false,
57+
areSuggestionsVisible: false,
58+
suggestions: [],
59+
},
60+
pluginConfig: {
61+
isExpanded: false, // This is destructured as defaultExpanded in the component
62+
customDatasets: [],
63+
osNamesURL: 'url',
64+
},
65+
appState: {
66+
dispatch: jest.fn(),
67+
interfaceType: 'keyboard',
68+
layoutRefs: { viewportRef },
69+
},
70+
mapState: { markers: {} },
71+
services: {},
72+
mapProvider: { crs: 'EPSG:3857' },
73+
}
74+
})
75+
76+
afterEach(() => {
77+
cleanup()
78+
})
79+
80+
it('renders OpenButton when defaultExpanded/isExpanded is false', () => {
81+
render(<Search {...props} />)
82+
expect(screen.getByTestId('open-button')).toBeInTheDocument()
83+
expect(screen.getByTestId('form')).toBeInTheDocument()
84+
expect(screen.getByTestId('close-button')).toBeInTheDocument()
85+
})
86+
87+
it('does not render OpenButton when defaultExpanded/isExpanded is true', () => {
88+
props.pluginConfig.isExpanded = true
89+
render(<Search {...props} />)
90+
expect(screen.queryByTestId('open-button')).not.toBeInTheDocument()
91+
expect(screen.getByTestId('close-button')).toBeInTheDocument()
92+
})
93+
94+
it('calls attachEvents once and persists it across re-renders (useRef coverage)', () => {
95+
const { rerender } = render(<Search {...props} />)
96+
97+
expect(createDatasets).toHaveBeenCalledWith({
98+
customDatasets: [],
99+
osNamesURL: 'url',
100+
crs: 'EPSG:3857',
101+
})
102+
103+
// Trigger a re-render with a prop change
104+
rerender(<Search {...props} appState={{ ...props.appState, interfaceType: 'touch' }} />)
105+
106+
// attachEvents should still only have been called once due to the useRef check
107+
expect(attachEvents).toHaveBeenCalledTimes(1)
108+
})
109+
110+
it('OpenButton click triggers handleOpenClick', () => {
111+
render(<Search {...props} />)
112+
const events = attachEvents.mock.results[0].value
113+
fireEvent.click(screen.getByTestId('open-button'))
114+
expect(events.handleOpenClick).toHaveBeenCalledTimes(1)
115+
})
116+
117+
it('CloseButton click triggers handleCloseClick', () => {
118+
render(<Search {...props} />)
119+
const events = attachEvents.mock.results[0].value
120+
fireEvent.click(screen.getByTestId('close-button'))
121+
expect(events.handleCloseClick).toHaveBeenCalledTimes(1)
122+
})
123+
124+
it('focuses input when pluginState.isExpanded is true', () => {
125+
// We have to mock the implementation because inputRef is internal
126+
// This is a bit of a workaround for testing internal refs
127+
props.pluginState.isExpanded = true
128+
render(<Search {...props} />)
129+
expect(screen.getByTestId('form')).toBeInTheDocument()
130+
})
131+
132+
describe('searchOpen logic (Line 46 coverage)', () => {
133+
it('is true when isExpanded is true', () => {
134+
props.pluginState.isExpanded = true
135+
render(<Search {...props} />)
136+
// If searchOpen is true, pointerEvents becomes 'none'
137+
expect(viewportRef.current.style.pointerEvents).toBe('none')
138+
})
139+
140+
it('is true when defaultExpanded is true and suggestions exist', () => {
141+
props.pluginConfig.isExpanded = true // defaultExpanded
142+
props.pluginState.isExpanded = false
143+
props.pluginState.areSuggestionsVisible = true
144+
props.pluginState.suggestions = ['item 1']
145+
146+
render(<Search {...props} />)
147+
expect(viewportRef.current.style.pointerEvents).toBe('none')
148+
})
149+
150+
it('is false when defaultExpanded is true but suggestions are empty', () => {
151+
props.pluginConfig.isExpanded = true
152+
props.pluginState.isExpanded = false
153+
props.pluginState.areSuggestionsVisible = true
154+
props.pluginState.suggestions = []
155+
156+
render(<Search {...props} />)
157+
// Should remain 'auto' (or not 'none')
158+
expect(viewportRef.current.style.pointerEvents).toBe('auto')
159+
})
160+
})
161+
162+
it('cleans up effects and restores pointerEvents on unmount', () => {
163+
props.pluginState.isExpanded = true
164+
const { unmount } = render(<Search {...props} />)
165+
expect(viewportRef.current.style.pointerEvents).toBe('none')
166+
167+
unmount()
168+
expect(viewportRef.current.style.pointerEvents).toBe('auto')
169+
})
170+
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// src/plugins/search/components/CloseButton/CloseButton.test.jsx
2+
3+
import { render, screen, fireEvent } from '@testing-library/react'
4+
import { CloseButton } from './CloseButton'
5+
6+
describe('CloseButton', () => {
7+
it('renders the button and calls onClick', () => {
8+
const onClick = jest.fn()
9+
10+
render(
11+
<CloseButton
12+
defaultExpanded={false}
13+
onClick={onClick}
14+
closeIcon={null}
15+
/>
16+
)
17+
18+
fireEvent.click(
19+
screen.getByRole('button', { name: /close search/i })
20+
)
21+
22+
expect(onClick).toHaveBeenCalledTimes(1)
23+
})
24+
25+
it('applies display:none when defaultExpanded is true', () => {
26+
render(
27+
<CloseButton
28+
defaultExpanded
29+
onClick={jest.fn()}
30+
closeIcon={null}
31+
/>
32+
)
33+
34+
const button = screen.getByLabelText('Close search', {
35+
selector: 'button',
36+
})
37+
38+
expect(button).toHaveStyle({ display: 'none' })
39+
})
40+
41+
it('renders the close icon SVG when closeIcon is provided', () => {
42+
const svgContent = '<path d="M1 1L23 23" />'
43+
const { container } = render(
44+
<CloseButton
45+
defaultExpanded={false}
46+
onClick={jest.fn()}
47+
closeIcon={svgContent}
48+
/>
49+
)
50+
51+
const svg = container.querySelector('svg')
52+
expect(svg).toBeTruthy()
53+
expect(svg.innerHTML).toContain('M1 1L23 23')
54+
})
55+
56+
it('does not render an svg when closeIcon is not provided', () => {
57+
const { container } = render(
58+
<CloseButton
59+
defaultExpanded={false}
60+
onClick={jest.fn()}
61+
closeIcon={null}
62+
/>
63+
)
64+
65+
expect(container.querySelector('svg')).toBeNull()
66+
})
67+
})

0 commit comments

Comments
 (0)