11import { act , render , renderHook , screen } from '@testing-library/react'
22import userEvent from '@testing-library/user-event'
3+ import { setTimeout as wait } from 'node:timers/promises'
34import React , {
45 createElement ,
6+ useEffect ,
57 useState ,
6- type ReactNode ,
7- useEffect
8+ type ReactNode
89} from 'react'
910import { describe , expect , it , vi } from 'vitest'
1011import {
@@ -16,7 +17,7 @@ import {
1617 withNuqsTestingAdapter ,
1718 type OnUrlUpdateFunction
1819} from './adapters/testing'
19- import { debounce } from './lib/queues/rate-limiting'
20+ import { debounce , throttle } from './lib/queues/rate-limiting'
2021import {
2122 parseAsArrayOf ,
2223 parseAsInteger ,
@@ -27,6 +28,8 @@ import {
2728import { useQueryState } from './useQueryState'
2829import { useQueryStates } from './useQueryStates'
2930
31+ const waitForNextTick = ( ) => wait ( 0 )
32+
3033describe ( 'useQueryStates' , ( ) => {
3134 it ( 'allows setting a single value' , async ( ) => {
3235 const onUrlUpdate = vi . fn < OnUrlUpdateFunction > ( )
@@ -767,6 +770,62 @@ describe('useQueryStates: update sequencing', () => {
767770 expect ( onUrlUpdate . mock . calls [ 0 ] ! [ 0 ] . queryString ) . toEqual ( '?b=pass' )
768771 expect ( onUrlUpdate . mock . calls [ 1 ] ! [ 0 ] . queryString ) . toEqual ( '?a=debounced' )
769772 } )
773+
774+ it ( 'does flush when pushing throttled updates' , async ( ) => {
775+ const onUrlUpdate = vi . fn < OnUrlUpdateFunction > ( )
776+ const { result } = renderHook (
777+ ( ) => useQueryStates ( { test : parseAsString } ) ,
778+ {
779+ wrapper : withNuqsTestingAdapter ( {
780+ onUrlUpdate,
781+ autoResetQueueOnUpdate : false
782+ } )
783+ }
784+ )
785+ let p : Promise < URLSearchParams > | undefined = undefined
786+ await act ( async ( ) => {
787+ p = result . current [ 1 ] (
788+ { test : 'pass' } ,
789+ { limitUrlUpdates : throttle ( 100 ) }
790+ )
791+ await waitForNextTick ( )
792+ } )
793+ expect ( onUrlUpdate ) . toHaveBeenCalledOnce ( )
794+ expect ( onUrlUpdate . mock . calls [ 0 ] ! [ 0 ] . queryString ) . toEqual ( '?test=pass' )
795+ await expect ( p ) . resolves . toEqual ( new URLSearchParams ( '?test=pass' ) )
796+ } )
797+
798+ it ( 'does not flush when pushing debounced updates' , async ( ) => {
799+ const onUrlUpdate = vi . fn < OnUrlUpdateFunction > ( )
800+ const { result } = renderHook (
801+ ( ) => useQueryStates ( { test : parseAsString } ) ,
802+ {
803+ wrapper : withNuqsTestingAdapter ( {
804+ onUrlUpdate,
805+ autoResetQueueOnUpdate : false
806+ } )
807+ }
808+ )
809+ // Flush a first time without resetting the queue to keep pending items
810+ // in the global throttle queue.
811+ await act ( ( ) => result . current [ 1 ] ( { test : 'init' } ) )
812+ expect ( onUrlUpdate ) . toHaveBeenCalledOnce ( )
813+ expect ( onUrlUpdate . mock . calls [ 0 ] ! [ 0 ] . queryString ) . toEqual ( '?test=init' )
814+ onUrlUpdate . mockClear ( )
815+ // Now push a debounced update, which should not flush immediately
816+ let p : Promise < URLSearchParams > | undefined = undefined
817+ await act ( async ( ) => {
818+ p = result . current [ 1 ] (
819+ { test : 'pass' } ,
820+ { limitUrlUpdates : debounce ( 100 ) }
821+ )
822+ await waitForNextTick ( )
823+ } )
824+ expect ( onUrlUpdate ) . not . toHaveBeenCalled ( )
825+ await expect ( p ) . resolves . toEqual ( new URLSearchParams ( '?test=pass' ) )
826+ expect ( onUrlUpdate ) . toHaveBeenCalledOnce ( )
827+ expect ( onUrlUpdate . mock . calls [ 0 ] ! [ 0 ] . queryString ) . toEqual ( '?test=pass' )
828+ } )
770829} )
771830
772831describe ( 'useQueryStates: adapter defaults' , ( ) => {
0 commit comments