From b3ed0c4afbe927d19ca21b09c9671bffa2b77839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 4 Aug 2026 17:49:35 -0600 Subject: [PATCH 1/4] Fix: land onboarding Track-business users on Home instead of Inbox The guard at the onboarding call sites used isReportTopmostSplitNavigator(), which only checks whether the Reports tab is topmost, not whether it actually shows a report. When a fresh onboarding user's base tab happened to be the empty Inbox, the guard treated that as a report worth preserving and skipped Navigation.navigate(ROUTES.HOME), leaving the user stranded on Inbox. Adds isReportRevealedInTopmostSplitNavigator() in src/libs/Navigation/helpers/, which also requires a SCREENS.REPORT route in the split's inner routes before treating it as revealed. Replaces the guard at both call sites in RHPVariantTest/index.ts and navigateAfterOnboarding.ts. isReportTopmostSplitNavigator is left untouched, since IOU flows and SignInModal still call it directly for unrelated checks. Fixes https://github.com/Expensify/Expensify/issues/663126 Recreated from #97009 on a clean branch off main. #97009's branch had ingested unrelated, still-in-progress commits from a separate PR (#96508, Concierge optimistic reveal cadence) through repeated cross-branch merges, so this is the same change isolated from that entanglement rather than a new fix. --- .../SidePanel/RHPVariantTest/index.ts | 6 ++-- ...isReportRevealedInTopmostSplitNavigator.ts | 34 +++++++++++++++++++ src/libs/navigateAfterOnboarding.ts | 4 +-- .../SidePanel/RHPVariantTest.test.ts | 22 ++++++------ tests/unit/navigateAfterOnboardingTest.ts | 23 +++++++++---- 5 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts diff --git a/src/components/SidePanel/RHPVariantTest/index.ts b/src/components/SidePanel/RHPVariantTest/index.ts index a859526d94c3..61716cac73e0 100644 --- a/src/components/SidePanel/RHPVariantTest/index.ts +++ b/src/components/SidePanel/RHPVariantTest/index.ts @@ -1,5 +1,5 @@ import SidePanelActions from '@libs/actions/SidePanel'; -import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; +import isReportRevealedInTopmostSplitNavigator from '@libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; @@ -67,7 +67,7 @@ const shouldOpenRHPVariant: ShouldOpenRHPVariant = (variantOverride) => { const handleRHPVariantNavigation: HandleRHPVariantNavigation = (onboardingPolicyID, variantOverride, navigationOptions) => { const variant = variantOverride ?? onboardingRHPVariant; if (variant === CONST.ONBOARDING_RHP_VARIANT.TRACK_EXPENSES_WITH_CONCIERGE) { - const shouldPreserveRevealedReport = isReportTopmostSplitNavigator(); + const shouldPreserveRevealedReport = isReportRevealedInTopmostSplitNavigator(); if (!shouldPreserveRevealedReport) { Navigation.navigate(ROUTES.HOME, navigationOptions); } @@ -78,7 +78,7 @@ const handleRHPVariantNavigation: HandleRHPVariantNavigation = (onboardingPolicy const isRHPHomePage = variant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE; if (isRHPHomePage) { - const shouldPreserveRevealedReport = isReportTopmostSplitNavigator(); + const shouldPreserveRevealedReport = isReportRevealedInTopmostSplitNavigator(); if (!shouldPreserveRevealedReport) { Navigation.navigate(ROUTES.HOME, navigationOptions); } diff --git a/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts b/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts new file mode 100644 index 000000000000..5d1577c181d1 --- /dev/null +++ b/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts @@ -0,0 +1,34 @@ +import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState'; + +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +import getTopmostFullScreenRoute from './getTopmostFullScreenRoute'; + +/** + * Returns true only when a report is revealed in the topmost Reports split navigator. Returns false when + * the Reports tab is topmost but shows only the empty Inbox sidebar. + * + * The read falls back to the preserved navigator state because the split's live state can be stripped to + * preserved-only inside the onboarding microtask. Without that fallback a deep-linked report is missed and + * the user gets sent to Home. + */ +function isReportRevealedInTopmostSplitNavigator(): boolean { + const topmostFullScreenRoute = getTopmostFullScreenRoute(); + + // getTopmostFullScreenRoute applies the tab-level preserved-state fallback, so this stays correct when + // the live tab state has been stripped. + if (topmostFullScreenRoute?.name !== NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) { + return false; + } + + // Read the split's live inner routes first, then fall back to the preserved state keyed on the split + // route's key. + const innerRoutes: ReadonlyArray<{name: string}> | undefined = + topmostFullScreenRoute.state?.routes ?? (topmostFullScreenRoute.key ? getPreservedNavigatorState(topmostFullScreenRoute.key)?.routes : undefined); + + // Only a report counts as revealed. The Inbox sidebar on its own does not. + return !!innerRoutes?.some((route) => route.name === SCREENS.REPORT); +} + +export default isReportRevealedInTopmostSplitNavigator; diff --git a/src/libs/navigateAfterOnboarding.ts b/src/libs/navigateAfterOnboarding.ts index 67156c392ea6..aead79253508 100644 --- a/src/libs/navigateAfterOnboarding.ts +++ b/src/libs/navigateAfterOnboarding.ts @@ -13,7 +13,7 @@ import Onyx from 'react-native-onyx'; import {setDisableDismissOnEscape} from './actions/Modal'; import SidePanelActions from './actions/SidePanel'; import {setOnboardingRHPVariant} from './actions/Welcome'; -import isReportTopmostSplitNavigator from './Navigation/helpers/isReportTopmostSplitNavigator'; +import isReportRevealedInTopmostSplitNavigator from './Navigation/helpers/isReportRevealedInTopmostSplitNavigator'; import {dismissOnboardingModalBeforeExit} from './Navigation/helpers/OnboardingNavigationUtils'; import shouldOpenOnAdminRoom from './Navigation/helpers/shouldOpenOnAdminRoom'; import Navigation from './Navigation/Navigation'; @@ -108,7 +108,7 @@ function navigateAfterOnboarding( ); if (reportID) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID), navigationOptions); - } else if (!isReportTopmostSplitNavigator()) { + } else if (!isReportRevealedInTopmostSplitNavigator()) { // Navigate to home to trigger guard evaluation Navigation.navigate(ROUTES.HOME, navigationOptions); } diff --git a/tests/unit/components/SidePanel/RHPVariantTest.test.ts b/tests/unit/components/SidePanel/RHPVariantTest.test.ts index 4878523f3712..2669e879ff4b 100644 --- a/tests/unit/components/SidePanel/RHPVariantTest.test.ts +++ b/tests/unit/components/SidePanel/RHPVariantTest.test.ts @@ -6,7 +6,7 @@ import ROUTES from '@src/ROUTES'; import type * as RHPVariantTest from '../../../../src/components/SidePanel/RHPVariantTest/index'; -const mockIsReportTopmostSplitNavigator = jest.fn(() => false); +const mockIsReportRevealedInTopmostSplitNavigator = jest.fn(() => false); jest.mock('@expensify/react-native-hybrid-app', () => ({ __esModule: true, @@ -35,9 +35,9 @@ jest.mock('react-native-onyx', () => ({ }, })); -jest.mock('@libs/Navigation/helpers/isReportTopmostSplitNavigator', () => ({ +jest.mock('@libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator', () => ({ __esModule: true, - default: () => mockIsReportTopmostSplitNavigator(), + default: () => mockIsReportRevealedInTopmostSplitNavigator(), })); jest.mock('@libs/Navigation/Navigation', () => ({ @@ -59,11 +59,11 @@ const {handleRHPVariantNavigation} = jest.requireActual(' describe('handleRHPVariantNavigation', () => { beforeEach(() => { jest.clearAllMocks(); - mockIsReportTopmostSplitNavigator.mockReturnValue(false); + mockIsReportRevealedInTopmostSplitNavigator.mockReturnValue(false); }); - it('preserves the topmost report for the rhpHomePage variant', () => { - mockIsReportTopmostSplitNavigator.mockReturnValue(true); + it('preserves the revealed report for the rhpHomePage variant', () => { + mockIsReportRevealedInTopmostSplitNavigator.mockReturnValue(true); handleRHPVariantNavigation('policyID', CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE); @@ -71,15 +71,15 @@ describe('handleRHPVariantNavigation', () => { expect(SidePanelActions.openSidePanel).toHaveBeenCalledWith(true); }); - it('navigates home for the rhpHomePage variant when no report is topmost', () => { + it('navigates home for the rhpHomePage variant when no report is revealed', () => { handleRHPVariantNavigation('policyID', CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE); expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.HOME, undefined); expect(SidePanelActions.openSidePanel).toHaveBeenCalledWith(true); }); - it('preserves the topmost report for the trackExpensesWithConcierge variant and opens the side panel on top of it', () => { - mockIsReportTopmostSplitNavigator.mockReturnValue(true); + it('preserves the revealed report for the trackExpensesWithConcierge variant and opens the side panel on top of it', () => { + mockIsReportRevealedInTopmostSplitNavigator.mockReturnValue(true); handleRHPVariantNavigation('policyID', CONST.ONBOARDING_RHP_VARIANT.TRACK_EXPENSES_WITH_CONCIERGE); @@ -87,7 +87,9 @@ describe('handleRHPVariantNavigation', () => { expect(SidePanelActions.openSidePanel).toHaveBeenCalledWith(true); }); - it('navigates home for the trackExpensesWithConcierge variant when no report is topmost', () => { + it('navigates home for the trackExpensesWithConcierge variant when the Inbox tab is topmost but no report is revealed', () => { + // Reproduces the reported bug. The Reports split navigator is topmost but shows only the empty Inbox + // sidebar, so onboarding must still land the user on Home. handleRHPVariantNavigation('policyID', CONST.ONBOARDING_RHP_VARIANT.TRACK_EXPENSES_WITH_CONCIERGE); expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.HOME, undefined); diff --git a/tests/unit/navigateAfterOnboardingTest.ts b/tests/unit/navigateAfterOnboardingTest.ts index 73a19c0529fe..d6ef53de8fc6 100644 --- a/tests/unit/navigateAfterOnboardingTest.ts +++ b/tests/unit/navigateAfterOnboardingTest.ts @@ -21,7 +21,7 @@ const REPORT_ID = '3'; const USER_ID = '4'; const mockFindLastAccessedReport = jest.fn, Parameters>(); const mockShouldOpenOnAdminRoom = jest.fn(); -const mockIsReportTopmostSplitNavigator = jest.fn(() => false); +const mockIsReportRevealedInTopmostSplitNavigator = jest.fn(() => false); jest.mock('@expensify/react-native-hybrid-app', () => ({ __esModule: true, @@ -74,9 +74,9 @@ jest.mock('@libs/Navigation/helpers/shouldOpenOnAdminRoom', () => ({ default: () => mockShouldOpenOnAdminRoom() as boolean, })); -jest.mock('@libs/Navigation/helpers/isReportTopmostSplitNavigator', () => ({ +jest.mock('@libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator', () => ({ __esModule: true, - default: () => mockIsReportTopmostSplitNavigator(), + default: () => mockIsReportRevealedInTopmostSplitNavigator(), })); describe('navigateAfterOnboarding', () => { @@ -88,7 +88,7 @@ describe('navigateAfterOnboarding', () => { beforeEach(async () => { jest.clearAllMocks(); - mockIsReportTopmostSplitNavigator.mockReturnValue(false); + mockIsReportRevealedInTopmostSplitNavigator.mockReturnValue(false); return Onyx.clear(); }); @@ -109,14 +109,25 @@ describe('navigateAfterOnboarding', () => { expect(navigate).toHaveBeenCalledWith(ROUTES.HOME, undefined); }); - it('should preserve the topmost report if onboardingAdminsChatReportID is not provided on larger screens', () => { + it('should preserve the revealed report if onboardingAdminsChatReportID is not provided on larger screens', () => { const navigate = jest.spyOn(Navigation, 'navigate'); - mockIsReportTopmostSplitNavigator.mockReturnValue(true); + // A report is revealed in the topmost split navigator, so the user should stay on it. + mockIsReportRevealedInTopmostSplitNavigator.mockReturnValue(true); navigateAfterOnboarding(false, true, '', {}, undefined, undefined); expect(navigate).not.toHaveBeenCalled(); }); + it('should navigate to home when the Inbox tab is topmost but no report is revealed on larger screens', () => { + const navigate = jest.spyOn(Navigation, 'navigate'); + // The Reports split navigator is topmost but shows only the empty Inbox sidebar, so the onboarding + // user must still land on Home. + mockIsReportRevealedInTopmostSplitNavigator.mockReturnValue(false); + + navigateAfterOnboarding(false, true, '', {}, undefined, undefined); + expect(navigate).toHaveBeenCalledWith(ROUTES.HOME, undefined); + }); + it('should not navigate to last accessed report if it is a concierge chat on small screens', async () => { const navigate = jest.spyOn(Navigation, 'navigate'); const lastAccessedReport = { From bb4ddf44a88e9bb998e9fd26a92978fb5acf8889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 5 Aug 2026 19:31:46 -0600 Subject: [PATCH 2/4] Polish onboarding navigation comments and tests --- .../isReportRevealedInTopmostSplitNavigator.ts | 12 +----------- .../unit/components/SidePanel/RHPVariantTest.test.ts | 2 -- tests/unit/navigateAfterOnboardingTest.ts | 3 --- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts b/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts index 5d1577c181d1..8dae72928a31 100644 --- a/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts +++ b/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts @@ -6,28 +6,18 @@ import SCREENS from '@src/SCREENS'; import getTopmostFullScreenRoute from './getTopmostFullScreenRoute'; /** - * Returns true only when a report is revealed in the topmost Reports split navigator. Returns false when - * the Reports tab is topmost but shows only the empty Inbox sidebar. - * - * The read falls back to the preserved navigator state because the split's live state can be stripped to - * preserved-only inside the onboarding microtask. Without that fallback a deep-linked report is missed and - * the user gets sent to Home. + * Onboarding can remove the live split state before this check, which would send a deep-linked report to Home. */ function isReportRevealedInTopmostSplitNavigator(): boolean { const topmostFullScreenRoute = getTopmostFullScreenRoute(); - // getTopmostFullScreenRoute applies the tab-level preserved-state fallback, so this stays correct when - // the live tab state has been stripped. if (topmostFullScreenRoute?.name !== NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) { return false; } - // Read the split's live inner routes first, then fall back to the preserved state keyed on the split - // route's key. const innerRoutes: ReadonlyArray<{name: string}> | undefined = topmostFullScreenRoute.state?.routes ?? (topmostFullScreenRoute.key ? getPreservedNavigatorState(topmostFullScreenRoute.key)?.routes : undefined); - // Only a report counts as revealed. The Inbox sidebar on its own does not. return !!innerRoutes?.some((route) => route.name === SCREENS.REPORT); } diff --git a/tests/unit/components/SidePanel/RHPVariantTest.test.ts b/tests/unit/components/SidePanel/RHPVariantTest.test.ts index 2669e879ff4b..c9091a3a0b09 100644 --- a/tests/unit/components/SidePanel/RHPVariantTest.test.ts +++ b/tests/unit/components/SidePanel/RHPVariantTest.test.ts @@ -88,8 +88,6 @@ describe('handleRHPVariantNavigation', () => { }); it('navigates home for the trackExpensesWithConcierge variant when the Inbox tab is topmost but no report is revealed', () => { - // Reproduces the reported bug. The Reports split navigator is topmost but shows only the empty Inbox - // sidebar, so onboarding must still land the user on Home. handleRHPVariantNavigation('policyID', CONST.ONBOARDING_RHP_VARIANT.TRACK_EXPENSES_WITH_CONCIERGE); expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.HOME, undefined); diff --git a/tests/unit/navigateAfterOnboardingTest.ts b/tests/unit/navigateAfterOnboardingTest.ts index d6ef53de8fc6..1f35c3d71214 100644 --- a/tests/unit/navigateAfterOnboardingTest.ts +++ b/tests/unit/navigateAfterOnboardingTest.ts @@ -111,7 +111,6 @@ describe('navigateAfterOnboarding', () => { it('should preserve the revealed report if onboardingAdminsChatReportID is not provided on larger screens', () => { const navigate = jest.spyOn(Navigation, 'navigate'); - // A report is revealed in the topmost split navigator, so the user should stay on it. mockIsReportRevealedInTopmostSplitNavigator.mockReturnValue(true); navigateAfterOnboarding(false, true, '', {}, undefined, undefined); @@ -120,8 +119,6 @@ describe('navigateAfterOnboarding', () => { it('should navigate to home when the Inbox tab is topmost but no report is revealed on larger screens', () => { const navigate = jest.spyOn(Navigation, 'navigate'); - // The Reports split navigator is topmost but shows only the empty Inbox sidebar, so the onboarding - // user must still land on Home. mockIsReportRevealedInTopmostSplitNavigator.mockReturnValue(false); navigateAfterOnboarding(false, true, '', {}, undefined, undefined); From 08b7cb12dbd71f723f971571796e9994fc6dbbc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez=20=28via=20MelvinBot=29?= Date: Sat, 8 Aug 2026 02:06:59 +0000 Subject: [PATCH 3/4] Require non-empty reportID in isReportRevealedInTopmostSplitNavigator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The helper matched on route.name === SCREENS.REPORT alone, so on a wide layout the placeholder REPORT route that SplitRouter pads the split with (carrying an empty reportID, or the Inbox route's params) was treated as a revealed report. That skipped Navigation.navigate(ROUTES.HOME) in the Track-business onboarding path, leaving the user on the empty padded central screen instead of Home. Now require a non-empty params.reportID before treating a REPORT route as revealed, using the same params guard idiom as popReportsSplitNavigatorToReport. Adds a unit test on the helper itself (both existing tests mock it, so neither exercised its route matching): asserts false for a REPORT route with no/empty reportID and true for a real reportID. Co-authored-by: Marco Chávez --- ...isReportRevealedInTopmostSplitNavigator.ts | 13 +++- ...portRevealedInTopmostSplitNavigatorTest.ts | 78 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 tests/unit/Navigation/isReportRevealedInTopmostSplitNavigatorTest.ts diff --git a/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts b/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts index 8dae72928a31..9f551cd1cd8f 100644 --- a/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts +++ b/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts @@ -15,10 +15,19 @@ function isReportRevealedInTopmostSplitNavigator(): boolean { return false; } - const innerRoutes: ReadonlyArray<{name: string}> | undefined = + const innerRoutes: ReadonlyArray<{name: string; params?: unknown}> | undefined = topmostFullScreenRoute.state?.routes ?? (topmostFullScreenRoute.key ? getPreservedNavigatorState(topmostFullScreenRoute.key)?.routes : undefined); - return !!innerRoutes?.some((route) => route.name === SCREENS.REPORT); + // A wide layout pads the split with a placeholder SCREENS.REPORT route that carries no reportID (or an empty one), + // so matching on the route name alone would treat that empty placeholder as a revealed report. Require a non-empty reportID. + return !!innerRoutes?.some( + (route) => + route.name === SCREENS.REPORT && + typeof route.params === 'object' && + route.params !== null && + 'reportID' in route.params && + !!(route.params as {reportID?: string}).reportID, + ); } export default isReportRevealedInTopmostSplitNavigator; diff --git a/tests/unit/Navigation/isReportRevealedInTopmostSplitNavigatorTest.ts b/tests/unit/Navigation/isReportRevealedInTopmostSplitNavigatorTest.ts new file mode 100644 index 000000000000..3ce4fe283c7f --- /dev/null +++ b/tests/unit/Navigation/isReportRevealedInTopmostSplitNavigatorTest.ts @@ -0,0 +1,78 @@ +import isReportRevealedInTopmostSplitNavigator from '@libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator'; + +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +const mockGetRootState = jest.fn(); + +jest.mock('@libs/Navigation/navigationRef', () => ({ + __esModule: true, + default: { + getRootState: () => mockGetRootState() as unknown, + }, +})); + +/** + * Builds a root state whose topmost TAB_NAVIGATOR focuses a REPORTS_SPLIT_NAVIGATOR containing the given inner routes. + */ +function rootStateWithReportsSplitRoutes(innerRoutes: Array<{name: string; params?: Record}>) { + return { + routes: [ + { + name: NAVIGATORS.TAB_NAVIGATOR, + state: { + index: 0, + routes: [ + { + name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, + state: {routes: innerRoutes}, + }, + ], + }, + }, + ], + }; +} + +describe('isReportRevealedInTopmostSplitNavigator', () => { + beforeEach(() => { + mockGetRootState.mockReset(); + }); + + it('returns false when there is no root state', () => { + mockGetRootState.mockReturnValue(undefined); + expect(isReportRevealedInTopmostSplitNavigator()).toBe(false); + }); + + it('returns false when the topmost full-screen route is not a REPORTS_SPLIT_NAVIGATOR', () => { + mockGetRootState.mockReturnValue({ + routes: [ + { + name: NAVIGATORS.TAB_NAVIGATOR, + state: {index: 0, routes: [{name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR}]}, + }, + ], + }); + expect(isReportRevealedInTopmostSplitNavigator()).toBe(false); + }); + + it('returns false when the split only contains the Inbox sidebar', () => { + mockGetRootState.mockReturnValue(rootStateWithReportsSplitRoutes([{name: SCREENS.INBOX}])); + expect(isReportRevealedInTopmostSplitNavigator()).toBe(false); + }); + + it('returns false for a placeholder REPORT route with no reportID param', () => { + mockGetRootState.mockReturnValue(rootStateWithReportsSplitRoutes([{name: SCREENS.INBOX}, {name: SCREENS.REPORT}])); + expect(isReportRevealedInTopmostSplitNavigator()).toBe(false); + }); + + it('returns false for a placeholder REPORT route with an empty reportID param', () => { + mockGetRootState.mockReturnValue(rootStateWithReportsSplitRoutes([{name: SCREENS.INBOX}, {name: SCREENS.REPORT, params: {reportID: ''}}])); + expect(isReportRevealedInTopmostSplitNavigator()).toBe(false); + }); + + it('returns true for a REPORT route with a non-empty reportID param', () => { + mockGetRootState.mockReturnValue(rootStateWithReportsSplitRoutes([{name: SCREENS.INBOX}, {name: SCREENS.REPORT, params: {reportID: '1234'}}])); + expect(isReportRevealedInTopmostSplitNavigator()).toBe(true); + }); +}); From 8cdafbfe498066882b362757972bfef502e2eadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Sat, 8 Aug 2026 14:05:17 -0600 Subject: [PATCH 4/4] Fix ESLint unsafe type assertion in isReportRevealedInTopmostSplitNavigator Drop the `as {reportID?: string}` cast on route.params. The preceding typeof/null/`in` narrowing chain already narrows it to `object & Record<'reportID', unknown>`, so the property is accessible directly and the cast was flagged by @typescript-eslint/no-unsafe-type-assertion. --- .../helpers/isReportRevealedInTopmostSplitNavigator.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts b/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts index 9f551cd1cd8f..58edb4266f53 100644 --- a/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts +++ b/src/libs/Navigation/helpers/isReportRevealedInTopmostSplitNavigator.ts @@ -21,12 +21,7 @@ function isReportRevealedInTopmostSplitNavigator(): boolean { // A wide layout pads the split with a placeholder SCREENS.REPORT route that carries no reportID (or an empty one), // so matching on the route name alone would treat that empty placeholder as a revealed report. Require a non-empty reportID. return !!innerRoutes?.some( - (route) => - route.name === SCREENS.REPORT && - typeof route.params === 'object' && - route.params !== null && - 'reportID' in route.params && - !!(route.params as {reportID?: string}).reportID, + (route) => route.name === SCREENS.REPORT && typeof route.params === 'object' && route.params !== null && 'reportID' in route.params && !!route.params.reportID, ); }