@@ -32,15 +32,23 @@ const setup = <T extends Data>(service: Service<T>, options?: InfiniteScrollOpti
3232 renderHook ( ( ) => useInfiniteScroll ( service , options ) ) ;
3333
3434describe ( 'useInfiniteScroll' , ( ) => {
35+ let mockRaf : ReturnType < typeof vi . spyOn > ;
36+
3537 beforeEach ( ( ) => {
3638 count = 0 ;
3739 } ) ;
3840
3941 beforeAll ( ( ) => {
4042 vi . useFakeTimers ( ) ;
43+ // Mock requestAnimationFrame to execute callbacks immediately
44+ mockRaf = vi . spyOn ( window , 'requestAnimationFrame' ) . mockImplementation ( ( cb : FrameRequestCallback ) => {
45+ cb ( 0 ) ;
46+ return 0 ;
47+ } ) as ReturnType < typeof vi . spyOn > ;
4148 } ) ;
4249
4350 afterAll ( ( ) => {
51+ mockRaf . mockRestore ( ) ;
4452 vi . useRealTimers ( ) ;
4553 } ) ;
4654
@@ -175,6 +183,7 @@ describe('useInfiniteScroll', () => {
175183 await act ( async ( ) => {
176184 vi . advanceTimersByTime ( 1000 ) ;
177185 } ) ;
186+
178187 expect ( result . current . loadingMore ) . toBe ( false ) ;
179188 //reverse order
180189 expect ( result . current . data ?. list ) . toMatchObject ( [ 4 , 5 , 6 , 1 , 2 , 3 ] ) ;
@@ -462,4 +471,63 @@ describe('useInfiniteScroll', () => {
462471 expect ( result . current . data ?. list . length ) . toBe ( 2 ) ;
463472 expect ( result . current . data ?. list ) . toEqual ( [ 1 , 2 ] ) ;
464473 } ) ;
474+
475+ test ( 'service should be called only once when scrolling to bottom multiple times quickly' , async ( ) => {
476+ const mockService = vi . fn ( async ( ) => {
477+ await sleep ( 1000 ) ;
478+ return { list : [ 1 , 2 , 3 ] , nextId : 1 } ;
479+ } ) ;
480+
481+ const events : Record < string , any > = { } ;
482+ const mockAddEventListener = vi
483+ . spyOn ( targetEl , 'addEventListener' )
484+ . mockImplementation ( ( eventName : string , callback : any ) => {
485+ events [ eventName ] = callback ;
486+ } ) ;
487+
488+ const scrollHeightSpy = vi . spyOn ( targetEl , 'scrollHeight' , 'get' ) . mockImplementation ( ( ) => 150 ) ;
489+ const clientHeightSpy = vi . spyOn ( targetEl , 'clientHeight' , 'get' ) . mockImplementation ( ( ) => 100 ) ;
490+
491+ const { result } = setup ( mockService , {
492+ target : targetEl ,
493+ isNoMore : ( d ) => d ?. nextId === undefined ,
494+ } ) ;
495+
496+ // Wait for initial load to complete
497+ await act ( async ( ) => {
498+ vi . advanceTimersByTime ( 1000 ) ;
499+ } ) ;
500+ expect ( result . current . loading ) . toBe ( false ) ;
501+ expect ( mockService ) . toHaveBeenCalledTimes ( 1 ) ;
502+
503+ // Set scroll position to bottom (scrollHeight - scrollTop <= clientHeight + threshold)
504+ // 150 - 50 = 100 <= 100 + 100 = 200, so it should trigger loadMore
505+ setTargetInfo ( 'scrollTop' , 50 ) ;
506+
507+ // Trigger scroll event multiple times quickly (before first request completes)
508+ act ( ( ) => {
509+ events [ 'scroll' ] ( ) ;
510+ } ) ;
511+
512+ // Service should be called once more (total 2 times: initial + loadMore)
513+ expect ( mockService ) . toHaveBeenCalledTimes ( 2 ) ;
514+
515+ // Trigger more scroll events while loading
516+ act ( ( ) => {
517+ events [ 'scroll' ] ( ) ;
518+ } ) ;
519+ act ( ( ) => {
520+ events [ 'scroll' ] ( ) ;
521+ } ) ;
522+ act ( ( ) => {
523+ events [ 'scroll' ] ( ) ;
524+ } ) ;
525+
526+ // Service should still only be called twice (no additional calls during loading)
527+ expect ( mockService ) . toHaveBeenCalledTimes ( 2 ) ;
528+
529+ mockAddEventListener . mockRestore ( ) ;
530+ scrollHeightSpy . mockRestore ( ) ;
531+ clientHeightSpy . mockRestore ( ) ;
532+ } ) ;
465533} ) ;
0 commit comments