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+ } )
0 commit comments