From 4052f9a4db738cce4afbe8573a33f838db2afe59 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 2 Oct 2024 13:06:17 +0200 Subject: [PATCH] Use SearchRouter instead of Chat finder and migrate e2e tests --- src/CONST.ts | 2 +- src/ROUTES.ts | 1 - .../Search/SearchRouter/SearchButton.tsx | 12 +- src/libs/E2E/reactNativeLaunchingTest.ts | 2 + ...est.e2e.ts => openSearchRouterTest.e2e.ts} | 17 +- .../Navigation/AppNavigator/AuthScreens.tsx | 10 +- .../createCustomBottomTabNavigator/TopBar.tsx | 33 +-- src/libs/Navigation/linkingConfig/config.ts | 1 - src/libs/Navigation/types.ts | 1 - src/pages/Search/SearchPageBottomTab.tsx | 7 +- .../SidebarScreen/BaseSidebarScreen.tsx | 5 + tests/e2e/config.ts | 4 + tests/perf-test/ChatFinderPage.perf-test.tsx | 217 ++++++++++++++++++ 13 files changed, 259 insertions(+), 53 deletions(-) rename src/libs/E2E/tests/{openChatFinderPageTest.e2e.ts => openSearchRouterTest.e2e.ts} (79%) create mode 100644 tests/perf-test/ChatFinderPage.perf-test.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 9a6bf21db303..0b130ae7564b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1084,7 +1084,7 @@ const CONST = { }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', - CHAT_FINDER_RENDER: 'search_render', + SEARCH_ROUTER_OPEN: 'search_router_render', CHAT_RENDER: 'chat_render', OPEN_REPORT: 'open_report', HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9c429dd3e909..d7e6b37a60fd 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -74,7 +74,6 @@ const ROUTES = { route: 'flag/:reportID/:reportActionID', getRoute: (reportID: string, reportActionID: string, backTo?: string) => getUrlWithBackToParam(`flag/${reportID}/${reportActionID}` as const, backTo), }, - CHAT_FINDER: 'chat-finder', PROFILE: { route: 'a/:accountID', getRoute: (accountID?: string | number, backTo?: string, login?: string) => { diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 05693ad5ea22..53660ee1f6c2 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -5,7 +5,11 @@ import {PressableWithoutFeedback} from '@components/Pressable'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Performance from '@libs/Performance'; import Permissions from '@libs/Permissions'; +import * as Session from '@userActions/Session'; +import Timing from '@userActions/Timing'; +import CONST from '@src/CONST'; import {useSearchRouterContext} from './SearchRouterContext'; function SearchButton() { @@ -22,9 +26,13 @@ function SearchButton() { { + onPress={Session.checkIfActionIsAllowed(() => { + // Todo [Search] add finishing for this timing in + Timing.start(CONST.TIMING.SEARCH_ROUTER_OPEN); + Performance.markStart(CONST.TIMING.SEARCH_ROUTER_OPEN); + openSearchRouter(); - }} + })} > ('./tests/appStartTimeTest.e2e').default, + // Todo [Search] rename + [E2EConfig.TEST_NAMES.OpenSearchRouter]: require('./tests/openSearchRouterTest.e2e').default, [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, diff --git a/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts similarity index 79% rename from src/libs/E2E/tests/openChatFinderPageTest.e2e.ts rename to src/libs/E2E/tests/openSearchRouterTest.e2e.ts index 2c2f2eda4efe..6ce1891a63d4 100644 --- a/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts +++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts @@ -3,14 +3,12 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; -import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; const test = () => { // check for login (if already logged in the action will simply resolve) - console.debug('[E2E] Logging in for chat finder'); + console.debug('[E2E] Logging in for new search router'); E2ELogin().then((neededLogin: boolean): Promise | undefined => { if (neededLogin) { @@ -20,7 +18,7 @@ const test = () => { ); } - console.debug('[E2E] Logged in, getting chat finder metrics and submitting them…'); + console.debug('[E2E] Logged in, getting search router metrics and submitting them…'); const [openSearchPagePromise, openSearchPageResolve] = getPromiseWithResolve(); const [loadSearchOptionsPromise, loadSearchOptionsResolve] = getPromiseWithResolve(); @@ -32,19 +30,12 @@ const test = () => { }); Performance.subscribeToMeasurements((entry) => { - if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { - console.debug(`[E2E] Sidebar loaded, navigating to chat finder route…`); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - return; - } - console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`); - if (entry.name === CONST.TIMING.CHAT_FINDER_RENDER) { + if (entry.name === CONST.TIMING.SEARCH_ROUTER_OPEN) { E2EClient.submitTestResults({ branch: Config.E2E_BRANCH, - name: 'Open Chat Finder Page TTI', + name: 'Open Search Router TTI', metric: entry.duration, unit: 'ms', }) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index f5f35fd21025..62be199ee183 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -8,6 +8,7 @@ import ComposeProviders from '@components/ComposeProviders'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import {SearchContextProvider} from '@components/Search/SearchContext'; import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; +import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useOnboardingFlowRouter from '@hooks/useOnboardingFlow'; import usePermissions from '@hooks/usePermissions'; @@ -233,6 +234,8 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const screenOptions = getRootNavigatorScreenOptions(shouldUseNarrowLayout, styles, StyleUtils); const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); + const {openSearchRouter} = useSearchRouterContext(); + const onboardingModalScreenOptions = useMemo(() => screenOptions.onboardingModalNavigator(onboardingIsMediumOrLargerScreenWidth), [screenOptions, onboardingIsMediumOrLargerScreenWidth]); const onboardingScreenOptions = useMemo( () => getOnboardingModalScreenOptions(shouldUseNarrowLayout, styles, StyleUtils, onboardingIsMediumOrLargerScreenWidth), @@ -241,6 +244,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const modal = useRef({}); const [didPusherInit, setDidPusherInit] = useState(false); const {isOnboardingCompleted} = useOnboardingFlowRouter(); + let initialReportID: string | undefined; const isInitialRender = useRef(true); if (isInitialRender.current) { @@ -363,13 +367,15 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie ); // Listen for the key K being pressed so that focus can be given to - // the chat switcher, or new group chat + // Search Router, or new group chat // based on the key modifiers pressed and the operating system const unsubscribeSearchShortcut = KeyboardShortcut.subscribe( searchShortcutConfig.shortcutKey, () => { Modal.close( - Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.CHAT_FINDER)), + Session.checkIfActionIsAllowed(() => { + openSearchRouter(); + }), true, true, ); diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx index 4684eb9637be..8967486165f8 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -2,32 +2,25 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Breadcrumbs from '@components/Breadcrumbs'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import SearchButton from '@components/Search/SearchRouter/SearchButton'; import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import Performance from '@libs/Performance'; import * as SearchUtils from '@libs/SearchUtils'; import SignInButton from '@pages/home/sidebar/SignInButton'; import * as Session from '@userActions/Session'; -import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; isCustomSearchQuery?: boolean; shouldDisplaySearchRouter?: boolean}; +type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; shouldDisplayCancelSearch?: boolean}; -function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, isCustomSearchQuery = false, shouldDisplaySearchRouter = false}: TopBarProps) { +function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false}: TopBarProps) { const styles = useThemeStyles(); - const theme = useTheme(); const {translate} = useLocalize(); const policy = usePolicy(activeWorkspaceID); const [session] = useOnyx(ONYXKEYS.SESSION, {selector: (sessionValue) => sessionValue && {authTokenType: sessionValue.authTokenType}}); @@ -63,7 +56,7 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, {displaySignIn && } - {isCustomSearchQuery && ( + {shouldDisplayCancelSearch && ( {translate('common.cancel')} )} - {shouldDisplaySearchRouter && } - {displaySearch && ( - - { - Timing.start(CONST.TIMING.CHAT_FINDER_RENDER); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - })} - > - - - - )} + {displaySearch && } ); diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 114e89ff2bf5..980b28c3d635 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -76,7 +76,6 @@ const config: LinkingOptions['config'] = { [SCREENS.NOT_FOUND]: '*', [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: { screens: { - [SCREENS.LEFT_MODAL.CHAT_FINDER]: ROUTES.CHAT_FINDER, [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: { path: ROUTES.WORKSPACE_SWITCHER, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b698681966e2..ecd2895053f1 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1236,7 +1236,6 @@ type TransactionDuplicateNavigatorParamList = { }; type LeftModalNavigatorParamList = { - [SCREENS.LEFT_MODAL.CHAT_FINDER]: undefined; [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: undefined; }; diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 38e4c5166884..b4fb2aac61ff 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -85,6 +85,8 @@ function SearchPageBottomTab() { ); } + const shouldDisplayCancelSearch = shouldUseNarrowLayout && !SearchUtils.isCannedSearchQuery(queryJSON); + return ( diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index edc8dfb3cb3a..e77f2000b85f 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -4,6 +4,7 @@ import {useOnyx} from 'react-native-onyx'; import ScreenWrapper from '@components/ScreenWrapper'; import useActiveWorkspaceFromNavigationState from '@hooks/useActiveWorkspaceFromNavigationState'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateLastAccessedWorkspace} from '@libs/actions/Policy/Policy'; import * as Browser from '@libs/Browser'; @@ -27,6 +28,7 @@ function BaseSidebarScreen() { const styles = useThemeStyles(); const activeWorkspaceID = useActiveWorkspaceFromNavigationState(); const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const [activeWorkspace] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID ?? -1}`); useEffect(() => { @@ -43,6 +45,8 @@ function BaseSidebarScreen() { updateLastAccessedWorkspace(undefined); }, [activeWorkspace, activeWorkspaceID]); + const shouldDisplaySearch = shouldUseNarrowLayout; + return ( + jest.fn((fn: Record) => { + // eslint-disable-next-line no-param-reassign + fn.cancel = jest.fn(); + return fn; + }), +); + +jest.mock('@src/libs/Log'); + +jest.mock('@src/libs/API', () => ({ + write: jest.fn(), + makeRequestWithSideEffects: jest.fn(), + read: jest.fn(), +})); + +jest.mock('@src/libs/Navigation/Navigation', () => ({ + dismissModalWithReport: jest.fn(), + getTopmostReportId: jest.fn(), + isNavigationReady: jest.fn(() => Promise.resolve()), + isDisplayedInModal: jest.fn(() => false), +})); + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useFocusEffect: jest.fn(), + useIsFocused: () => true, + useRoute: () => jest.fn(), + useNavigation: () => ({ + navigate: jest.fn(), + addListener: () => jest.fn(), + }), + createNavigationContainerRef: () => ({ + addListener: () => jest.fn(), + removeListener: () => jest.fn(), + isReady: () => jest.fn(), + getCurrentRoute: () => jest.fn(), + getState: () => jest.fn(), + }), + }; +}); + +jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType) => { + function WithNavigationFocus(props: WithNavigationFocusProps) { + return ( + + ); + } + + WithNavigationFocus.displayName = 'WithNavigationFocus'; + + return WithNavigationFocus; +}); +// mock of useDismissedReferralBanners +jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: jest.fn(() => ({ + isDismissed: false, + setAsDismissed: () => {}, + })), +})); + +const getMockedReports = (length = 100) => + createCollection( + (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, + (index) => createRandomReport(index), + length, + ); + +const getMockedPersonalDetails = (length = 100) => + createCollection( + (item) => item.accountID, + (index) => createPersonalDetails(index), + length, + ); + +const mockedReports = getMockedReports(600); +const mockedBetas = Object.values(CONST.BETAS); +const mockedPersonalDetails = getMockedPersonalDetails(100); +const mockedOptions = createOptionList(mockedPersonalDetails, mockedReports); + +beforeAll(() => + Onyx.init({ + keys: ONYXKEYS, + safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT], + }), +); + +// Initialize the network key for OfflineWithFeedback +beforeEach(() => { + global.fetch = TestHelper.getGlobalFetchMock(); + wrapOnyxWithWaitForBatchedUpdates(Onyx); + Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); +}); + +// Clear out Onyx after each test so that each test starts with a clean state +afterEach(() => { + Onyx.clear(); +}); + +function ChatFinderPageWrapper(args: any) { + return ( + + + + {/* */} + + + ); +} + +function ChatFinderPageWithCachedOptions(args: any) { + return ( + + ({options: mockedOptions, initializeOptions: () => {}, areOptionsInitialized: true}), [])}> + {/* */} + + + ); +} + +test('[ChatFinderPage] should render list with cached options', async () => { + const {addListener} = createAddListenerMock(); + + const scenario = async () => { + await screen.findByTestId('ChatFinderPage'); // Todo [Search] fix testID no longer existing + }; + + // const navigation = {addListener} as unknown as StackNavigationProp; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + .then(() => measureRenders(, {scenario})); +}); + +test('[ChatFinderPage] should interact when text input changes', async () => { + const {addListener} = createAddListenerMock(); + + const scenario = async () => { + await screen.findByTestId('ChatFinderPage'); + + const input = screen.getByTestId('selection-list-text-input'); + fireEvent.changeText(input, 'Email Four'); + fireEvent.changeText(input, 'Report'); + fireEvent.changeText(input, 'Email Five'); + }; + + // const navigation = {addListener} as unknown as StackNavigationProp; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + .then(() => + measureRenders( + , + {scenario}, + ), + ); +});