@@ -2,10 +2,10 @@ import { chatStore } from '../AskAi/chat.store'
22import { cooldownStore } from '../cooldown.store'
33import { modalStore } from '../modal.store'
44import { Search } from './Search'
5- import { searchStore } from './search.store'
5+ import { searchStore , NO_SELECTION } from './search.store'
66import { SearchResultItem } from './useSearchQuery'
77import { QueryClient , QueryClientProvider } from '@tanstack/react-query'
8- import { render , screen , waitFor } from '@testing-library/react'
8+ import { render , screen , waitFor , act } from '@testing-library/react'
99import userEvent from '@testing-library/user-event'
1010import * as React from 'react'
1111
@@ -42,7 +42,12 @@ const TestWrapper = ({ children }: { children: React.ReactNode }) => {
4242
4343// Helper to reset all stores
4444const resetStores = ( ) => {
45- searchStore . setState ( { searchTerm : '' , page : 1 } )
45+ searchStore . setState ( {
46+ searchTerm : '' ,
47+ page : 1 ,
48+ typeFilter : 'all' ,
49+ selectedIndex : NO_SELECTION ,
50+ } )
4651 chatStore . setState ( {
4752 chatMessages : [ ] ,
4853 conversationId : null ,
@@ -117,6 +122,20 @@ describe('Search Component', () => {
117122 ) as HTMLInputElement
118123 expect ( input . value ) . toBe ( 'kibana' )
119124 } )
125+
126+ it ( 'should reset selectedIndex when search term changes' , async ( ) => {
127+ // Arrange
128+ searchStore . setState ( { searchTerm : 'test' , selectedIndex : 2 } )
129+ const user = userEvent . setup ( )
130+
131+ // Act
132+ render ( < Search /> , { wrapper : TestWrapper } )
133+ const input = screen . getByPlaceholderText ( / s e a r c h i n d o c s / i)
134+ await user . type ( input , 'x' )
135+
136+ // Assert - selectedIndex should reset to 0
137+ expect ( searchStore . getState ( ) . selectedIndex ) . toBe ( 0 )
138+ } )
120139 } )
121140
122141 describe ( 'Ask AI button' , ( ) => {
@@ -158,7 +177,9 @@ describe('Search Component', () => {
158177 await waitFor ( ( ) => {
159178 const messages = chatStore . getState ( ) . chatMessages
160179 expect ( messages . length ) . toBeGreaterThanOrEqual ( 1 )
161- expect ( messages [ 0 ] . content ) . toBe ( 'what is kibana' )
180+ expect ( messages [ 0 ] . content ) . toBe (
181+ 'Tell me more about what is kibana'
182+ )
162183 } )
163184 expect ( modalStore . getState ( ) . mode ) . toBe ( 'askAi' )
164185 } )
@@ -184,7 +205,7 @@ describe('Search Component', () => {
184205 } )
185206
186207 describe ( 'Search on Enter' , ( ) => {
187- it ( 'should trigger chat when Enter is pressed with valid search' , async ( ) => {
208+ it ( 'should trigger chat when Enter is pressed with valid search and no results ' , async ( ) => {
188209 // Arrange
189210 searchStore . setState ( { searchTerm : 'elasticsearch query' } )
190211 const user = userEvent . setup ( )
@@ -198,7 +219,9 @@ describe('Search Component', () => {
198219 // Assert
199220 await waitFor ( ( ) => {
200221 const messages = chatStore . getState ( ) . chatMessages
201- expect ( messages [ 0 ] ?. content ) . toBe ( 'elasticsearch query' )
222+ expect ( messages [ 0 ] ?. content ) . toBe (
223+ 'Tell me more about elasticsearch query'
224+ )
202225 } )
203226 } )
204227
@@ -247,7 +270,7 @@ describe('Search Component', () => {
247270 } )
248271 } )
249272
250- describe ( 'Arrow key navigation' , ( ) => {
273+ describe ( 'Selection navigation' , ( ) => {
251274 beforeEach ( ( ) => {
252275 mockSearchResponse ( [
253276 {
@@ -266,36 +289,60 @@ describe('Search Component', () => {
266289 score : 0.8 ,
267290 parents : [ ] ,
268291 } ,
292+ {
293+ type : 'doc' ,
294+ url : '/test3' ,
295+ title : 'Test Result 3' ,
296+ description : 'Description 3' ,
297+ score : 0.7 ,
298+ parents : [ ] ,
299+ } ,
269300 ] )
270301 } )
271302
272- it ( 'should navigate from input to first result on ArrowDown ' , async ( ) => {
273- // Arrange
274- searchStore . setState ( { searchTerm : 'test' } )
303+ it ( 'should select first item after typing (selectedIndex = 0) ' , async ( ) => {
304+ // Arrange - start with no selection
305+ expect ( searchStore . getState ( ) . selectedIndex ) . toBe ( NO_SELECTION )
275306 const user = userEvent . setup ( )
276307
277308 // Act
278309 render ( < Search /> , { wrapper : TestWrapper } )
310+ const input = screen . getByPlaceholderText ( / s e a r c h i n d o c s / i)
311+ await user . type ( input , 'test' )
279312
280- // Wait for results to load
281313 await waitFor ( ( ) => {
282314 expect ( screen . getByText ( 'Test Result 1' ) ) . toBeInTheDocument ( )
283315 } )
284316
317+ // Assert - selection appears after typing
318+ expect ( searchStore . getState ( ) . selectedIndex ) . toBe ( 0 )
319+ } )
320+
321+ it ( 'should move focus to second result on ArrowDown from input (first is already visually selected)' , async ( ) => {
322+ // Arrange
323+ const user = userEvent . setup ( )
324+
325+ // Act
326+ render ( < Search /> , { wrapper : TestWrapper } )
327+
285328 const input = screen . getByPlaceholderText ( / s e a r c h i n d o c s / i)
286- await user . click ( input )
287- await user . keyboard ( '{ArrowDown}' )
329+ await user . type ( input , 'test' )
288330
289- // Assert
290331 await waitFor ( ( ) => {
291- const firstResult = screen
292- . getByText ( 'Test Result 1' )
293- . closest ( 'a' )
294- expect ( firstResult ) . toHaveFocus ( )
332+ expect ( screen . getByText ( 'Test Result 1' ) ) . toBeInTheDocument ( )
295333 } )
334+
335+ // selectedIndex is now 0 after typing
336+ expect ( searchStore . getState ( ) . selectedIndex ) . toBe ( 0 )
337+
338+ await user . keyboard ( '{ArrowDown}' )
339+
340+ // Assert - focus moved to second result (first is already visually selected)
341+ const secondResult = screen . getByText ( 'Test Result 2' ) . closest ( 'a' )
342+ expect ( secondResult ) . toHaveFocus ( )
296343 } )
297344
298- it ( 'should navigate between results with ArrowDown' , async ( ) => {
345+ it ( 'should move focus between results with ArrowDown/ArrowUp ' , async ( ) => {
299346 // Arrange
300347 searchStore . setState ( { searchTerm : 'test' } )
301348 const user = userEvent . setup ( )
@@ -307,13 +354,104 @@ describe('Search Component', () => {
307354 expect ( screen . getByText ( 'Test Result 1' ) ) . toBeInTheDocument ( )
308355 } )
309356
357+ // Focus first result
310358 const firstResult = screen . getByText ( 'Test Result 1' ) . closest ( 'a' ) !
311- firstResult . focus ( )
312- await user . keyboard ( '{ArrowDown}' )
359+ await act ( async ( ) => {
360+ firstResult . focus ( )
361+ } )
313362
314- // Assert
363+ // Navigate down
364+ await user . keyboard ( '{ArrowDown}' )
315365 const secondResult = screen . getByText ( 'Test Result 2' ) . closest ( 'a' )
316366 expect ( secondResult ) . toHaveFocus ( )
367+ expect ( searchStore . getState ( ) . selectedIndex ) . toBe ( 1 )
368+
369+ // Navigate up
370+ await user . keyboard ( '{ArrowUp}' )
371+ expect ( firstResult ) . toHaveFocus ( )
372+ expect ( searchStore . getState ( ) . selectedIndex ) . toBe ( 0 )
373+ } )
374+
375+ it ( 'should clear selection when ArrowUp from first item goes to input' , async ( ) => {
376+ // Arrange
377+ const user = userEvent . setup ( )
378+
379+ // Act
380+ render ( < Search /> , { wrapper : TestWrapper } )
381+ const input = screen . getByPlaceholderText ( / s e a r c h i n d o c s / i)
382+ await user . type ( input , 'test' )
383+
384+ await waitFor ( ( ) => {
385+ expect ( screen . getByText ( 'Test Result 1' ) ) . toBeInTheDocument ( )
386+ } )
387+
388+ // Focus first result then press ArrowUp
389+ const firstResult = screen . getByText ( 'Test Result 1' ) . closest ( 'a' ) !
390+ await act ( async ( ) => {
391+ firstResult . focus ( )
392+ } )
393+ expect ( searchStore . getState ( ) . selectedIndex ) . toBe ( 0 )
394+
395+ await user . keyboard ( '{ArrowUp}' )
396+
397+ // Assert - focus goes to input, selection is cleared
398+ expect ( input ) . toHaveFocus ( )
399+ expect ( searchStore . getState ( ) . selectedIndex ) . toBe ( NO_SELECTION )
400+ } )
401+
402+ it ( 'should clear selection when ArrowDown from last item goes to button' , async ( ) => {
403+ // Arrange
404+ const user = userEvent . setup ( )
405+
406+ // Act
407+ render ( < Search /> , { wrapper : TestWrapper } )
408+ const input = screen . getByPlaceholderText ( / s e a r c h i n d o c s / i)
409+ await user . type ( input , 'test' )
410+
411+ await waitFor ( ( ) => {
412+ expect ( screen . getByText ( 'Test Result 3' ) ) . toBeInTheDocument ( )
413+ } )
414+
415+ // Focus the last item directly
416+ const lastResult = screen . getByText ( 'Test Result 3' ) . closest ( 'a' ) !
417+ await act ( async ( ) => {
418+ lastResult . focus ( )
419+ } )
420+ expect ( searchStore . getState ( ) . selectedIndex ) . toBe ( 2 )
421+
422+ // Try to go down from last item
423+ await user . keyboard ( '{ArrowDown}' )
424+
425+ // Assert - focus moves to button, selection is cleared
426+ const button = screen . getByRole ( 'button' , {
427+ name : / t e l l m e m o r e a b o u t / i,
428+ } )
429+ expect ( button ) . toHaveFocus ( )
430+ expect ( searchStore . getState ( ) . selectedIndex ) . toBe ( NO_SELECTION )
431+ } )
432+
433+ it ( 'should render isSelected prop on the selected item' , async ( ) => {
434+ // Arrange
435+ searchStore . setState ( { searchTerm : 'test' , selectedIndex : 1 } )
436+
437+ // Act
438+ render ( < Search /> , { wrapper : TestWrapper } )
439+
440+ await waitFor ( ( ) => {
441+ expect ( screen . getByText ( 'Test Result 2' ) ) . toBeInTheDocument ( )
442+ } )
443+
444+ // Assert - the second result should have the data-selected attribute
445+ const secondResultLink = screen
446+ . getByText ( 'Test Result 2' )
447+ . closest ( 'a' )
448+ expect ( secondResultLink ) . toHaveAttribute ( 'data-selected' , 'true' )
449+
450+ // First and third should not be selected
451+ const firstResultLink = screen
452+ . getByText ( 'Test Result 1' )
453+ . closest ( 'a' )
454+ expect ( firstResultLink ) . not . toHaveAttribute ( 'data-selected' )
317455 } )
318456 } )
319457
0 commit comments