1- import { EmailLinkErrorCodeStatus } from '@clerk/shared/error' ;
1+ import { ClerkRuntimeError , EmailLinkErrorCodeStatus } from '@clerk/shared/error' ;
22import type {
33 ActiveSessionResource ,
44 PendingSessionResource ,
@@ -13,8 +13,10 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test,
1313import { mockJwt } from '@/test/core-fixtures' ;
1414
1515import { mockNativeRuntime } from '../../test/utils' ;
16+ import { AuthCookieService } from '../auth/AuthCookieService' ;
1617import type { DevBrowser } from '../auth/devBrowser' ;
1718import { Clerk } from '../clerk' ;
19+ import * as errorsModule from '../errors' ;
1820import { eventBus , events } from '../events' ;
1921import type { DisplayConfig , Organization } from '../resources/internal' ;
2022import { BaseResource , Client , Environment , SignIn , SignUp } from '../resources/internal' ;
@@ -157,6 +159,118 @@ describe('Clerk singleton', () => {
157159 } ) ;
158160 } ) ;
159161
162+ describe ( 'load retry behavior' , ( ) => {
163+ let originalMountComponentRenderer : typeof Clerk . mountComponentRenderer ;
164+
165+ const createMockAuthService = ( ) => ( {
166+ decorateUrlWithDevBrowserToken : vi . fn ( ( url : URL ) => url ) ,
167+ getSessionCookie : vi . fn ( ( ) => null ) ,
168+ handleUnauthenticatedDevBrowser : vi . fn ( ( ) => Promise . resolve ( ) ) ,
169+ isSignedOut : vi . fn ( ( ) => false ) ,
170+ setClientUatCookieForDevelopmentInstances : vi . fn ( ) ,
171+ startPollingForToken : vi . fn ( ) ,
172+ stopPollingForToken : vi . fn ( ) ,
173+ } ) ;
174+
175+ const createMockComponentControls = ( ) => {
176+ const componentInstance = {
177+ mountImpersonationFab : vi . fn ( ) ,
178+ updateProps : vi . fn ( ) ,
179+ } ;
180+
181+ return {
182+ ensureMounted : vi . fn ( ) . mockResolvedValue ( componentInstance ) ,
183+ prioritizedOn : vi . fn ( ) ,
184+ } ;
185+ } ;
186+
187+ beforeEach ( ( ) => {
188+ originalMountComponentRenderer = Clerk . mountComponentRenderer ;
189+ } ) ;
190+
191+ afterEach ( ( ) => {
192+ Clerk . mountComponentRenderer = originalMountComponentRenderer ;
193+ vi . useRealTimers ( ) ;
194+ } ) ;
195+
196+ it ( 'retries once when dev browser authentication is lost' , async ( ) => {
197+ vi . useFakeTimers ( ) ;
198+
199+ const mockAuthService = createMockAuthService ( ) ;
200+ const authCreateSpy = vi
201+ . spyOn ( AuthCookieService , 'create' )
202+ . mockResolvedValue ( mockAuthService as unknown as AuthCookieService ) ;
203+
204+ const componentControls = createMockComponentControls ( ) ;
205+ const devBrowserError = Object . assign ( new Error ( 'dev browser unauthenticated' ) , {
206+ errors : [ { code : 'dev_browser_unauthenticated' } ] ,
207+ status : 401 ,
208+ } ) ;
209+
210+ const mountSpy = vi
211+ . fn < NonNullable < typeof Clerk . mountComponentRenderer > > ( )
212+ . mockImplementationOnce ( ( ) => {
213+ throw devBrowserError ;
214+ } )
215+ . mockReturnValue ( componentControls ) ;
216+
217+ Clerk . mountComponentRenderer = mountSpy ;
218+ mockClientFetch . mockClear ( ) ;
219+
220+ const sut = new Clerk ( productionPublishableKey ) ;
221+
222+ try {
223+ const loadPromise = sut . load ( ) ;
224+
225+ await vi . advanceTimersByTimeAsync ( 2000 ) ;
226+ await loadPromise ;
227+ } finally {
228+ authCreateSpy . mockRestore ( ) ;
229+ }
230+
231+ expect ( mountSpy ) . toHaveBeenCalledTimes ( 2 ) ;
232+ expect ( mockAuthService . handleUnauthenticatedDevBrowser ) . toHaveBeenCalledTimes ( 1 ) ;
233+ expect ( mockClientFetch ) . toHaveBeenCalledTimes ( 2 ) ;
234+ } ) ;
235+
236+ it ( 'surfaces network errors after exhausting retries' , async ( ) => {
237+ vi . useFakeTimers ( ) ;
238+
239+ const mockAuthService = createMockAuthService ( ) ;
240+ const authCreateSpy = vi
241+ . spyOn ( AuthCookieService , 'create' )
242+ . mockResolvedValue ( mockAuthService as unknown as AuthCookieService ) ;
243+
244+ const networkError = new ClerkRuntimeError ( 'Network failure' , { code : 'network_error' } ) ;
245+ const mountSpy = vi . fn < NonNullable < typeof Clerk . mountComponentRenderer > > ( ) . mockImplementation ( ( ) => {
246+ throw networkError ;
247+ } ) ;
248+
249+ Clerk . mountComponentRenderer = mountSpy ;
250+ mockClientFetch . mockClear ( ) ;
251+
252+ const errorSpy = vi . spyOn ( errorsModule , 'clerkErrorInitFailed' ) ;
253+ const sut = new Clerk ( productionPublishableKey ) ;
254+
255+ try {
256+ const loadPromise = sut . load ( ) ;
257+ const expectationPromise = expect ( loadPromise ) . rejects . toThrow ( / C l e r k : N e t w o r k f a i l u r e / ) ;
258+
259+ await vi . advanceTimersByTimeAsync ( 2000 ) ;
260+ await expectationPromise ;
261+
262+ expect ( mountSpy ) . toHaveBeenCalledTimes ( 2 ) ;
263+ expect ( mockClientFetch ) . toHaveBeenCalledTimes ( 2 ) ;
264+ expect ( errorSpy ) . toHaveBeenCalledTimes ( 1 ) ;
265+ expect ( errorSpy ) . toHaveBeenLastCalledWith ( networkError ) ;
266+ expect ( mockAuthService . handleUnauthenticatedDevBrowser ) . not . toHaveBeenCalled ( ) ;
267+ } finally {
268+ authCreateSpy . mockRestore ( ) ;
269+ errorSpy . mockRestore ( ) ;
270+ }
271+ } ) ;
272+ } ) ;
273+
160274 describe ( '.setActive' , ( ) => {
161275 describe ( 'with `active` session status' , ( ) => {
162276 const mockSession = {
0 commit comments