@@ -10,6 +10,7 @@ import type {SearchResults} from '@src/types/onyx';
1010import type * as NativeNavigation from '@react-navigation/native' ;
1111
1212const mockSearch = jest . fn < void , unknown [ ] > ( ) ;
13+ let mockIsSearchRequestInFlight = false ;
1314let mockCurrentSearchResults : SearchResults | undefined ;
1415let mockShouldUseLiveData = false ;
1516
@@ -33,6 +34,7 @@ jest.mock('@libs/actions/Search', () => ({
3334 search : ( ...args : unknown [ ] ) => mockSearch ( ...args ) ,
3435 markPageRequestedSearch : jest . fn ( ) ,
3536 clearPageRequestedSearch : jest . fn ( ) ,
37+ isSearchRequestInFlight : ( ) => mockIsSearchRequestInFlight ,
3638} ) ) ;
3739
3840jest . mock ( '@react-navigation/native' , ( ) => ( {
@@ -88,6 +90,7 @@ describe('useSearchPageSetup', () => {
8890 beforeEach ( ( ) => {
8991 mockSearch . mockClear ( ) ;
9092 mockShouldUseLiveData = false ;
93+ mockIsSearchRequestInFlight = false ;
9194 } ) ;
9295
9396 it ( 'retries an unresolved search when temporary search prevention clears' , async ( ) => {
@@ -118,6 +121,51 @@ describe('useSearchPageSetup', () => {
118121 expect ( mockSearch ) . toHaveBeenCalledWith ( expect . objectContaining ( { isLoading : false } ) ) ;
119122 } ) ;
120123
124+ it ( 'does not resend a live first-page request after the page already sent this query' , async ( ) => {
125+ // Given the page already sent the first page of this query, and its response has loaded
126+ const queryJSON = getQueryJSON ( ) ;
127+ const { rerender} = renderHook (
128+ ( { searchResults} ) => {
129+ mockCurrentSearchResults = searchResults ;
130+ useSearchPageSetup ( queryJSON ) ;
131+ } ,
132+ { initialProps : { searchResults : makeUnresolvedSearchResults ( queryJSON . hash , true ) } } ,
133+ ) ;
134+ await waitFor ( ( ) => expect ( mockSearch ) . toHaveBeenCalledTimes ( 1 ) ) ;
135+ rerender ( { searchResults : makeCachedSearchResults ( queryJSON . hash , false , CONST . SEARCH . SNAPSHOT_STATE . LOADED ) } ) ;
136+
137+ // When Search sends its own first-page request (for example the select-all totals request)
138+ mockIsSearchRequestInFlight = true ;
139+ rerender ( { searchResults : makeCachedSearchResults ( queryJSON . hash , true , CONST . SEARCH . SNAPSHOT_STATE . LOADING ) } ) ;
140+ await Promise . resolve ( ) ;
141+
142+ // Then the page does not send it again, because the resent request would run as a duplicate once the first
143+ // one returns and show the footer's loading skeleton after the totals arrived
144+ expect ( mockSearch ) . toHaveBeenCalledTimes ( 1 ) ;
145+ } ) ;
146+
147+ it ( 'sends a cached query once when Search starts its first-page request' , async ( ) => {
148+ // Given a cached query that the page did not need to request on this visit
149+ const queryJSON = getQueryJSON ( ) ;
150+ const { rerender} = renderHook (
151+ ( { searchResults} ) => {
152+ mockCurrentSearchResults = searchResults ;
153+ useSearchPageSetup ( queryJSON ) ;
154+ } ,
155+ { initialProps : { searchResults : makeCachedSearchResults ( queryJSON . hash , false , CONST . SEARCH . SNAPSHOT_STATE . LOADED ) } } ,
156+ ) ;
157+ await Promise . resolve ( ) ;
158+ expect ( mockSearch ) . not . toHaveBeenCalled ( ) ;
159+
160+ // When Search sends its own first-page request for it
161+ mockIsSearchRequestInFlight = true ;
162+ rerender ( { searchResults : makeCachedSearchResults ( queryJSON . hash , true , CONST . SEARCH . SNAPSHOT_STATE . LOADING ) } ) ;
163+
164+ // Then the page still sends the query once, so the visit is saved to recent searches
165+ await waitFor ( ( ) => expect ( mockSearch ) . toHaveBeenCalledTimes ( 1 ) ) ;
166+ expect ( mockSearch ) . toHaveBeenCalledWith ( expect . objectContaining ( { shouldSaveRecentSearch : true } ) ) ;
167+ } ) ;
168+
121169 it ( 'does not restart the first page while a later page is loading' , async ( ) => {
122170 const queryJSON = getQueryJSON ( ) ;
123171 mockCurrentSearchResults = makeCachedSearchResults ( queryJSON . hash , true , CONST . SEARCH . SNAPSHOT_STATE . LOADING , CONST . SEARCH . RESULTS_PAGE_SIZE ) ;
0 commit comments