@@ -82,8 +82,6 @@ const zkEvmProfileData = {
8282const mockErrorMsg = 'NONO' ;
8383
8484describe ( 'AuthManager' , ( ) => {
85- afterEach ( jest . resetAllMocks ) ;
86-
8785 let authManager : AuthManager ;
8886 let mockSigninPopup : jest . Mock ;
8987 let mockSigninCallback : jest . Mock ;
@@ -96,8 +94,16 @@ describe('AuthManager', () => {
9694 let mockOverlayAppend : jest . Mock ;
9795 let mockOverlayRemove : jest . Mock ;
9896 let mockRevokeTokens : jest . Mock ;
97+ let originalWindowOpen : any ;
9998
10099 beforeEach ( ( ) => {
100+ // Store original window.open and replace with mock globally
101+ originalWindowOpen = window . open ;
102+ window . open = jest . fn ( ) . mockReturnValue ( {
103+ closed : false ,
104+ close : jest . fn ( ) ,
105+ } ) ;
106+
101107 mockSigninPopup = jest . fn ( ) ;
102108 mockSigninCallback = jest . fn ( ) ;
103109 mockSigninRedirectCallback = jest . fn ( ) ;
@@ -132,6 +138,12 @@ describe('AuthManager', () => {
132138 authManager = new AuthManager ( getConfig ( ) ) ;
133139 } ) ;
134140
141+ afterEach ( ( ) => {
142+ jest . resetAllMocks ( ) ;
143+ // Restore original window.open
144+ window . open = originalWindowOpen ;
145+ } ) ;
146+
135147 describe ( 'constructor' , ( ) => {
136148 it ( 'should initialise AuthManager with the correct default configuration' , ( ) => {
137149 const config = getConfig ( ) ;
@@ -204,6 +216,90 @@ describe('AuthManager', () => {
204216 } ) ;
205217 } ) ;
206218
219+ describe ( 'popup closed detection' , ( ) => {
220+ let mockPopupRef : any ;
221+ let mockSetInterval : jest . SpyInstance ;
222+ let mockClearInterval : jest . SpyInstance ;
223+
224+ beforeEach ( ( ) => {
225+ mockPopupRef = {
226+ closed : false ,
227+ close : jest . fn ( ) ,
228+ } ;
229+
230+ // Override the global mock for these tests
231+ ( window . open as jest . Mock ) . mockReturnValue ( mockPopupRef ) ;
232+
233+ mockSetInterval = jest . spyOn ( global , 'setInterval' ) ;
234+ mockClearInterval = jest . spyOn ( global , 'clearInterval' ) ;
235+ } ) ;
236+
237+ afterEach ( ( ) => {
238+ mockSetInterval . mockRestore ( ) ;
239+ mockClearInterval . mockRestore ( ) ;
240+ } ) ;
241+
242+ it ( 'should reject with "Popup closed by user" when popup is closed during authentication' , async ( ) => {
243+ // Mock signinPopup to return a promise that never resolves (simulating long authentication)
244+ const signinPromise = new Promise < any > ( ( ) => {
245+ // Promise never resolves to simulate ongoing authentication
246+ } ) ;
247+ mockSigninPopup . mockReturnValue ( signinPromise ) ;
248+
249+ // Start the login process
250+ const loginPromise = authManager . login ( ) ;
251+
252+ // Wait for the polling to be set up
253+ await new Promise ( ( resolve ) => {
254+ setTimeout ( resolve , 10 ) ;
255+ } ) ;
256+
257+ // Verify polling was set up
258+ expect ( mockSetInterval ) . toHaveBeenCalledWith ( expect . any ( Function ) , 500 ) ;
259+
260+ // Get the polling function and simulate popup being closed
261+ const pollingCallback = mockSetInterval . mock . calls [ 0 ] [ 0 ] ;
262+ mockPopupRef . closed = true ;
263+ pollingCallback ( ) ; // Manually trigger the polling callback
264+
265+ // Expect the login to reject with popup closed error
266+ await expect ( loginPromise ) . rejects . toThrow ( 'Popup closed by user' ) ;
267+ } ) ;
268+
269+ it ( 'should clean up timer when user authentication completes successfully' , async ( ) => {
270+ mockSigninPopup . mockResolvedValue ( mockOidcUser ) ;
271+
272+ await authManager . login ( ) ;
273+
274+ // Verify timer was cleared
275+ expect ( mockClearInterval ) . toHaveBeenCalled ( ) ;
276+ // Verify popup was closed
277+ expect ( mockPopupRef . close ) . toHaveBeenCalled ( ) ;
278+ } ) ;
279+
280+ it ( 'should fall back to original behavior when popup reference is not available' , async ( ) => {
281+ // Mock window.open to return null (popup blocked or failed)
282+ ( window . open as jest . Mock ) . mockReturnValue ( null ) ;
283+ mockSigninPopup . mockResolvedValue ( mockOidcUser ) ;
284+
285+ const result = await authManager . login ( ) ;
286+
287+ expect ( result ) . toEqual ( mockUser ) ;
288+ // Should not set up polling when no popup reference
289+ expect ( mockSetInterval ) . not . toHaveBeenCalled ( ) ;
290+ } ) ;
291+
292+ it ( 'should use correct polling duration constant' , async ( ) => {
293+ const neverResolvingPromise = new Promise ( ( ) => { } ) ;
294+ mockSigninPopup . mockReturnValue ( neverResolvingPromise ) ;
295+
296+ authManager . login ( ) . catch ( ( ) => { } ) ; // Ignore rejection for this test
297+
298+ // Verify the polling interval uses the correct constant (500ms)
299+ expect ( mockSetInterval ) . toHaveBeenCalledWith ( expect . any ( Function ) , 500 ) ;
300+ } ) ;
301+ } ) ;
302+
207303 describe ( 'when the user has registered for imx' , ( ) => {
208304 it ( 'should populate the imx object' , async ( ) => {
209305 mockSigninPopup . mockResolvedValue ( mockOidcUser ) ;
@@ -269,7 +365,9 @@ describe('AuthManager', () => {
269365 } ) ;
270366
271367 it ( 'should throw the error if user is failed to login' , async ( ) => {
272- mockSigninPopup . mockRejectedValue ( new Error ( mockErrorMsg ) ) ;
368+ mockSigninPopup . mockImplementation ( ( ) => {
369+ throw new Error ( mockErrorMsg ) ;
370+ } ) ;
273371
274372 await expect ( ( ) => authManager . login ( ) ) . rejects . toThrow (
275373 new PassportError (
@@ -282,7 +380,9 @@ describe('AuthManager', () => {
282380
283381 describe ( 'when the popup is blocked' , ( ) => {
284382 beforeEach ( ( ) => {
285- mockSigninPopup . mockRejectedValueOnce ( new Error ( 'Attempted to navigate on a disposed window' ) ) ;
383+ mockSigninPopup . mockImplementationOnce ( ( ) => {
384+ throw new Error ( 'Attempted to navigate on a disposed window' ) ;
385+ } ) ;
286386 } ) ;
287387
288388 it ( 'should render the blocked popup overlay' , async ( ) => {
@@ -326,7 +426,9 @@ describe('AuthManager', () => {
326426
327427 describe ( 'and the user closes the popup' , ( ) => {
328428 it ( 'should throw an error' , async ( ) => {
329- mockSigninPopup . mockRejectedValueOnce ( new Error ( 'Popup closed by user' ) ) ;
429+ mockSigninPopup . mockImplementationOnce ( ( ) => {
430+ throw new Error ( 'Popup closed by user' ) ;
431+ } ) ;
330432
331433 await expect ( ( ) => authManager . login ( ) ) . rejects . toThrow (
332434 new Error ( 'Popup closed by user' ) ,
@@ -368,7 +470,9 @@ describe('AuthManager', () => {
368470
369471 describe ( 'when getUser throws an error' , ( ) => {
370472 it ( 'calls attempts to sign in the user using signinPopup' , async ( ) => {
371- mockGetUser . mockRejectedValue ( new Error ( mockErrorMsg ) ) ;
473+ mockGetUser . mockImplementation ( ( ) => {
474+ throw new Error ( mockErrorMsg ) ;
475+ } ) ;
372476 mockSigninPopup . mockReturnValue ( mockOidcUser ) ;
373477 ( isAccessTokenExpiredOrExpiring as jest . Mock ) . mockReturnValue ( false ) ;
374478
@@ -380,6 +484,30 @@ describe('AuthManager', () => {
380484 } ) ;
381485
382486 describe ( 'loginCallback' , ( ) => {
487+ let mockSigninPopupCallback : jest . Mock ;
488+
489+ beforeEach ( ( ) => {
490+ mockSigninPopupCallback = jest . fn ( ) ;
491+ ( UserManager as jest . Mock ) . mockImplementation ( ( config ) => ( {
492+ signinPopup : mockSigninPopup ,
493+ signinCallback : mockSigninCallback ,
494+ signinPopupCallback : mockSigninPopupCallback ,
495+ signinRedirectCallback : mockSigninRedirectCallback ,
496+ signoutRedirect : mockSignoutRedirect ,
497+ signoutSilent : mockSignoutSilent ,
498+ getUser : mockGetUser ,
499+ signinSilent : mockSigninSilent ,
500+ storeUser : mockStoreUser ,
501+ revokeTokens : mockRevokeTokens ,
502+ settings : {
503+ metadata : {
504+ end_session_endpoint : config . metadata ?. end_session_endpoint ,
505+ } ,
506+ } ,
507+ } ) ) ;
508+ authManager = new AuthManager ( getConfig ( ) ) ;
509+ } ) ;
510+
383511 it ( 'should call login callback' , async ( ) => {
384512 await authManager . loginCallback ( ) ;
385513
@@ -437,7 +565,9 @@ describe('AuthManager', () => {
437565 logoutMode : 'redirect' ,
438566 } ) ;
439567 const manager = new AuthManager ( configuration ) ;
440- mockRevokeTokens . mockRejectedValue ( new Error ( mockErrorMsg ) ) ;
568+ mockRevokeTokens . mockImplementation ( ( ) => {
569+ throw new Error ( mockErrorMsg ) ;
570+ } ) ;
441571
442572 await expect ( ( ) => manager . logout ( ) ) . rejects . toThrow (
443573 new PassportError (
@@ -453,7 +583,9 @@ describe('AuthManager', () => {
453583 logoutMode : 'silent' ,
454584 } ) ;
455585 const manager = new AuthManager ( configuration ) ;
456- mockRevokeTokens . mockRejectedValue ( new Error ( mockErrorMsg ) ) ;
586+ mockRevokeTokens . mockImplementation ( ( ) => {
587+ throw new Error ( mockErrorMsg ) ;
588+ } ) ;
457589
458590 await expect ( ( ) => manager . logout ( ) ) . rejects . toThrow (
459591 new PassportError (
@@ -471,7 +603,9 @@ describe('AuthManager', () => {
471603 } ) ;
472604 const manager = new AuthManager ( configuration ) ;
473605
474- mockSignoutRedirect . mockRejectedValue ( new Error ( mockErrorMsg ) ) ;
606+ mockSignoutRedirect . mockImplementation ( ( ) => {
607+ throw new Error ( mockErrorMsg ) ;
608+ } ) ;
475609
476610 await expect ( ( ) => manager . logout ( ) ) . rejects . toThrow (
477611 new PassportError (
@@ -488,7 +622,9 @@ describe('AuthManager', () => {
488622 } ) ;
489623 const manager = new AuthManager ( configuration ) ;
490624
491- mockSignoutSilent . mockRejectedValue ( new Error ( mockErrorMsg ) ) ;
625+ mockSignoutSilent . mockImplementation ( ( ) => {
626+ throw new Error ( mockErrorMsg ) ;
627+ } ) ;
492628
493629 await expect ( ( ) => manager . logout ( ) ) . rejects . toThrow (
494630 new PassportError (
0 commit comments