diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c29f9a96f1ace..83db556a2b2e4 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -64,7 +64,7 @@ const ROUTES = { // eslint-disable-next-line no-restricted-syntax -- Legacy route generation WORKSPACES_LIST: {route: 'workspaces', getRoute: (backTo?: string) => getUrlWithBackToParam('workspaces', backTo)}, - + SEARCH_ROUTER: 'search-router', SEARCH_ROOT: { route: 'search', getRoute: ({query, rawQuery, name}: {query: SearchQueryString; rawQuery?: SearchQueryString; name?: string}) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ec662857bd6e3..7e5ae41f124a9 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -243,6 +243,7 @@ const SCREENS = { RIGHT_MODAL: { SETTINGS: 'Settings', TWO_FACTOR_AUTH: 'TwoFactorAuth', + SEARCH_ROUTER: 'Search_Router', NEW_CHAT: 'NewChat', DETAILS: 'Details', PROFILE: 'Profile', diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index d3716f671c6e6..023666896c5c9 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -1,9 +1,7 @@ -import {findFocusedRoute} from '@react-navigation/native'; import {deepEqual} from 'fast-equals'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {TextInputProps} from 'react-native'; import {InteractionManager, View} from 'react-native'; -import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; @@ -38,57 +36,21 @@ import {getReportAction} from '@libs/ReportActionsUtils'; import {getReportOrDraftReport} from '@libs/ReportUtils'; import type {OptionData} from '@libs/ReportUtils'; import {getAutocompleteQueryWithComma, getQueryWithoutAutocompletedPart} from '@libs/SearchAutocompleteUtils'; -import {getPolicyNameWithFallback, getQueryWithUpdatedValues, sanitizeSearchValue} from '@libs/SearchQueryUtils'; +import {getQueryWithUpdatedValues, sanitizeSearchValue} from '@libs/SearchQueryUtils'; import StringUtils from '@libs/StringUtils'; import Navigation from '@navigation/Navigation'; -import type {ReportsSplitNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; import {navigateToAndOpenReport, searchInServer} from '@userActions/Report'; import {setSearchContext} from '@userActions/Search'; import CONST, {CONTINUATION_DETECTION_SEARCH_FILTER_KEYS} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; import type Report from '@src/types/onyx/Report'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import type {SubstitutionMap} from './getQueryWithSubstitutions'; import {getQueryWithSubstitutions} from './getQueryWithSubstitutions'; import {getUpdatedSubstitutionsMap} from './getUpdatedSubstitutionsMap'; - -function getContextualSearchAutocompleteKey(item: SearchQueryItem, policies: OnyxCollection, reports?: OnyxCollection) { - if (item.roomType === CONST.SEARCH.DATA_TYPES.INVOICE) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.TO}:${item.searchQuery}`; - } - if (item.roomType === CONST.SEARCH.DATA_TYPES.CHAT) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.IN}:${item.searchQuery}`; - } - if (item.roomType === CONST.SEARCH.DATA_TYPES.EXPENSE) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${item.policyID ? getPolicyNameWithFallback(item.policyID, policies, reports) : ''}`; - } -} - -function getContextualSearchQuery(item: SearchQueryItem, policies: OnyxCollection, reports?: OnyxCollection) { - const baseQuery = `${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TYPE}:${item.roomType}`; - let additionalQuery = ''; - - switch (item.roomType) { - case CONST.SEARCH.DATA_TYPES.EXPENSE: - additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID}:${sanitizeSearchValue(item.policyID ? getPolicyNameWithFallback(item.policyID, policies, reports) : '')}`; - break; - case CONST.SEARCH.DATA_TYPES.INVOICE: - additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID}:${item.policyID}`; - if (item.autocompleteID) { - additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO}:${sanitizeSearchValue(item.searchQuery ?? '')}`; - } - break; - case CONST.SEARCH.DATA_TYPES.CHAT: - default: - additionalQuery = ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN}:${sanitizeSearchValue(item.searchQuery ?? '')}`; - break; - } - return baseQuery + additionalQuery; -} +import {getContextualReportData, getContextualSearchAutocompleteKey, getContextualSearchQuery} from './SearchRouterUtils'; type SearchRouterProps = { onRouterClose: () => void; @@ -127,18 +89,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const [autocompleteSubstitutions, setAutocompleteSubstitutions] = useState({}); const textInputRef = useRef(null); - const contextualReportID = useRootNavigationState((state) => { - // Safe handling when navigation is not yet initialized - if (!state) { - return undefined; - } - - const focusedRoute = findFocusedRoute(state); - if (focusedRoute?.name === SCREENS.REPORT || focusedRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { - // We're guaranteed that the type of params is of SCREENS.REPORT - return (focusedRoute.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]).reportID; - } - }); + const {contextualReportID, isSearchRouterScreen} = useRootNavigationState(getContextualReportData); const getAdditionalSections: GetAdditionalSectionsCallback = useCallback( ({recentReports}) => { @@ -151,7 +102,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla return undefined; } - if (!isSearchRouterDisplayed) { + if (!isSearchRouterDisplayed && !isSearchRouterScreen) { return undefined; } let reportForContextualSearch = recentReports.find((option) => option.reportID === contextualReportID); @@ -215,7 +166,18 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla }, ]; }, - [contextualReportID, textInputValue, isSearchRouterDisplayed, translate, expensifyIcons.MagnifyingGlass, styles.activeComponentBG, reports, personalDetails, currentUserAccountID], + [ + contextualReportID, + textInputValue, + isSearchRouterDisplayed, + isSearchRouterScreen, + translate, + expensifyIcons.MagnifyingGlass, + styles.activeComponentBG, + reports, + personalDetails, + currentUserAccountID, + ], ); const searchQueryItem = textInputValue diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index 58ca77133a7c4..56a60c8fb59af 100644 --- a/src/components/Search/SearchRouter/SearchRouterContext.tsx +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -8,6 +8,7 @@ import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import {closeSearch, openSearch} from './toggleSearch'; type SearchRouterStateContextType = { isSearchRouterDisplayed: boolean; @@ -44,7 +45,6 @@ function SearchRouterContextProvider({children}: ChildrenProps) { const [isSearchRouterDisplayed, setIsSearchRouterDisplayed] = useState(false); const searchRouterDisplayedRef = useRef(false); const searchPageInputRef = useRef(undefined); - useEffect(() => { if (!canListenPopState) { return; @@ -79,15 +79,16 @@ function SearchRouterContextProvider({children}: ChildrenProps) { } close( () => { - setIsSearchRouterDisplayed(true); + openSearch(setIsSearchRouterDisplayed); searchRouterDisplayedRef.current = true; }, false, true, ); }; + const closeSearchRouter = () => { - setIsSearchRouterDisplayed(false); + closeSearch(setIsSearchRouterDisplayed); searchRouterDisplayedRef.current = false; if (isBrowserWithHistory) { const state = window.history.state as HistoryState | null; diff --git a/src/components/Search/SearchRouter/SearchRouterModal/index.native.tsx b/src/components/Search/SearchRouter/SearchRouterModal/index.native.tsx new file mode 100644 index 0000000000000..2654b32fcd25b --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouterModal/index.native.tsx @@ -0,0 +1,8 @@ +/** + * On native devices SearchRouter is served from SearchRouterPage, on web from SearchRouterModal. + */ +function SearchRouterModal() { + return null; +} + +export default SearchRouterModal; diff --git a/src/components/Search/SearchRouter/SearchRouterModal.tsx b/src/components/Search/SearchRouter/SearchRouterModal/index.tsx similarity index 93% rename from src/components/Search/SearchRouter/SearchRouterModal.tsx rename to src/components/Search/SearchRouter/SearchRouterModal/index.tsx index d456e095e401d..bde79df92d104 100644 --- a/src/components/Search/SearchRouter/SearchRouterModal.tsx +++ b/src/components/Search/SearchRouter/SearchRouterModal/index.tsx @@ -3,12 +3,12 @@ import {Dimensions} from 'react-native'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import Modal from '@components/Modal'; import ScreenWrapperContainer from '@components/ScreenWrapper/ScreenWrapperContainer'; +import SearchRouter from '@components/Search/SearchRouter/SearchRouter'; +import {useSearchRouterActions, useSearchRouterState} from '@components/Search/SearchRouter/SearchRouterContext'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; import {isMobileIOS} from '@libs/Browser'; import CONST from '@src/CONST'; -import SearchRouter from './SearchRouter'; -import {useSearchRouterActions, useSearchRouterState} from './SearchRouterContext'; const isMobileWebIOS = isMobileIOS(); diff --git a/src/components/Search/SearchRouter/SearchRouterPage/index.native.tsx b/src/components/Search/SearchRouter/SearchRouterPage/index.native.tsx new file mode 100644 index 0000000000000..4be5dc0269e8e --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouterPage/index.native.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SearchRouter from '@components/Search/SearchRouter/SearchRouter'; +import {useSearchRouterActions, useSearchRouterState} from '@components/Search/SearchRouter/SearchRouterContext'; + +function SearchRouterPage() { + const {closeSearchRouter} = useSearchRouterActions(); + const {isSearchRouterDisplayed} = useSearchRouterState(); + + return ( + + + + ); +} + +export default SearchRouterPage; diff --git a/src/components/Search/SearchRouter/SearchRouterPage/index.tsx b/src/components/Search/SearchRouter/SearchRouterPage/index.tsx new file mode 100644 index 0000000000000..ca82a5518ac6e --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouterPage/index.tsx @@ -0,0 +1,11 @@ +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; + +/** + * On native devices SearchRouter is served from SearchRouterPage, on web from SearchRouterModal. + */ +function SearchRouterPage() { + return Navigation.navigate(ROUTES.HOME); +} + +export default SearchRouterPage; diff --git a/src/components/Search/SearchRouter/SearchRouterUtils.ts b/src/components/Search/SearchRouter/SearchRouterUtils.ts new file mode 100644 index 0000000000000..ec4a8fe2157f6 --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouterUtils.ts @@ -0,0 +1,89 @@ +import {findFocusedRoute} from '@react-navigation/native'; +import type {NavigationState} from '@react-navigation/routers'; +import type {OnyxCollection} from 'react-native-onyx'; +import type {SearchQueryItem} from '@components/SelectionListWithSections/Search/SearchQueryListItem'; +import {getPolicyNameWithFallback, sanitizeSearchValue} from '@libs/SearchQueryUtils'; +import type {ReportsSplitNavigatorParamList} from '@navigation/types'; +import CONST from '@src/CONST'; +import SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; + +type ContextualReportData = { + contextualReportID: string | undefined; + isSearchRouterScreen: boolean; +}; + +/** + * Extracts contextual report data from the navigation state. + * + * This function determines: + * 1. Whether the current screen is the SearchRouter modal + * 2. The reportID of the contextual report (if any) that was focused before opening the SearchRouter + * + * When the SearchRouter is open, it looks at the previous route in the stack to find the underlying + * report context. Otherwise it looks @ current screen for report context. + * This allows the search to provide contextual suggestions based on the report + * the user was viewing when they opened the search. + * + * @param state - The root navigation state from useRootNavigationState hook + * @returns Object containing contextualReportID (the report ID if on a report screen) and isSearchRouterScreen (whether SearchRouter is focused) + */ +function getContextualReportData(state: NavigationState | undefined): ContextualReportData { + // Safe handling when navigation is not yet initialized + if (!state) { + return {contextualReportID: undefined, isSearchRouterScreen: false}; + } + let maybeReportRoute = findFocusedRoute(state); + const isSearchRouterScreen = maybeReportRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_ROUTER; + if (isSearchRouterScreen) { + const stateWithoutLastRoute = { + ...state, + routes: state.routes.slice(0, -1), + index: state.index !== 0 ? state.index - 1 : 0, + }; + maybeReportRoute = findFocusedRoute(stateWithoutLastRoute); + } + + if (maybeReportRoute?.name === SCREENS.REPORT || maybeReportRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { + // We're guaranteed that the type of params is of SCREENS.REPORT + return {contextualReportID: (maybeReportRoute?.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]).reportID, isSearchRouterScreen}; + } + return {contextualReportID: undefined, isSearchRouterScreen}; +} + +function getContextualSearchAutocompleteKey(item: SearchQueryItem, policies: OnyxCollection, reports?: OnyxCollection) { + if (item.roomType === CONST.SEARCH.DATA_TYPES.INVOICE) { + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.TO}:${item.searchQuery}`; + } + if (item.roomType === CONST.SEARCH.DATA_TYPES.CHAT) { + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.IN}:${item.searchQuery}`; + } + if (item.roomType === CONST.SEARCH.DATA_TYPES.EXPENSE) { + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${item.policyID ? getPolicyNameWithFallback(item.policyID, policies, reports) : ''}`; + } +} + +function getContextualSearchQuery(item: SearchQueryItem, policies: OnyxCollection, reports?: OnyxCollection) { + const baseQuery = `${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TYPE}:${item.roomType}`; + let additionalQuery = ''; + + switch (item.roomType) { + case CONST.SEARCH.DATA_TYPES.EXPENSE: + additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID}:${sanitizeSearchValue(item.policyID ? getPolicyNameWithFallback(item.policyID, policies, reports) : '')}`; + break; + case CONST.SEARCH.DATA_TYPES.INVOICE: + additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID}:${item.policyID}`; + if (item.autocompleteID) { + additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO}:${sanitizeSearchValue(item.searchQuery ?? '')}`; + } + break; + case CONST.SEARCH.DATA_TYPES.CHAT: + default: + additionalQuery = ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN}:${sanitizeSearchValue(item.searchQuery ?? '')}`; + break; + } + return baseQuery + additionalQuery; +} + +export {getContextualReportData, getContextualSearchAutocompleteKey, getContextualSearchQuery}; +export type {ContextualReportData}; diff --git a/src/components/Search/SearchRouter/toggleSearch/index.native.tsx b/src/components/Search/SearchRouter/toggleSearch/index.native.tsx new file mode 100644 index 0000000000000..0b3883baaf0e1 --- /dev/null +++ b/src/components/Search/SearchRouter/toggleSearch/index.native.tsx @@ -0,0 +1,12 @@ +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; + +function openSearch() { + return Navigation.navigate(ROUTES.SEARCH_ROUTER); +} + +function closeSearch() { + return Navigation.dismissModal(); +} + +export {openSearch, closeSearch}; diff --git a/src/components/Search/SearchRouter/toggleSearch/index.tsx b/src/components/Search/SearchRouter/toggleSearch/index.tsx new file mode 100644 index 0000000000000..766f94d826001 --- /dev/null +++ b/src/components/Search/SearchRouter/toggleSearch/index.tsx @@ -0,0 +1,9 @@ +function openSearch(setSearchState: React.Dispatch>) { + return setSearchState(true); +} + +function closeSearch(setSearchState: React.Dispatch>) { + return setSearchState(false); +} + +export {openSearch, closeSearch}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c2dac3cdd6f21..46fb0e77d6715 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -853,6 +853,10 @@ const TwoFactorAuthenticatorStackNavigator = createModalStackNavigator require('../../../../pages/settings/Security/TwoFactorAuth/SuccessPage').default, }); +const SearchRouterModalStackNavigator = createModalStackNavigator({ + [SCREENS.RIGHT_MODAL.SEARCH_ROUTER]: () => require('../../../../components/Search/SearchRouter/SearchRouterPage').default, +}); + const EnablePaymentsStackNavigator = createModalStackNavigator({ [SCREENS.ENABLE_PAYMENTS_ROOT]: () => require('../../../../pages/EnablePayments/EnablePaymentsPage').default, }); @@ -1068,4 +1072,5 @@ export { WorkspaceDuplicateModalStackNavigator, WorkspacesDomainModalStackNavigator, MultifactorAuthenticationStackNavigator, + SearchRouterModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 0b1bf503ee637..ab6222ef13c48 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -185,6 +185,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { screenListeners={screenListeners} id={NAVIGATORS.RIGHT_MODAL_NAVIGATOR} > + ['config'] = { }, [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: { screens: { + [SCREENS.RIGHT_MODAL.SEARCH_ROUTER]: { + path: ROUTES.SEARCH_ROUTER, + exact: true, + }, [SCREENS.RIGHT_MODAL.SETTINGS]: { screens: { [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e960e738f4df8..1cee192f433b9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2283,6 +2283,7 @@ type WorkspacesDomainModalNavigatorParamList = { type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.SETTINGS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.SEARCH_ROUTER]: undefined; [SCREENS.RIGHT_MODAL.NEW_CHAT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.DETAILS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams; diff --git a/tests/perf-test/SearchRouter.perf-test.tsx b/tests/perf-test/SearchRouter.perf-test.tsx index 24a75dcff3002..b93e7b324d9a4 100644 --- a/tests/perf-test/SearchRouter.perf-test.tsx +++ b/tests/perf-test/SearchRouter.perf-test.tsx @@ -43,7 +43,11 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ isDisplayedInModal: jest.fn(() => false), })); -jest.mock('@src/hooks/useRootNavigationState'); +jest.mock('@src/hooks/useRootNavigationState', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: () => ({contextualReportID: undefined, isSearchRouterScreen: false}), +})); jest.mock('@react-navigation/native', () => { const actualNav = jest.requireActual('@react-navigation/native'); diff --git a/tests/unit/SearchRouterUtilsTest.ts b/tests/unit/SearchRouterUtilsTest.ts new file mode 100644 index 0000000000000..0cad24158b0f0 --- /dev/null +++ b/tests/unit/SearchRouterUtilsTest.ts @@ -0,0 +1,175 @@ +import type {NavigationState} from '@react-navigation/native'; +import {getContextualReportData} from '@components/Search/SearchRouter/SearchRouterUtils'; +import SCREENS from '@src/SCREENS'; + +// Helper to create minimal navigation state for testing +// The function only uses index, routes, and nested state properties +function createMockState(partialState: {index: number; routes: Array<{name: string; params?: Record; state?: unknown}>}): NavigationState { + return partialState as NavigationState; +} + +describe('SearchRouterUtils', () => { + describe('getContextualReportData', () => { + it('returns undefined contextualReportID and false isSearchRouterScreen when state is undefined', () => { + const result = getContextualReportData(undefined); + + expect(result).toEqual({ + contextualReportID: undefined, + isSearchRouterScreen: false, + }); + }); + + it('returns reportID when focused on a Report screen', () => { + const state = createMockState({ + index: 0, + routes: [ + { + name: SCREENS.REPORT, + params: {reportID: '12345'}, + }, + ], + }); + + const result = getContextualReportData(state); + + expect(result).toEqual({ + contextualReportID: '12345', + isSearchRouterScreen: false, + }); + }); + + it('returns reportID when focused on ExpenseReport screen', () => { + const state = createMockState({ + index: 0, + routes: [ + { + name: SCREENS.RIGHT_MODAL.EXPENSE_REPORT, + params: {reportID: '67890'}, + }, + ], + }); + + const result = getContextualReportData(state); + + expect(result).toEqual({ + contextualReportID: '67890', + isSearchRouterScreen: false, + }); + }); + + it('returns isSearchRouterScreen true and extracts reportID from previous route when SearchRouter is open over a Report', () => { + const state = createMockState({ + index: 1, + routes: [ + { + name: SCREENS.REPORT, + params: {reportID: '11111'}, + }, + { + name: SCREENS.RIGHT_MODAL.SEARCH_ROUTER, + params: {}, + }, + ], + }); + + const result = getContextualReportData(state); + + expect(result).toEqual({ + contextualReportID: '11111', + isSearchRouterScreen: true, + }); + }); + + it('returns undefined contextualReportID when SearchRouter is open but no report underneath', () => { + const state = createMockState({ + index: 1, + routes: [ + { + name: SCREENS.HOME, + params: {}, + }, + { + name: SCREENS.RIGHT_MODAL.SEARCH_ROUTER, + params: {}, + }, + ], + }); + + const result = getContextualReportData(state); + + expect(result).toEqual({ + contextualReportID: undefined, + isSearchRouterScreen: true, + }); + }); + + it('returns undefined contextualReportID when on a non-report screen', () => { + const state = createMockState({ + index: 0, + routes: [ + { + name: SCREENS.HOME, + params: {}, + }, + ], + }); + + const result = getContextualReportData(state); + + expect(result).toEqual({ + contextualReportID: undefined, + isSearchRouterScreen: false, + }); + }); + + it('handles nested navigation state with Report screen', () => { + const state = createMockState({ + index: 0, + routes: [ + { + name: 'RootNavigator', + state: { + index: 0, + routes: [ + { + name: SCREENS.REPORT, + params: {reportID: '55555'}, + }, + ], + }, + }, + ], + }); + + const result = getContextualReportData(state); + + expect(result).toEqual({ + contextualReportID: '55555', + isSearchRouterScreen: false, + }); + }); + + it('extracts reportID from ExpenseReport when SearchRouter is open over it', () => { + const state = createMockState({ + index: 1, + routes: [ + { + name: SCREENS.RIGHT_MODAL.EXPENSE_REPORT, + params: {reportID: '99999'}, + }, + { + name: SCREENS.RIGHT_MODAL.SEARCH_ROUTER, + params: {}, + }, + ], + }); + + const result = getContextualReportData(state); + + expect(result).toEqual({ + contextualReportID: '99999', + isSearchRouterScreen: true, + }); + }); + }); +});