From fc186750f2f41c8196cb3493266f005bad8fca43 Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Wed, 18 Mar 2026 14:20:01 +0000 Subject: [PATCH 01/13] Fix onboarding flow not appearing after anonymous-to-authenticated sign-in When an anonymous user signs in via the SignInModal (e.g. "Reply in thread" in a public room), Navigation.dismissModal() and openApp() were firing in parallel. The dismiss triggered the OnboardingGuard while IS_LOADING_APP was still true, causing it to skip the onboarding redirect entirely. Now we ensure openApp(true) fully completes (IS_LOADING_APP transitions to false, NVP_ONBOARDING is loaded) before dismissing the modal, so the OnboardingGuard evaluates with accurate data and properly redirects new users to onboarding. Co-authored-by: Eric Han --- src/pages/signin/SignInModal.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pages/signin/SignInModal.tsx b/src/pages/signin/SignInModal.tsx index 44cb7dd93d75..dcdaed946161 100644 --- a/src/pages/signin/SignInModal.tsx +++ b/src/pages/signin/SignInModal.tsx @@ -34,16 +34,20 @@ function SignInModal() { useEffect(() => { const isAnonymousUser = session?.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS; if (!isAnonymousUser) { - // Signing in RHP is only for anonymous users - Navigation.isNavigationReady().then(() => { - Navigation.dismissModal(); - }); - // To prevent deadlock when OpenReport and OpenApp overlap, wait for the queue to be idle before calling openApp. // This ensures that any communication gaps between the client and server during OpenReport processing do not cause the queue to pause, // which would prevent us from processing or clearing the queue. - waitForIdle().then(() => { - openApp(true); + // + // We must wait for openApp to fully complete (IS_LOADING_APP → false, NVP_ONBOARDING loaded) before dismissing the modal, + // so the OnboardingGuard can properly evaluate and redirect new users to onboarding. + // Without this, dismissModal() triggers the guard while IS_LOADING_APP is still true, causing it to skip the onboarding redirect. + Promise.all([ + waitForIdle() + .then(() => openApp(true)) + .then(() => waitForIdle()), + Navigation.isNavigationReady(), + ]).then(() => { + Navigation.dismissModal(); }); } }, [session?.authTokenType]); From 176c12e7bad124ba0faf4ca8c3b42bf0b0c9de25 Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 08:45:49 +0000 Subject: [PATCH 02/13] Add unit tests for SignInModal useEffect behavior Tests cover: - Anonymous users don't trigger openApp or dismissModal - Non-anonymous users trigger openApp then dismissModal - dismissModal waits for both openApp chain and isNavigationReady (Promise.all) - Session transition from anonymous to authenticated triggers the correct flow Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 188 +++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 tests/unit/SignInModalTest.tsx diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx new file mode 100644 index 000000000000..041721fbf7b1 --- /dev/null +++ b/tests/unit/SignInModalTest.tsx @@ -0,0 +1,188 @@ +import {render} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +const mockOpenApp = jest.fn(() => Promise.resolve()); +const mockWaitForIdle = jest.fn(() => Promise.resolve()); +const mockDismissModal = jest.fn(); +const mockIsNavigationReady = jest.fn(() => Promise.resolve()); +const mockGoBack = jest.fn(); + +jest.mock('@libs/actions/App', () => ({ + openApp: (...args: unknown[]) => mockOpenApp(...args), +})); + +jest.mock('@libs/Network/SequentialQueue', () => ({ + waitForIdle: () => mockWaitForIdle(), +})); + +jest.mock('@libs/Navigation/Navigation', () => ({ + __esModule: true, + default: { + dismissModal: (...args: unknown[]) => mockDismissModal(...args), + isNavigationReady: () => mockIsNavigationReady(), + goBack: (...args: unknown[]) => mockGoBack(...args), + }, +})); + +jest.mock('@libs/Browser', () => ({ + isMobileSafari: jest.fn(() => false), +})); + +jest.mock('@hooks/useAndroidBackButtonHandler', () => jest.fn()); + +jest.mock('@pages/signin/SignInPage', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires, rulesdir/no-inline-named-export + const React = require('react'); + const {View} = require('react-native'); + const MockSignInPage = React.forwardRef((_props: object, _ref: React.Ref) => ); + MockSignInPage.displayName = 'MockSignInPage'; + return { + __esModule: true, + default: MockSignInPage, + SignInPage: MockSignInPage, + }; +}); + +jest.mock('@components/HeaderWithBackButton', () => { + const {View} = require('react-native'); + return () => ; +}); + +jest.mock('@components/ScreenWrapper', () => { + const {View} = require('react-native'); + return ({children}: {children: React.ReactNode}) => {children}; +}); + +// eslint-disable-next-line rulesdir/no-inline-named-export +import SignInModal from '@pages/signin/SignInModal'; + +describe('SignInModal', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + afterEach(async () => { + await Onyx.clear(); + }); + + it('should not call openApp or dismissModal when user is anonymous', async () => { + await Onyx.merge(ONYXKEYS.SESSION, { + authToken: 'anonymousToken', + authTokenType: CONST.AUTH_TOKEN_TYPES.ANONYMOUS, + }); + await waitForBatchedUpdates(); + + render(); + await waitForBatchedUpdatesWithAct(); + + expect(mockOpenApp).not.toHaveBeenCalled(); + expect(mockDismissModal).not.toHaveBeenCalled(); + }); + + it('should call openApp and dismissModal when user is not anonymous', async () => { + await Onyx.merge(ONYXKEYS.SESSION, { + authToken: 'realToken', + authTokenType: undefined, + }); + await waitForBatchedUpdates(); + + render(); + await waitForBatchedUpdatesWithAct(); + + expect(mockWaitForIdle).toHaveBeenCalled(); + expect(mockOpenApp).toHaveBeenCalledWith(true); + expect(mockDismissModal).toHaveBeenCalledTimes(1); + }); + + it('should wait for both openApp chain and isNavigationReady before dismissing modal', async () => { + const callOrder: string[] = []; + + // Make openApp chain take time to resolve + let resolveOpenApp: () => void; + const openAppPromise = new Promise((resolve) => { + resolveOpenApp = resolve; + }); + mockOpenApp.mockImplementation(() => { + callOrder.push('openApp'); + return openAppPromise; + }); + + // Make isNavigationReady take time to resolve + let resolveNavReady: () => void; + const navReadyPromise = new Promise((resolve) => { + resolveNavReady = resolve; + }); + mockIsNavigationReady.mockImplementation(() => { + callOrder.push('isNavigationReady'); + return navReadyPromise; + }); + + mockDismissModal.mockImplementation(() => { + callOrder.push('dismissModal'); + }); + + await Onyx.merge(ONYXKEYS.SESSION, { + authToken: 'realToken', + authTokenType: undefined, + }); + await waitForBatchedUpdates(); + + render(); + await waitForBatchedUpdatesWithAct(); + + // At this point, openApp and isNavigationReady are pending - dismissModal should NOT have been called + expect(mockDismissModal).not.toHaveBeenCalled(); + + // Resolve isNavigationReady first - dismissModal should still wait for openApp + resolveNavReady!(); + await waitForBatchedUpdatesWithAct(); + expect(mockDismissModal).not.toHaveBeenCalled(); + + // Now resolve openApp - dismissModal should be called after both are done + resolveOpenApp!(); + await waitForBatchedUpdatesWithAct(); + expect(mockDismissModal).toHaveBeenCalledTimes(1); + + // Verify dismissModal was called after both openApp and isNavigationReady + expect(callOrder.indexOf('dismissModal')).toBeGreaterThan(callOrder.indexOf('openApp')); + expect(callOrder.indexOf('dismissModal')).toBeGreaterThan(callOrder.indexOf('isNavigationReady')); + }); + + it('should call openApp and dismissModal when session transitions from anonymous to authenticated', async () => { + // Start as anonymous + await Onyx.merge(ONYXKEYS.SESSION, { + authToken: 'anonymousToken', + authTokenType: CONST.AUTH_TOKEN_TYPES.ANONYMOUS, + }); + await waitForBatchedUpdates(); + + const {rerender} = render(); + await waitForBatchedUpdatesWithAct(); + + expect(mockOpenApp).not.toHaveBeenCalled(); + expect(mockDismissModal).not.toHaveBeenCalled(); + + // Transition to authenticated user + await Onyx.merge(ONYXKEYS.SESSION, { + authToken: 'realToken', + authTokenType: undefined, + }); + await waitForBatchedUpdates(); + + rerender(); + await waitForBatchedUpdatesWithAct(); + + expect(mockOpenApp).toHaveBeenCalledWith(true); + expect(mockDismissModal).toHaveBeenCalledTimes(1); + }); +}); From 8bd17bdd94ba82d749ef8c6b32d211cef955a2e9 Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 08:47:46 +0000 Subject: [PATCH 03/13] Fix: run prettier on SignInModalTest.tsx Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index 041721fbf7b1..b5973863a2d3 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -1,5 +1,7 @@ import {render} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; +// eslint-disable-next-line rulesdir/no-inline-named-export +import SignInModal from '@pages/signin/SignInModal'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -57,9 +59,6 @@ jest.mock('@components/ScreenWrapper', () => { return ({children}: {children: React.ReactNode}) => {children}; }); -// eslint-disable-next-line rulesdir/no-inline-named-export -import SignInModal from '@pages/signin/SignInModal'; - describe('SignInModal', () => { beforeAll(() => { Onyx.init({keys: ONYXKEYS}); From d49fd5c1dc3bb43926809807700da766fb2897ee Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 08:53:04 +0000 Subject: [PATCH 04/13] Fix: resolve ESLint errors in SignInModalTest.tsx - Add type-safe casts for require() calls inside jest.mock factories - Add eslint-disable comments for __esModule naming convention - Add file-level disable for no-non-null-assertion - Use React.createElement instead of JSX in mock factories Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index b5973863a2d3..60dd6faf5973 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -1,4 +1,7 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import {render} from '@testing-library/react-native'; +import React from 'react'; +import type {View as RNView} from 'react-native'; import Onyx from 'react-native-onyx'; // eslint-disable-next-line rulesdir/no-inline-named-export import SignInModal from '@pages/signin/SignInModal'; @@ -22,6 +25,7 @@ jest.mock('@libs/Network/SequentialQueue', () => ({ })); jest.mock('@libs/Navigation/Navigation', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention -- __esModule is required by Jest to properly mock ES modules with default exports __esModule: true, default: { dismissModal: (...args: unknown[]) => mockDismissModal(...args), @@ -37,12 +41,12 @@ jest.mock('@libs/Browser', () => ({ jest.mock('@hooks/useAndroidBackButtonHandler', () => jest.fn()); jest.mock('@pages/signin/SignInPage', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires, rulesdir/no-inline-named-export - const React = require('react'); - const {View} = require('react-native'); - const MockSignInPage = React.forwardRef((_props: object, _ref: React.Ref) => ); + const MockReact = require('react') as typeof React; + const {View} = require('react-native') as {View: typeof RNView}; + const MockSignInPage = MockReact.forwardRef((_, ref: React.Ref) => MockReact.createElement(View, {testID: 'MockSignInPage', ref})); MockSignInPage.displayName = 'MockSignInPage'; return { + // eslint-disable-next-line @typescript-eslint/naming-convention -- __esModule is required by Jest to properly mock ES modules with default exports __esModule: true, default: MockSignInPage, SignInPage: MockSignInPage, @@ -50,13 +54,15 @@ jest.mock('@pages/signin/SignInPage', () => { }); jest.mock('@components/HeaderWithBackButton', () => { - const {View} = require('react-native'); - return () => ; + const MockReact = require('react') as typeof React; + const {View} = require('react-native') as {View: typeof RNView}; + return () => MockReact.createElement(View, {testID: 'MockHeaderWithBackButton'}); }); jest.mock('@components/ScreenWrapper', () => { - const {View} = require('react-native'); - return ({children}: {children: React.ReactNode}) => {children}; + const MockReact = require('react') as typeof React; + const {View} = require('react-native') as {View: typeof RNView}; + return ({children}: {children: React.ReactNode}) => MockReact.createElement(View, {testID: 'MockScreenWrapper'}, children); }); describe('SignInModal', () => { From 1902a19a0823509ef2769b046604d5f44648bd38 Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 08:58:01 +0000 Subject: [PATCH 05/13] Fix: resolve ESLint and TypeScript errors in SignInModalTest Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index 60dd6faf5973..03b2807c11e2 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -17,7 +17,7 @@ const mockIsNavigationReady = jest.fn(() => Promise.resolve()); const mockGoBack = jest.fn(); jest.mock('@libs/actions/App', () => ({ - openApp: (...args: unknown[]) => mockOpenApp(...args), + openApp: mockOpenApp, })); jest.mock('@libs/Network/SequentialQueue', () => ({ @@ -28,9 +28,9 @@ jest.mock('@libs/Navigation/Navigation', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention -- __esModule is required by Jest to properly mock ES modules with default exports __esModule: true, default: { - dismissModal: (...args: unknown[]) => mockDismissModal(...args), + dismissModal: mockDismissModal, isNavigationReady: () => mockIsNavigationReady(), - goBack: (...args: unknown[]) => mockGoBack(...args), + goBack: mockGoBack, }, })); @@ -43,7 +43,7 @@ jest.mock('@hooks/useAndroidBackButtonHandler', () => jest.fn()); jest.mock('@pages/signin/SignInPage', () => { const MockReact = require('react') as typeof React; const {View} = require('react-native') as {View: typeof RNView}; - const MockSignInPage = MockReact.forwardRef((_, ref: React.Ref) => MockReact.createElement(View, {testID: 'MockSignInPage', ref})); + const MockSignInPage = MockReact.forwardRef(() => MockReact.createElement(View, {testID: 'MockSignInPage'})); MockSignInPage.displayName = 'MockSignInPage'; return { // eslint-disable-next-line @typescript-eslint/naming-convention -- __esModule is required by Jest to properly mock ES modules with default exports From 47b7a58987d2f39720d194b02e1230a79e2a0d78 Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 09:02:43 +0000 Subject: [PATCH 06/13] Fix: add missing isSafari mock in SignInModalTest Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index 03b2807c11e2..2f8c58084f94 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -36,6 +36,7 @@ jest.mock('@libs/Navigation/Navigation', () => ({ jest.mock('@libs/Browser', () => ({ isMobileSafari: jest.fn(() => false), + isSafari: jest.fn(() => false), })); jest.mock('@hooks/useAndroidBackButtonHandler', () => jest.fn()); From bbc0e3934a63772254be3774ba2a0d0f1ae57bc3 Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 09:08:07 +0000 Subject: [PATCH 07/13] Fix: add missing getBrowser and other Browser module exports to mock Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index 2f8c58084f94..0e50989bc152 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -35,8 +35,16 @@ jest.mock('@libs/Navigation/Navigation', () => ({ })); jest.mock('@libs/Browser', () => ({ + getBrowser: jest.fn(() => ''), + isMobile: jest.fn(() => false), + isMobileIOS: jest.fn(() => false), isMobileSafari: jest.fn(() => false), + isMobileWebKit: jest.fn(() => false), isSafari: jest.fn(() => false), + isModernSafari: jest.fn(() => false), + isMobileChrome: jest.fn(() => false), + isChromeIOS: jest.fn(() => false), + isMobileSafariOnIos26: jest.fn(() => false), })); jest.mock('@hooks/useAndroidBackButtonHandler', () => jest.fn()); From 738faa80e4b56b7e5fe438d946cf1f97dfb26a1c Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 09:20:54 +0000 Subject: [PATCH 08/13] Fix: resolve test failures in SignInModalTest - Add missing `flush` export to SequentialQueue mock (Network module imports it during initialization) - Mock `useSession` from OnyxListItemProvider to avoid requiring the full context provider tree in unit tests - Update tests to set session data via the mock instead of Onyx.merge Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index 0e50989bc152..53f241abf5af 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -15,12 +15,14 @@ const mockWaitForIdle = jest.fn(() => Promise.resolve()); const mockDismissModal = jest.fn(); const mockIsNavigationReady = jest.fn(() => Promise.resolve()); const mockGoBack = jest.fn(); +let mockSessionData: {authToken?: string; authTokenType?: string} | undefined; jest.mock('@libs/actions/App', () => ({ openApp: mockOpenApp, })); jest.mock('@libs/Network/SequentialQueue', () => ({ + flush: jest.fn(), waitForIdle: () => mockWaitForIdle(), })); @@ -49,6 +51,10 @@ jest.mock('@libs/Browser', () => ({ jest.mock('@hooks/useAndroidBackButtonHandler', () => jest.fn()); +jest.mock('@components/OnyxListItemProvider', () => ({ + useSession: () => mockSessionData, +})); + jest.mock('@pages/signin/SignInPage', () => { const MockReact = require('react') as typeof React; const {View} = require('react-native') as {View: typeof RNView}; @@ -81,6 +87,7 @@ describe('SignInModal', () => { beforeEach(async () => { jest.clearAllMocks(); + mockSessionData = undefined; await Onyx.clear(); await waitForBatchedUpdates(); }); @@ -90,11 +97,10 @@ describe('SignInModal', () => { }); it('should not call openApp or dismissModal when user is anonymous', async () => { - await Onyx.merge(ONYXKEYS.SESSION, { + mockSessionData = { authToken: 'anonymousToken', authTokenType: CONST.AUTH_TOKEN_TYPES.ANONYMOUS, - }); - await waitForBatchedUpdates(); + }; render(); await waitForBatchedUpdatesWithAct(); @@ -104,11 +110,10 @@ describe('SignInModal', () => { }); it('should call openApp and dismissModal when user is not anonymous', async () => { - await Onyx.merge(ONYXKEYS.SESSION, { + mockSessionData = { authToken: 'realToken', authTokenType: undefined, - }); - await waitForBatchedUpdates(); + }; render(); await waitForBatchedUpdatesWithAct(); @@ -145,11 +150,10 @@ describe('SignInModal', () => { callOrder.push('dismissModal'); }); - await Onyx.merge(ONYXKEYS.SESSION, { + mockSessionData = { authToken: 'realToken', authTokenType: undefined, - }); - await waitForBatchedUpdates(); + }; render(); await waitForBatchedUpdatesWithAct(); @@ -174,11 +178,10 @@ describe('SignInModal', () => { it('should call openApp and dismissModal when session transitions from anonymous to authenticated', async () => { // Start as anonymous - await Onyx.merge(ONYXKEYS.SESSION, { + mockSessionData = { authToken: 'anonymousToken', authTokenType: CONST.AUTH_TOKEN_TYPES.ANONYMOUS, - }); - await waitForBatchedUpdates(); + }; const {rerender} = render(); await waitForBatchedUpdatesWithAct(); @@ -187,11 +190,10 @@ describe('SignInModal', () => { expect(mockDismissModal).not.toHaveBeenCalled(); // Transition to authenticated user - await Onyx.merge(ONYXKEYS.SESSION, { + mockSessionData = { authToken: 'realToken', authTokenType: undefined, - }); - await waitForBatchedUpdates(); + }; rerender(); await waitForBatchedUpdatesWithAct(); From 69202e7eb4f7c4be8d181fe42d4a17b689afa799 Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 09:31:07 +0000 Subject: [PATCH 09/13] Fix: use wrapper function for openApp mock to avoid hoisting issue Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index 53f241abf5af..ef0207d8106d 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -18,7 +18,7 @@ const mockGoBack = jest.fn(); let mockSessionData: {authToken?: string; authTokenType?: string} | undefined; jest.mock('@libs/actions/App', () => ({ - openApp: mockOpenApp, + openApp: (...args: unknown[]) => mockOpenApp(...args), })); jest.mock('@libs/Network/SequentialQueue', () => ({ From 7f802b10bfbf69da8bd938dad2de0c80d8602a6a Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 09:40:38 +0000 Subject: [PATCH 10/13] Fix: use direct jest.fn() in mock factory for openApp The inline mock factory for @libs/actions/App was not properly overriding the manual __mocks__/App.ts mock. Switch to defining jest.fn() directly in the factory and importing from the mocked module, matching the pattern used by other tests (e.g. useConfirmReadyToOpenApp.test.ts). Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index ef0207d8106d..3c88d343bf62 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -3,6 +3,7 @@ import {render} from '@testing-library/react-native'; import React from 'react'; import type {View as RNView} from 'react-native'; import Onyx from 'react-native-onyx'; +import {openApp} from '@libs/actions/App'; // eslint-disable-next-line rulesdir/no-inline-named-export import SignInModal from '@pages/signin/SignInModal'; import CONST from '@src/CONST'; @@ -10,7 +11,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -const mockOpenApp = jest.fn(() => Promise.resolve()); const mockWaitForIdle = jest.fn(() => Promise.resolve()); const mockDismissModal = jest.fn(); const mockIsNavigationReady = jest.fn(() => Promise.resolve()); @@ -18,9 +18,11 @@ const mockGoBack = jest.fn(); let mockSessionData: {authToken?: string; authTokenType?: string} | undefined; jest.mock('@libs/actions/App', () => ({ - openApp: (...args: unknown[]) => mockOpenApp(...args), + openApp: jest.fn(() => Promise.resolve()), })); +const mockOpenApp = openApp as jest.Mock; + jest.mock('@libs/Network/SequentialQueue', () => ({ flush: jest.fn(), waitForIdle: () => mockWaitForIdle(), From b566ec7a7ca296ee6e0fa8c068293fde722b4983 Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 09:48:50 +0000 Subject: [PATCH 11/13] Fix: use wrapper functions in Navigation mock to avoid hoisting issue Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index 3c88d343bf62..e77fd207b998 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -32,9 +32,9 @@ jest.mock('@libs/Navigation/Navigation', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention -- __esModule is required by Jest to properly mock ES modules with default exports __esModule: true, default: { - dismissModal: mockDismissModal, + dismissModal: (...args: unknown[]) => mockDismissModal(...args), isNavigationReady: () => mockIsNavigationReady(), - goBack: mockGoBack, + goBack: (...args: unknown[]) => mockGoBack(...args), }, })); From 33bc56f8b7e669156aa0a8ff01799d7c9f020169 Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Fri, 20 Mar 2026 09:56:48 +0000 Subject: [PATCH 12/13] Fix: Navigation mock in SignInModalTest to match codebase patterns Remove __esModule/default wrapper from Navigation mock (matching all other test files) and use block bodies to avoid returning any values that trigger no-unsafe-return ESLint errors. Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index e77fd207b998..717756f32b6a 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -29,12 +29,12 @@ jest.mock('@libs/Network/SequentialQueue', () => ({ })); jest.mock('@libs/Navigation/Navigation', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention -- __esModule is required by Jest to properly mock ES modules with default exports - __esModule: true, - default: { - dismissModal: (...args: unknown[]) => mockDismissModal(...args), - isNavigationReady: () => mockIsNavigationReady(), - goBack: (...args: unknown[]) => mockGoBack(...args), + dismissModal: (...args: unknown[]) => { + mockDismissModal(...args); + }, + isNavigationReady: () => mockIsNavigationReady(), + goBack: (...args: unknown[]) => { + mockGoBack(...args); }, })); From 644099d43deb40d08c055a1bb7fa093e67abeca5 Mon Sep 17 00:00:00 2001 From: "Eric Han (via MelvinBot)" Date: Tue, 24 Mar 2026 11:34:18 +0000 Subject: [PATCH 13/13] Remove unnecessary eslint-disable comments from SignInModalTest - Remove file-level no-non-null-assertion disable by initializing deferred promise resolvers with no-op defaults - Remove no-inline-named-export disable that was unnecessary Co-authored-by: Eric Han --- tests/unit/SignInModalTest.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/unit/SignInModalTest.tsx b/tests/unit/SignInModalTest.tsx index 717756f32b6a..766d60f13d32 100644 --- a/tests/unit/SignInModalTest.tsx +++ b/tests/unit/SignInModalTest.tsx @@ -1,10 +1,8 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import {render} from '@testing-library/react-native'; import React from 'react'; import type {View as RNView} from 'react-native'; import Onyx from 'react-native-onyx'; import {openApp} from '@libs/actions/App'; -// eslint-disable-next-line rulesdir/no-inline-named-export import SignInModal from '@pages/signin/SignInModal'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -129,7 +127,7 @@ describe('SignInModal', () => { const callOrder: string[] = []; // Make openApp chain take time to resolve - let resolveOpenApp: () => void; + let resolveOpenApp = () => {}; const openAppPromise = new Promise((resolve) => { resolveOpenApp = resolve; }); @@ -139,7 +137,7 @@ describe('SignInModal', () => { }); // Make isNavigationReady take time to resolve - let resolveNavReady: () => void; + let resolveNavReady = () => {}; const navReadyPromise = new Promise((resolve) => { resolveNavReady = resolve; }); @@ -164,12 +162,12 @@ describe('SignInModal', () => { expect(mockDismissModal).not.toHaveBeenCalled(); // Resolve isNavigationReady first - dismissModal should still wait for openApp - resolveNavReady!(); + resolveNavReady(); await waitForBatchedUpdatesWithAct(); expect(mockDismissModal).not.toHaveBeenCalled(); // Now resolve openApp - dismissModal should be called after both are done - resolveOpenApp!(); + resolveOpenApp(); await waitForBatchedUpdatesWithAct(); expect(mockDismissModal).toHaveBeenCalledTimes(1);