@@ -54,7 +54,7 @@ const mockEnsureState = (): MockStateShape => {
5454} ;
5555
5656jest . mock ( '@react-navigation/native' , ( ) => {
57- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
57+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- jest.requireActual returns unknown; this is the standard pattern for spreading the actual module in jest.mock factories.
5858 const actualNav = jest . requireActual ( '@react-navigation/native' ) ;
5959 const ReactActual = jest . requireActual < typeof React > ( 'react' ) ;
6060 // Stable navigation object so useCallback([navigation]) doesn't change across renders.
@@ -287,4 +287,30 @@ describe('BaseValidateCodeForm focus behavior on screen focus', () => {
287287 // ref.focus is part of the imperative handle — calling it should not throw.
288288 expect ( ( ) => ref . current ?. focus ( ) ) . not . toThrow ( ) ;
289289 } ) ;
290+
291+ it ( 'does not steal focus if the screen unfocuses before isWindowReadyToFocus resolves' , async ( ) => {
292+ const state = mockEnsureState ( ) ;
293+ state . isMobileSafariReturn = false ;
294+
295+ const { unmount} = renderForm ( ) ;
296+ await waitForBatchedUpdatesWithAct ( ) ;
297+
298+ // transitionEnd fires while the window is still blurred; focusOnce kicks off the pending promise.
299+ await act ( async ( ) => {
300+ for ( const handler of state . transitionEndHandlers ) {
301+ handler ( { data : { closing : false } } ) ;
302+ }
303+ } ) ;
304+
305+ // Screen unmounts (or blurs) before isWindowReadyToFocus settles — cleanup must mark the effect cancelled.
306+ unmount ( ) ;
307+
308+ // Now resolve the window-ready promise. The deferred focus should NOT happen.
309+ await act ( async ( ) => {
310+ state . windowReady . resolve ( ) ;
311+ await state . windowReady . promise ;
312+ } ) ;
313+
314+ expect ( state . focusLastSelected ) . not . toHaveBeenCalled ( ) ;
315+ } ) ;
290316} ) ;
0 commit comments