11import { act , cleanup , render , waitFor } from '@testing-library/react' ;
2- import React from 'react' ;
2+ import React , { useTransition } from 'react' ;
33import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest' ;
44
55import { useDeferredRefresh } from '../useDeferredRefresh' ;
@@ -11,10 +11,38 @@ vi.mock('next/navigation', () => ({
1111} ) ) ;
1212
1313let currentRefresh : ( ( ) => void ) | undefined ;
14+ let startHeldTransition : ( ( ) => void ) | undefined ;
15+ let finishHeldTransition : ( ( ) => void ) | undefined ;
16+
17+ // Suspends inside a transition until the gate promise resolves, keeping
18+ // React's transition lanes pending (the state the deferral gate protects)
19+ const gate : { promise : Promise < void > | null ; done : boolean } = { promise : null , done : false } ;
20+
21+ const Suspender = ( ) => {
22+ if ( ! gate . done ) {
23+ // eslint-disable-next-line @typescript-eslint/only-throw-error
24+ throw gate . promise ;
25+ }
26+ return null ;
27+ } ;
1428
1529const Harness = ( ) => {
1630 currentRefresh = useDeferredRefresh ( ) ;
17- return null ;
31+ const [ suspended , setSuspended ] = React . useState ( false ) ;
32+ const [ , startTransition ] = useTransition ( ) ;
33+ startHeldTransition = ( ) => {
34+ gate . done = false ;
35+ let resolveGate ! : ( ) => void ;
36+ gate . promise = new Promise < void > ( res => {
37+ resolveGate = res ;
38+ } ) ;
39+ finishHeldTransition = ( ) => {
40+ gate . done = true ;
41+ resolveGate ( ) ;
42+ } ;
43+ startTransition ( ( ) => setSuspended ( true ) ) ;
44+ } ;
45+ return < React . Suspense fallback = { null } > { suspended ? < Suspender /> : null } </ React . Suspense > ;
1846} ;
1947
2048const refresh = ( ) => {
@@ -47,6 +75,35 @@ describe('useDeferredRefresh', () => {
4775 } ) ;
4876 } ) ;
4977
78+ it ( 'does not dispatch router.refresh while another transition is pending' , async ( ) => {
79+ render ( < Harness /> ) ;
80+
81+ act ( ( ) => {
82+ startHeldTransition ! ( ) ;
83+ } ) ;
84+
85+ act ( ( ) => {
86+ refresh ( ) ;
87+ } ) ;
88+
89+ // Let effects and microtasks run; the refresh must stay parked while the
90+ // held transition keeps React's transition lanes pending
91+ await act ( async ( ) => {
92+ await new Promise ( res => setTimeout ( res , 20 ) ) ;
93+ } ) ;
94+ expect ( mockRefresh ) . not . toHaveBeenCalled ( ) ;
95+ expect ( window . __clerk_internal_refresh ?. pending ) . toBe ( true ) ;
96+
97+ act ( ( ) => {
98+ finishHeldTransition ! ( ) ;
99+ } ) ;
100+
101+ await waitFor ( ( ) => {
102+ expect ( mockRefresh ) . toHaveBeenCalledTimes ( 1 ) ;
103+ } ) ;
104+ expect ( window . __clerk_internal_refresh ?. pending ) . toBe ( false ) ;
105+ } ) ;
106+
50107 it ( 'coalesces concurrent requests into a single router.refresh' , async ( ) => {
51108 render ( < Harness /> ) ;
52109
0 commit comments