diff --git a/tests/perf-test/SearchPage.perf-test.tsx b/tests/perf-test/ChatFinderPage.perf-test.tsx similarity index 62% rename from tests/perf-test/SearchPage.perf-test.tsx rename to tests/perf-test/ChatFinderPage.perf-test.tsx index 6ccecf0f735b..9285b3b8b24d 100644 --- a/tests/perf-test/SearchPage.perf-test.tsx +++ b/tests/perf-test/ChatFinderPage.perf-test.tsx @@ -1,7 +1,6 @@ import type * as NativeNavigation from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import {fireEvent, screen, waitFor} from '@testing-library/react-native'; -import type {TextMatch} from '@testing-library/react-native/build/matches'; +import {fireEvent, screen} from '@testing-library/react-native'; import React, {useMemo} from 'react'; import type {ComponentType} from 'react'; import Onyx from 'react-native-onyx'; @@ -23,7 +22,6 @@ import type {Beta, PersonalDetails, Report} from '@src/types/onyx'; import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomReport from '../utils/collections/reports'; -import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; @@ -123,7 +121,6 @@ beforeEach(() => { // Clear out Onyx after each test so that each test starts with a clean state afterEach(() => { Onyx.clear(); - PusherHelper.teardown(); }); type ChatFinderPageProps = StackScreenProps & { @@ -134,12 +131,14 @@ type ChatFinderPageProps = StackScreenProps - + + + + ); } @@ -158,7 +157,7 @@ function ChatFinderPageWithCachedOptions(args: ChatFinderPageProps) { ); } -test('[Search Page] should render list with cached options', async () => { +test('[ChatFinderPage] should render list with cached options', async () => { const {addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { @@ -182,7 +181,7 @@ test('[Search Page] should render list with cached options', async () => { ); }); -test('[Search Page] should interact when text input changes', async () => { +test('[ChatFinderPage] should interact when text input changes', async () => { const {addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { @@ -210,94 +209,3 @@ test('[Search Page] should interact when text input changes', async () => { .then(() => measurePerformance(, {scenario})) ); }); - -test.skip('[Search Page] should render selection list', async () => { - const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); - const smallMockedPersonalDetails = getMockedPersonalDetails(5); - - const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); - await waitFor(triggerTransitionEnd as Awaited<() => Promise>); - await screen.findByTestId('selection-list'); - await screen.findByText(smallMockedPersonalDetails['1'].login as TextMatch); - await screen.findByText(smallMockedPersonalDetails['2'].login as TextMatch); - }; - - const navigation = {addListener}; - - return ( - waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - ...mockedReports, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: smallMockedPersonalDetails, - [ONYXKEYS.BETAS]: mockedBetas, - [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, - }), - ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - .then(() => measurePerformance(, {scenario})) - ); -}); - -test('[Search Page] should search in selection list', async () => { - const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); - - const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); - await waitFor(triggerTransitionEnd as Awaited<() => Promise>); - - const input = screen.getByTestId('selection-list-text-input'); - const searchValue = mockedPersonalDetails['88'].login; - - fireEvent.changeText(input, searchValue); - await screen.findByText(searchValue as TextMatch); - }; - - const navigation = {addListener}; - - return ( - waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - ...mockedReports, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, - [ONYXKEYS.BETAS]: mockedBetas, - [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, - }), - ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - .then(() => measurePerformance(, {scenario})) - ); -}); - -test('[Search Page] should click on list item', async () => { - const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); - - const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); - const input = screen.getByTestId('selection-list-text-input'); - await waitFor(triggerTransitionEnd as Awaited<() => Promise>); - - const searchValue = mockedPersonalDetails['4'].login as TextMatch; - fireEvent.changeText(input, searchValue); - - const optionButton = await screen.findByText(searchValue); - fireEvent.press(optionButton); - }; - - const navigation = {addListener}; - return ( - waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - ...mockedReports, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, - [ONYXKEYS.BETAS]: mockedBetas, - [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, - }), - ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - .then(() => measurePerformance(, {scenario})) - ); -}); diff --git a/tests/perf-test/README.md b/tests/perf-test/README.md index 35af9dc35b7d..2b66f7c147f3 100644 --- a/tests/perf-test/README.md +++ b/tests/perf-test/README.md @@ -12,6 +12,8 @@ We use Reassure for monitoring performance regression. It helps us check if our - The primary focus is on testing business cases rather than small, reusable parts that typically don't introduce regressions, although some tests in that area are still necessary. - To achieve this goal, it's recommended to stay relatively high up in the React tree, targeting whole screens to recreate real-life scenarios that users may encounter. - For example, consider scenarios where an additional `useMemo` call could impact performance negatively. +- Please note that high-complexity components, such as `ReportScreen` for example, may have many external dependencies (as well as their child components), which may cause tests to be flaky. Therefore, it is not recommended to add detailed tests to these types of components. Instead, add a test for those lower in the React tree (e.g. `Composer`). +- Make sure all additional dependencies are mocked correctly (navigation, contexts, external libraries, API etc.). ## `measureFunction` API approach diff --git a/tests/perf-test/ReportScreen.perf-test.tsx b/tests/perf-test/ReportScreen.perf-test.tsx index c89b7fe54b5f..ed5d502ac123 100644 --- a/tests/perf-test/ReportScreen.perf-test.tsx +++ b/tests/perf-test/ReportScreen.perf-test.tsx @@ -1,5 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import {fireEvent, screen, waitFor} from '@testing-library/react-native'; +import {screen, waitFor} from '@testing-library/react-native'; import type {ComponentType} from 'react'; import React from 'react'; import Onyx from 'react-native-onyx'; @@ -16,7 +16,6 @@ import {CurrentReportIDContextProvider} from '@src/components/withCurrentReportI import {KeyboardStateProvider} from '@src/components/withKeyboardState'; import {WindowDimensionsProvider} from '@src/components/withWindowDimensions'; import CONST from '@src/CONST'; -import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; import {ReportAttachmentsProvider} from '@src/pages/home/report/ReportAttachmentsContext'; import ReportScreen from '@src/pages/home/ReportScreen'; @@ -36,6 +35,12 @@ import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatch type ReportScreenWrapperProps = StackScreenProps; +jest.mock('@src/libs/API', () => ({ + write: jest.fn(), + makeRequestWithSideEffects: jest.fn(), + read: jest.fn(), +})); + jest.mock('react-native-reanimated', () => { const actualNav = jest.requireActual('react-native-reanimated/mock'); return { @@ -156,10 +161,10 @@ function ReportScreenWrapper(props: ReportScreenWrapperProps) { } const report = {...createRandomReport(1), policyID: '1'}; -const reportActions = ReportTestUtils.getMockedReportActionsMap(500); +const reportActions = ReportTestUtils.getMockedReportActionsMap(1000); const mockRoute = {params: {reportID: '1'}}; -test('[ReportScreen] should render ReportScreen with composer interactions', () => { +test('[ReportScreen] should render ReportScreen', () => { const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { /** @@ -173,83 +178,10 @@ test('[ReportScreen] should render ReportScreen with composer interactions', () await waitFor(triggerTransitionEnd); // Query for the composer - const composer = await screen.findByTestId('composer'); - - // Type in the composer - fireEvent.changeText(composer, 'Test message'); - - const hintSendButtonText = Localize.translateLocal('common.send'); - - // Query for the send button - const sendButton = await screen.findByLabelText(hintSendButtonText); - - // Click on the send button - fireEvent.press(sendButton); - - const hintHeaderText = Localize.translateLocal('common.back'); - - // Query for the header - await screen.findByLabelText(hintHeaderText); - }; - - const navigation = {addListener}; - - return waitForBatchedUpdates() - .then(() => { - const reportCollectionDataSet: ReportCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.REPORT}${mockRoute.params.reportID}`]: report, - }; - - const reportActionsCollectionDataSet: ReportActionsCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockRoute.params.reportID}`]: reportActions, - }; - - return Onyx.multiSet({ - [ONYXKEYS.IS_SIDEBAR_LOADED]: true, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails, - [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS], - [`${ONYXKEYS.COLLECTION.POLICY}`]: policies, - [ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: true, - ...reportCollectionDataSet, - ...reportActionsCollectionDataSet, - }); - }) - .then(() => - measurePerformance( - , - {scenario}, - ), - ); -}); - -test.skip('[ReportScreen] should press of the report item', () => { - const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); - const scenario = async () => { - /** - * First make sure ReportScreen is mounted, so that we can trigger - * the transitionEnd event manually. - * - * If we don't do that, then the transitionEnd event will be triggered - * before the ReportScreen is mounted, and the test will fail. - */ - await screen.findByTestId('ReportScreen'); - - await waitFor(triggerTransitionEnd); + await screen.findByTestId('composer'); // Query for the report list await screen.findByTestId('report-actions-list'); - - const hintText = Localize.translateLocal('accessibilityHints.chatMessage'); - - // Query for the list of items - const reportItems = await screen.findAllByLabelText(hintText); - - fireEvent.press(reportItems[0], 'onLongPress'); }; const navigation = {addListener}; @@ -269,6 +201,7 @@ test.skip('[ReportScreen] should press of the report item', () => { [ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails, [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS], [`${ONYXKEYS.COLLECTION.POLICY}`]: policies, + [ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: true, ...reportCollectionDataSet, ...reportActionsCollectionDataSet, }); diff --git a/tests/perf-test/SignInPage.perf-test.tsx b/tests/perf-test/SignInPage.perf-test.tsx deleted file mode 100644 index 67a8723192d2..000000000000 --- a/tests/perf-test/SignInPage.perf-test.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import type * as NativeNavigation from '@react-navigation/native'; -import {fireEvent, screen} from '@testing-library/react-native'; -import React from 'react'; -import Onyx from 'react-native-onyx'; -import {measurePerformance} from 'reassure'; -import ComposeProviders from '@components/ComposeProviders'; -import {LocaleContextProvider} from '@components/LocaleContextProvider'; -import OnyxProvider from '@components/OnyxProvider'; -import {WindowDimensionsProvider} from '@components/withWindowDimensions'; -import * as Localize from '@libs/Localize'; -import type * as Navigation from '@libs/Navigation/Navigation'; -import ONYXKEYS from '@src/ONYXKEYS'; -import SignInPage from '@src/pages/signin/SignInPage'; -import getValidCodeCredentials from '../utils/collections/getValidCodeCredentials'; -import userAccount, {getValidAccount} from '../utils/collections/userAccount'; -import PusherHelper from '../utils/PusherHelper'; -import * as TestHelper from '../utils/TestHelper'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; - -jest.mock('../../src/libs/Log'); - -jest.mock('../../src/libs/API', () => ({ - write: jest.fn(), - makeRequestWithSideEffects: jest.fn(), - read: jest.fn(), -})); - -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(), - }), - } as typeof NativeNavigation; -}); - -type Props = Partial & {navigation: Partial}; - -function SignInPageWrapper(args: Props) { - return ( - - - - ); -} - -const login = 'test@mail.com'; - -describe('SignInPage', () => { - beforeAll(() => { - Onyx.init({ - keys: ONYXKEYS, - safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], - }); - }); - - // Initialize the network key for OfflineWithFeedback - beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; - 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(); - PusherHelper.teardown(); - }); - - test('[SignInPage] should add username and click continue button', () => { - const addListener = jest.fn(); - const scenario = async () => { - // Checking the SignInPage is mounted - await screen.findByTestId('SignInPage'); - - const usernameInput = screen.getByTestId('username'); - - fireEvent.changeText(usernameInput, login); - - const hintContinueButtonText = Localize.translateLocal('common.continue'); - - const continueButton = await screen.findByText(hintContinueButtonText); - - fireEvent.press(continueButton); - }; - - const navigation = {addListener}; - - return waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - [ONYXKEYS.ACCOUNT]: userAccount, - [ONYXKEYS.IS_SIDEBAR_LOADED]: false, - }), - ) - .then(() => measurePerformance(, {scenario})); - }); - - test.skip('[SignInPage] should add magic code and click Sign In button', () => { - const addListener = jest.fn(); - const scenario = async () => { - // Checking the SignInPage is mounted - await screen.findByTestId('SignInPage'); - - const welcomeBackText = Localize.translateLocal('welcomeText.welcomeBack'); - const enterMagicCodeText = Localize.translateLocal('welcomeText.welcomeEnterMagicCode', {login}); - - await screen.findByText(`${welcomeBackText} ${enterMagicCodeText}`); - const magicCodeInput = screen.getByTestId('validateCode'); - - fireEvent.changeText(magicCodeInput, '123456'); - - const signInButtonText = Localize.translateLocal('common.signIn'); - const signInButton = await screen.findByText(signInButtonText); - - fireEvent.press(signInButton); - }; - - const navigation = {addListener}; - - return waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - [ONYXKEYS.ACCOUNT]: getValidAccount(login), - [ONYXKEYS.CREDENTIALS]: getValidCodeCredentials(login), - [ONYXKEYS.IS_SIDEBAR_LOADED]: false, - }), - ) - .then(() => measurePerformance(, {scenario})); - }); -});