@@ -91,7 +91,9 @@ describe('createBillingPaginatedHook', () => {
9191 expect ( useFetcherMock ) . toHaveBeenCalledWith ( 'user' ) ;
9292
9393 expect ( fetcherMock ) . not . toHaveBeenCalled ( ) ;
94+ // Ensures that SWR does not update the loading state even if the fetcher is not called.
9495 expect ( result . current . isLoading ) . toBe ( false ) ;
96+ expect ( result . current . isFetching ) . toBe ( false ) ;
9597 } ) ;
9698
9799 it ( 'authenticated hook: does not fetch when user is null' , ( ) => {
@@ -181,4 +183,220 @@ describe('createBillingPaginatedHook', () => {
181183 expect ( fetcherMock ) . not . toHaveBeenCalled ( ) ;
182184 expect ( result . current . isLoading ) . toBe ( false ) ;
183185 } ) ;
186+
187+ describe ( 'authenticated hook - after sign-out previously loaded data are cleared' , ( ) => {
188+ it ( 'pagination mode: data is cleared when user signs out' , async ( ) => {
189+ fetcherMock . mockImplementation ( ( params : any ) =>
190+ Promise . resolve ( {
191+ data : Array . from ( { length : params . pageSize } , ( _ , i ) => ( { id : `p${ params . initialPage } -${ i } ` } ) ) ,
192+ total_count : 5 ,
193+ } ) ,
194+ ) ;
195+
196+ const { result, rerender } = renderHook ( ( ) => useDummyAuth ( { initialPage : 1 , pageSize : 2 } ) , {
197+ wrapper,
198+ } ) ;
199+
200+ await waitFor ( ( ) => expect ( result . current . isFetching ) . toBe ( true ) ) ;
201+ await waitFor ( ( ) => expect ( result . current . isFetching ) . toBe ( false ) ) ;
202+ expect ( result . current . data . length ) . toBe ( 2 ) ;
203+ expect ( result . current . page ) . toBe ( 1 ) ;
204+ expect ( result . current . pageCount ) . toBe ( 3 ) ; // ceil(5/2)
205+
206+ // Simulate sign-out
207+ mockUser = null ;
208+ rerender ( ) ;
209+
210+ // Data should become empty
211+ await waitFor ( ( ) => expect ( result . current . data ) . toEqual ( [ ] ) ) ;
212+ expect ( result . current . count ) . toBe ( 0 ) ;
213+ expect ( result . current . page ) . toBe ( 1 ) ;
214+ expect ( result . current . pageCount ) . toBe ( 0 ) ;
215+ } ) ;
216+
217+ it ( 'pagination mode: with keepPreviousData=true data is cleared after sign-out' , async ( ) => {
218+ fetcherMock . mockImplementation ( ( params : any ) =>
219+ Promise . resolve ( {
220+ data : Array . from ( { length : params . pageSize } , ( _ , i ) => ( { id : `item-${ params . initialPage } -${ i } ` } ) ) ,
221+ total_count : 20 ,
222+ } ) ,
223+ ) ;
224+
225+ const { result, rerender } = renderHook (
226+ ( ) => useDummyAuth ( { initialPage : 1 , pageSize : 5 , keepPreviousData : true } ) ,
227+ {
228+ wrapper,
229+ } ,
230+ ) ;
231+
232+ // Wait for initial data load
233+ await waitFor ( ( ) => expect ( result . current . isLoading ) . toBe ( true ) ) ;
234+ await waitFor ( ( ) => expect ( result . current . isLoading ) . toBe ( false ) ) ;
235+
236+ expect ( result . current . data . length ) . toBe ( 5 ) ;
237+ expect ( result . current . data ) . toEqual ( [
238+ { id : 'item-1-0' } ,
239+ { id : 'item-1-1' } ,
240+ { id : 'item-1-2' } ,
241+ { id : 'item-1-3' } ,
242+ { id : 'item-1-4' } ,
243+ ] ) ;
244+ expect ( result . current . count ) . toBe ( 20 ) ;
245+
246+ // Simulate sign-out by setting mockUser to null
247+ mockUser = null ;
248+ rerender ( ) ;
249+
250+ // Attention: We are forcing fetcher to be executed instead of setting the key to null
251+ // because SWR will continue to display the cached data when the key is null and `keepPreviousData` is true.
252+ // This means that SWR will update the loading state to true even if the fetcher is not called,
253+ // because the key changes from `{..., userId: 'user_1'}` to `{..., userId: undefined}`.
254+ await waitFor ( ( ) => expect ( result . current . isLoading ) . toBe ( true ) ) ;
255+ await waitFor ( ( ) => expect ( result . current . isLoading ) . toBe ( false ) ) ;
256+
257+ // Data should be cleared even with keepPreviousData: true
258+ // The key difference here vs usePagesOrInfinite test: userId in cache key changes
259+ // from 'user_1' to undefined, which changes the cache key (not just makes it null)
260+ await waitFor ( ( ) => expect ( result . current . data ) . toEqual ( [ ] ) ) ;
261+ expect ( result . current . count ) . toBe ( 0 ) ;
262+ expect ( result . current . page ) . toBe ( 1 ) ;
263+ expect ( result . current . pageCount ) . toBe ( 0 ) ;
264+ } ) ;
265+
266+ it ( 'infinite mode: data is cleared when user signs out' , async ( ) => {
267+ fetcherMock . mockImplementation ( ( params : any ) =>
268+ Promise . resolve ( {
269+ data : Array . from ( { length : params . pageSize } , ( _ , i ) => ( { id : `p${ params . initialPage } -${ i } ` } ) ) ,
270+ total_count : 10 ,
271+ } ) ,
272+ ) ;
273+
274+ const { result, rerender } = renderHook ( ( ) => useDummyAuth ( { initialPage : 1 , pageSize : 2 , infinite : true } ) , {
275+ wrapper,
276+ } ) ;
277+
278+ await waitFor ( ( ) => expect ( result . current . isFetching ) . toBe ( true ) ) ;
279+ await waitFor ( ( ) => expect ( result . current . isFetching ) . toBe ( false ) ) ;
280+ expect ( result . current . data . length ) . toBe ( 2 ) ;
281+ expect ( result . current . page ) . toBe ( 1 ) ;
282+ expect ( result . current . pageCount ) . toBe ( 5 ) ; // ceil(10/2)
283+
284+ // Simulate sign-out
285+ mockUser = null ;
286+ rerender ( ) ;
287+
288+ await waitFor ( ( ) => expect ( result . current . data ) . toEqual ( [ ] ) ) ;
289+ expect ( result . current . count ) . toBe ( 0 ) ;
290+ expect ( result . current . page ) . toBe ( 1 ) ;
291+ expect ( result . current . pageCount ) . toBe ( 0 ) ;
292+ } ) ;
293+ } ) ;
294+
295+ describe ( 'unauthenticated hook - data persists after sign-out' , ( ) => {
296+ it ( 'pagination mode: data persists when user signs out' , async ( ) => {
297+ fetcherMock . mockImplementation ( ( params : any ) =>
298+ Promise . resolve ( {
299+ data : Array . from ( { length : params . pageSize } , ( _ , i ) => ( { id : `p${ params . initialPage } -${ i } ` } ) ) ,
300+ total_count : 5 ,
301+ } ) ,
302+ ) ;
303+
304+ const { result, rerender } = renderHook ( ( ) => useDummyUnauth ( { initialPage : 1 , pageSize : 2 } ) , {
305+ wrapper,
306+ } ) ;
307+
308+ await waitFor ( ( ) => expect ( result . current . isFetching ) . toBe ( true ) ) ;
309+ await waitFor ( ( ) => expect ( result . current . isFetching ) . toBe ( false ) ) ;
310+ expect ( result . current . data . length ) . toBe ( 2 ) ;
311+ expect ( result . current . page ) . toBe ( 1 ) ;
312+ expect ( result . current . pageCount ) . toBe ( 3 ) ; // ceil(5/2)
313+
314+ const originalData = [ ...result . current . data ] ;
315+ const originalCount = result . current . count ;
316+
317+ // Simulate sign-out
318+ mockUser = null ;
319+ rerender ( ) ;
320+
321+ // Data should persist for unauthenticated hooks
322+ expect ( result . current . data ) . toEqual ( originalData ) ;
323+ expect ( result . current . count ) . toBe ( originalCount ) ;
324+ expect ( result . current . page ) . toBe ( 1 ) ;
325+ expect ( result . current . pageCount ) . toBe ( 3 ) ;
326+ } ) ;
327+
328+ it ( 'pagination mode: with keepPreviousData=true data persists after sign-out' , async ( ) => {
329+ fetcherMock . mockImplementation ( ( params : any ) =>
330+ Promise . resolve ( {
331+ data : Array . from ( { length : params . pageSize } , ( _ , i ) => ( { id : `item-${ params . initialPage } -${ i } ` } ) ) ,
332+ total_count : 20 ,
333+ } ) ,
334+ ) ;
335+
336+ const { result, rerender } = renderHook (
337+ ( ) => useDummyUnauth ( { initialPage : 1 , pageSize : 5 , keepPreviousData : true } ) ,
338+ {
339+ wrapper,
340+ } ,
341+ ) ;
342+
343+ // Wait for initial data load
344+ await waitFor ( ( ) => expect ( result . current . isLoading ) . toBe ( true ) ) ;
345+ await waitFor ( ( ) => expect ( result . current . isLoading ) . toBe ( false ) ) ;
346+
347+ expect ( result . current . data . length ) . toBe ( 5 ) ;
348+ expect ( result . current . data ) . toEqual ( [
349+ { id : 'item-1-0' } ,
350+ { id : 'item-1-1' } ,
351+ { id : 'item-1-2' } ,
352+ { id : 'item-1-3' } ,
353+ { id : 'item-1-4' } ,
354+ ] ) ;
355+ expect ( result . current . count ) . toBe ( 20 ) ;
356+
357+ const originalData = [ ...result . current . data ] ;
358+
359+ // Simulate sign-out by setting mockUser to null
360+ mockUser = null ;
361+ rerender ( ) ;
362+
363+ // Data should persist for unauthenticated hooks even with keepPreviousData: true
364+ expect ( result . current . data ) . toEqual ( originalData ) ;
365+ expect ( result . current . count ) . toBe ( 20 ) ;
366+ expect ( result . current . page ) . toBe ( 1 ) ;
367+ expect ( result . current . pageCount ) . toBe ( 4 ) ; // ceil(20/5)
368+ } ) ;
369+
370+ it ( 'infinite mode: data persists when user signs out' , async ( ) => {
371+ fetcherMock . mockImplementation ( ( params : any ) =>
372+ Promise . resolve ( {
373+ data : Array . from ( { length : params . pageSize } , ( _ , i ) => ( { id : `p${ params . initialPage } -${ i } ` } ) ) ,
374+ total_count : 10 ,
375+ } ) ,
376+ ) ;
377+
378+ const { result, rerender } = renderHook ( ( ) => useDummyUnauth ( { initialPage : 1 , pageSize : 2 , infinite : true } ) , {
379+ wrapper,
380+ } ) ;
381+
382+ await waitFor ( ( ) => expect ( result . current . isFetching ) . toBe ( true ) ) ;
383+ await waitFor ( ( ) => expect ( result . current . isFetching ) . toBe ( false ) ) ;
384+ expect ( result . current . data . length ) . toBe ( 2 ) ;
385+ expect ( result . current . page ) . toBe ( 1 ) ;
386+ expect ( result . current . pageCount ) . toBe ( 5 ) ; // ceil(10/2)
387+
388+ const originalData = [ ...result . current . data ] ;
389+ const originalCount = result . current . count ;
390+
391+ // Simulate sign-out
392+ mockUser = null ;
393+ rerender ( ) ;
394+
395+ // Data should persist for unauthenticated hooks
396+ expect ( result . current . data ) . toEqual ( originalData ) ;
397+ expect ( result . current . count ) . toBe ( originalCount ) ;
398+ expect ( result . current . page ) . toBe ( 1 ) ;
399+ expect ( result . current . pageCount ) . toBe ( 5 ) ;
400+ } ) ;
401+ } ) ;
184402} ) ;
0 commit comments