From d6159cadc3b3ffb6e9a9fb300e31c28b4ccf60cb Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Wed, 3 Jan 2024 16:30:34 +0100 Subject: [PATCH 1/8] feat: initial SearchPage reassure tests setup --- tests/perf-test/SearchPage.perf-test.js | 142 ++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 tests/perf-test/SearchPage.perf-test.js diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js new file mode 100644 index 000000000000..bacd73836ffa --- /dev/null +++ b/tests/perf-test/SearchPage.perf-test.js @@ -0,0 +1,142 @@ +import {fireEvent, screen} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import {measurePerformance} from 'reassure'; +import _ from 'underscore'; +import SearchPage from '@pages/SearchPage'; +import ComposeProviders from '../../src/components/ComposeProviders'; +import {LocaleContextProvider} from '../../src/components/LocaleContextProvider'; +import OnyxProvider from '../../src/components/OnyxProvider'; +import {CurrentReportIDContextProvider} from '../../src/components/withCurrentReportID'; +import {KeyboardStateProvider} from '../../src/components/withKeyboardState'; +import {WindowDimensionsProvider} from '../../src/components/withWindowDimensions'; +import CONST from '../../src/CONST'; +import ONYXKEYS from '../../src/ONYXKEYS'; +import * as LHNTestUtils from '../utils/LHNTestUtils'; +import PusherHelper from '../utils/PusherHelper'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; + +jest.mock('../../src/components/withNavigationFocus', () => (Component) => { + function WithNavigationFocus(props) { + return ( + + ); + } + + WithNavigationFocus.displayName = 'WithNavigationFocus'; + + return WithNavigationFocus; +}); + +jest.mock('../../src/libs/Navigation/Navigation'); + +const mockedNavigate = jest.fn(); +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useFocusEffect: jest.fn(), + useIsFocused: () => ({ + navigate: mockedNavigate, + }), + useRoute: () => jest.fn(), + useNavigation: () => ({ + navigate: jest.fn(), + addListener: () => jest.fn(), + }), + createNavigationContainerRef: jest.fn(), + }; +}); + +beforeAll(() => + Onyx.init({ + keys: ONYXKEYS, + safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT], + registerStorageEventListener: () => {}, + }), +); + +// 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(); + PusherHelper.teardown(); +}); + +function SearchPageWrapper() { + return ( + + + + ); +} + +const runs = CONST.PERFORMANCE_TESTS.RUNS; + +test('[Search Page] should interact when text input changes', () => { + const scenario = async () => { + await screen.findByTestId('SearchPage'); + + const input = screen.getByTestId('options-selector-input'); + + fireEvent.changeText(input, 'Email Four'); + fireEvent.changeText(input, 'Report'); + fireEvent.changeText(input, 'Email Five'); + + }; + + // TODO create util to generate many reports + const report = LHNTestUtils.getFakeReport(); + const report2 = LHNTestUtils.getFakeReport(); + const mockedReports = { + [`report_${report.reportID}`]: report, + [`report_${report2.reportID}`]: report2, + }; + const mockedBetas = _.values(CONST.BETAS); + const mockedPersonalDetails = LHNTestUtils.fakePersonalDetails; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + [ONYXKEYS.IS_SIDEBAR_LOADED]: true, + [`${ONYXKEYS.COLLECTION.REPORT}`]: mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [`${ONYXKEYS.IS_SEARCHING_FOR_REPORTS}`]: true, + }), + ) + .then(() => + measurePerformance( + , + {scenario, runs}, + ), + ); +}); + +test('[Search Page] should render options list', () => { + // TODO +}); + +test('[Search Page] should click on list item', () => { + // TODO +}); From 2e5041e0a9ba4c03efef82b12af20ae5396da1ae Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Mon, 8 Jan 2024 16:53:02 +0100 Subject: [PATCH 2/8] feat: added reassure tests for SearchPage --- tests/perf-test/SearchPage.perf-test.js | 85 +++++++++++++++++++------ 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index bacd73836ffa..1ce840002670 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -4,6 +4,7 @@ import Onyx from 'react-native-onyx'; import {measurePerformance} from 'reassure'; import _ from 'underscore'; import SearchPage from '@pages/SearchPage'; +import createRandomReport from '../utils/collections/reports'; import ComposeProviders from '../../src/components/ComposeProviders'; import {LocaleContextProvider} from '../../src/components/LocaleContextProvider'; import OnyxProvider from '../../src/components/OnyxProvider'; @@ -54,6 +55,23 @@ jest.mock('@react-navigation/native', () => { }; }); +const getMockedReportsMap = (length = 100) => { + const mockReports = Array.from({length}, (__, i) => { + const reportID = i + 1; + const report = createRandomReport(reportID) + const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; + + return {[reportKey]: report}; + }); + + return _.assign({}, ...mockReports); +}; + +const mockedReports = getMockedReportsMap(500); +const mockedBetas = _.values(CONST.BETAS); +const mockedPersonalDetails = LHNTestUtils.fakePersonalDetails; + + beforeAll(() => Onyx.init({ keys: ONYXKEYS, @@ -92,37 +110,28 @@ function SearchPageWrapper() { } const runs = CONST.PERFORMANCE_TESTS.RUNS; - -test('[Search Page] should interact when text input changes', () => { + + test('[Search Page] should interact when text input changes', () => { const scenario = async () => { await screen.findByTestId('SearchPage'); const input = screen.getByTestId('options-selector-input'); - + fireEvent.changeText(input, 'Email Four'); fireEvent.changeText(input, 'Report'); fireEvent.changeText(input, 'Email Five'); }; - - // TODO create util to generate many reports - const report = LHNTestUtils.getFakeReport(); - const report2 = LHNTestUtils.getFakeReport(); - const mockedReports = { - [`report_${report.reportID}`]: report, - [`report_${report2.reportID}`]: report2, - }; - const mockedBetas = _.values(CONST.BETAS); - const mockedPersonalDetails = LHNTestUtils.fakePersonalDetails; + return waitForBatchedUpdates() .then(() => Onyx.multiSet({ + ...mockedReports, [ONYXKEYS.IS_SIDEBAR_LOADED]: true, - [`${ONYXKEYS.COLLECTION.REPORT}`]: mockedReports, [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, [ONYXKEYS.BETAS]: mockedBetas, - [`${ONYXKEYS.IS_SEARCHING_FOR_REPORTS}`]: true, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) .then(() => @@ -131,12 +140,52 @@ test('[Search Page] should interact when text input changes', () => { {scenario, runs}, ), ); -}); +}); + test('[Search Page] should render options list', () => { - // TODO + const scenario = async () => { + await screen.findByTestId('SearchPage'); + const input = screen.getByTestId('options-selector-input'); + + fireEvent.changeText(input, 'email5@test.com'); + await screen.findByText('email5@test.com'); + }; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.IS_SIDEBAR_LOADED]: true, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + + }), + ) + .then(() => measurePerformance(, {scenario, runs})); }); test('[Search Page] should click on list item', () => { - // TODO + const scenario = async () => { + await screen.findByTestId('SearchPage'); + const input = screen.getByTestId('options-selector-input'); + + fireEvent.changeText(input, 'email6@test.com'); + const optionButton = await screen.findByText('email6@test.com'); + + fireEvent.press(optionButton); + }; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.IS_SIDEBAR_LOADED]: true, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + .then(() => measurePerformance(, {scenario, runs})); }); From e84014009c1894c34b042abc597be3038c01319e Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Tue, 9 Jan 2024 16:38:58 +0100 Subject: [PATCH 3/8] feat: add reassure tests for SearchPage, mock navigation --- src/components/ScreenWrapper/index.js | 3 +- src/pages/SearchPage.js | 6 +- tests/perf-test/SearchPage.perf-test.js | 138 +++++++++++++++--------- 3 files changed, 97 insertions(+), 50 deletions(-) diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index 432139353c56..8a23d749b9da 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -52,7 +52,8 @@ const ScreenWrapper = React.forwardRef( ) => { /** * We are only passing navigation as prop from - * ReportScreenWrapper -> ReportScreen -> ScreenWrapper + * ReportScreenWrapper -> ReportScreen -> ScreenWrapper and + * SearchPage -> ScreenWrapper * * so in other places where ScreenWrapper is used, we need to * fallback to useNavigation. diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 061f43e73de8..3d887b06d2c4 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -33,6 +33,8 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, + + navigation: PropTypes.shape({}) }; const defaultProps = { @@ -40,9 +42,10 @@ const defaultProps = { personalDetails: {}, reports: {}, isSearchingForReports: false, + navigation: {} }; -function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { +function SearchPage({betas, personalDetails, reports, isSearchingForReports, navigation}) { const [searchValue, setSearchValue] = useState(''); const [searchOptions, setSearchOptions] = useState({ recentReports: {}, @@ -165,6 +168,7 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { includeSafeAreaPaddingBottom={false} testID={SearchPage.displayName} onEntryTransitionEnd={updateOptions} + navigation={navigation} > {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( <> diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index 1ce840002670..93836f01d0aa 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -1,10 +1,9 @@ -import {fireEvent, screen} from '@testing-library/react-native'; +import {act, fireEvent, screen} from '@testing-library/react-native'; import React from 'react'; import Onyx from 'react-native-onyx'; import {measurePerformance} from 'reassure'; import _ from 'underscore'; import SearchPage from '@pages/SearchPage'; -import createRandomReport from '../utils/collections/reports'; import ComposeProviders from '../../src/components/ComposeProviders'; import {LocaleContextProvider} from '../../src/components/LocaleContextProvider'; import OnyxProvider from '../../src/components/OnyxProvider'; @@ -13,28 +12,13 @@ import {KeyboardStateProvider} from '../../src/components/withKeyboardState'; import {WindowDimensionsProvider} from '../../src/components/withWindowDimensions'; import CONST from '../../src/CONST'; import ONYXKEYS from '../../src/ONYXKEYS'; +import createRandomReport from '../utils/collections/reports'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; -jest.mock('../../src/components/withNavigationFocus', () => (Component) => { - function WithNavigationFocus(props) { - return ( - - ); - } - - WithNavigationFocus.displayName = 'WithNavigationFocus'; - - return WithNavigationFocus; -}); - jest.mock('../../src/libs/Navigation/Navigation'); const mockedNavigate = jest.fn(); @@ -58,7 +42,7 @@ jest.mock('@react-navigation/native', () => { const getMockedReportsMap = (length = 100) => { const mockReports = Array.from({length}, (__, i) => { const reportID = i + 1; - const report = createRandomReport(reportID) + const report = createRandomReport(reportID); const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; return {[reportKey]: report}; @@ -67,11 +51,10 @@ const getMockedReportsMap = (length = 100) => { return _.assign({}, ...mockReports); }; -const mockedReports = getMockedReportsMap(500); +const mockedReports = getMockedReportsMap(600); const mockedBetas = _.values(CONST.BETAS); const mockedPersonalDetails = LHNTestUtils.fakePersonalDetails; - beforeAll(() => Onyx.init({ keys: ONYXKEYS, @@ -93,36 +76,60 @@ afterEach(() => { PusherHelper.teardown(); }); -function SearchPageWrapper() { +function SearchPageWrapper(args) { return ( - - + + ); } const runs = CONST.PERFORMANCE_TESTS.RUNS; - - test('[Search Page] should interact when text input changes', () => { + +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * Same aproach as in ReportScreen.perf-test.js + * + * P.S: This can't be moved to a utils file because Jest wants any external function to stay in the scope. + * + * @returns {Object} An object with two functions: triggerTransitionEnd and addListener + */ +const createAddListenerMock = () => { + const transitionEndListeners = []; + const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + }; + + const addListener = jest.fn().mockImplementation((listener, callback) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + _.filter(transitionEndListeners, (cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + + +test('[Search Page] should interact when text input changes', async () => { + const {addListener} = createAddListenerMock(); + const scenario = async () => { await screen.findByTestId('SearchPage'); const input = screen.getByTestId('options-selector-input'); - fireEvent.changeText(input, 'Email Four'); fireEvent.changeText(input, 'Report'); fireEvent.changeText(input, 'Email Five'); - }; - + + const navigation = {addListener}; return waitForBatchedUpdates() .then(() => @@ -134,24 +141,56 @@ const runs = CONST.PERFORMANCE_TESTS.RUNS; [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) + .then(() => measurePerformance(, {scenario, runs})); +}); + +test('[Search Page] should render options list', async () => { + const {triggerTransitionEnd, addListener} = createAddListenerMock(); + + const scenario = async () => { + await screen.findByTestId('SearchPage'); + await act(triggerTransitionEnd); + await screen.findByText('email2@test.com'); + await screen.findByText('email3@test.com'); + }; + + const navigation = {addListener}; + + return waitForBatchedUpdates() .then(() => - measurePerformance( - , - {scenario, runs}, - ), - ); -}); + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.IS_SIDEBAR_LOADED]: true, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + .then(() => measurePerformance(, {scenario, runs})); +}); +test('[Search Page] should search in options list', async () => { + const {triggerTransitionEnd, addListener} = createAddListenerMock(); -test('[Search Page] should render options list', () => { const scenario = async () => { await screen.findByTestId('SearchPage'); const input = screen.getByTestId('options-selector-input'); fireEvent.changeText(input, 'email5@test.com'); + await act(triggerTransitionEnd); await screen.findByText('email5@test.com'); + + fireEvent.changeText(input, 'email8@test.com'); + await act(triggerTransitionEnd); + await screen.findByText('email8@test.com'); + + fireEvent.changeText(input, 'email2@test.com'); + await act(triggerTransitionEnd); + await screen.findByText('email2@test.com'); }; + const navigation = {addListener}; + return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -160,23 +199,26 @@ test('[Search Page] should render options list', () => { [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, [ONYXKEYS.BETAS]: mockedBetas, [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, - }), ) - .then(() => measurePerformance(, {scenario, runs})); + .then(() => measurePerformance(, {scenario, runs})); }); -test('[Search Page] should click on list item', () => { +test('[Search Page] should click on list item', async () => { + const {triggerTransitionEnd, addListener} = createAddListenerMock(); + const scenario = async () => { await screen.findByTestId('SearchPage'); const input = screen.getByTestId('options-selector-input'); fireEvent.changeText(input, 'email6@test.com'); + await act(triggerTransitionEnd); const optionButton = await screen.findByText('email6@test.com'); fireEvent.press(optionButton); }; + const navigation = {addListener}; return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -187,5 +229,5 @@ test('[Search Page] should click on list item', () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => measurePerformance(, {scenario, runs})); + .then(() => measurePerformance(, {scenario, runs})); }); From db3f77d56285c78723be4f7fd1c63be5cf6c5526 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Tue, 9 Jan 2024 17:26:01 +0100 Subject: [PATCH 4/8] refactor: fix prettier and typo --- src/pages/SearchPage.js | 4 ++-- tests/perf-test/SearchPage.perf-test.js | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 3d887b06d2c4..35697fea661d 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -34,7 +34,7 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, - navigation: PropTypes.shape({}) + navigation: PropTypes.shape({}), }; const defaultProps = { @@ -42,7 +42,7 @@ const defaultProps = { personalDetails: {}, reports: {}, isSearchingForReports: false, - navigation: {} + navigation: {}, }; function SearchPage({betas, personalDetails, reports, isSearchingForReports, navigation}) { diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index 93836f01d0aa..30768bd1760d 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -92,8 +92,8 @@ const runs = CONST.PERFORMANCE_TESTS.RUNS; /** * This is a helper function to create a mock for the addListener function of the react-navigation library. - * Same aproach as in ReportScreen.perf-test.js - * + * Same approach as in ReportScreen.perf-test.js + * * P.S: This can't be moved to a utils file because Jest wants any external function to stay in the scope. * * @returns {Object} An object with two functions: triggerTransitionEnd and addListener @@ -116,7 +116,6 @@ const createAddListenerMock = () => { return {triggerTransitionEnd, addListener}; }; - test('[Search Page] should interact when text input changes', async () => { const {addListener} = createAddListenerMock(); From c8b23a56e244146cb1e0745283de46ddaf1de580 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Mon, 15 Jan 2024 15:50:32 +0100 Subject: [PATCH 5/8] refactor: use different method for mocking personal data --- tests/perf-test/SearchPage.perf-test.js | 34 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index 30768bd1760d..23df48f98dc1 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -12,8 +12,8 @@ import {KeyboardStateProvider} from '../../src/components/withKeyboardState'; import {WindowDimensionsProvider} from '../../src/components/withWindowDimensions'; import CONST from '../../src/CONST'; import ONYXKEYS from '../../src/ONYXKEYS'; +import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomReport from '../utils/collections/reports'; -import * as LHNTestUtils from '../utils/LHNTestUtils'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -51,9 +51,19 @@ const getMockedReportsMap = (length = 100) => { return _.assign({}, ...mockReports); }; +const getMockedPersonalDetailsMap = (length) => { + const mockPersonalDetails = Array.from({length}, (__, i) => { + const personalDetailsKey = i + 1; + const personalDetails = createPersonalDetails(personalDetailsKey); + return {[personalDetailsKey]: personalDetails}; + }); + + return _.assign({}, ...mockPersonalDetails); +}; + const mockedReports = getMockedReportsMap(600); const mockedBetas = _.values(CONST.BETAS); -const mockedPersonalDetails = LHNTestUtils.fakePersonalDetails; +const mockedPersonalDetails = getMockedPersonalDetailsMap(10); beforeAll(() => Onyx.init({ @@ -149,8 +159,8 @@ test('[Search Page] should render options list', async () => { const scenario = async () => { await screen.findByTestId('SearchPage'); await act(triggerTransitionEnd); - await screen.findByText('email2@test.com'); - await screen.findByText('email3@test.com'); + await screen.findByText(mockedPersonalDetails['1'].login); + await screen.findByText(mockedPersonalDetails['2'].login); }; const navigation = {addListener}; @@ -175,17 +185,17 @@ test('[Search Page] should search in options list', async () => { await screen.findByTestId('SearchPage'); const input = screen.getByTestId('options-selector-input'); - fireEvent.changeText(input, 'email5@test.com'); + fireEvent.changeText(input, mockedPersonalDetails['5'].login); await act(triggerTransitionEnd); - await screen.findByText('email5@test.com'); + await screen.findByText(mockedPersonalDetails['5'].login); - fireEvent.changeText(input, 'email8@test.com'); + fireEvent.changeText(input, mockedPersonalDetails['8'].login); await act(triggerTransitionEnd); - await screen.findByText('email8@test.com'); + await screen.findByText(mockedPersonalDetails['8'].login); - fireEvent.changeText(input, 'email2@test.com'); + fireEvent.changeText(input, mockedPersonalDetails['2'].login); await act(triggerTransitionEnd); - await screen.findByText('email2@test.com'); + await screen.findByText(mockedPersonalDetails['2'].login); }; const navigation = {addListener}; @@ -210,9 +220,9 @@ test('[Search Page] should click on list item', async () => { await screen.findByTestId('SearchPage'); const input = screen.getByTestId('options-selector-input'); - fireEvent.changeText(input, 'email6@test.com'); + fireEvent.changeText(input, mockedPersonalDetails['6'].login); await act(triggerTransitionEnd); - const optionButton = await screen.findByText('email6@test.com'); + const optionButton = await screen.findByText(mockedPersonalDetails['6'].login); fireEvent.press(optionButton); }; From e1a73ab28f10ea21f27e4707237c1542061c89f6 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Mon, 15 Jan 2024 16:08:47 +0100 Subject: [PATCH 6/8] feat: create larger personal detail list --- tests/perf-test/SearchPage.perf-test.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index 23df48f98dc1..80a5f475bd82 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -63,7 +63,7 @@ const getMockedPersonalDetailsMap = (length) => { const mockedReports = getMockedReportsMap(600); const mockedBetas = _.values(CONST.BETAS); -const mockedPersonalDetails = getMockedPersonalDetailsMap(10); +const mockedPersonalDetails = getMockedPersonalDetailsMap(100); beforeAll(() => Onyx.init({ @@ -155,12 +155,13 @@ test('[Search Page] should interact when text input changes', async () => { test('[Search Page] should render options list', async () => { const {triggerTransitionEnd, addListener} = createAddListenerMock(); + const smallMockedPersonalDetails = getMockedPersonalDetailsMap(5); const scenario = async () => { await screen.findByTestId('SearchPage'); await act(triggerTransitionEnd); - await screen.findByText(mockedPersonalDetails['1'].login); - await screen.findByText(mockedPersonalDetails['2'].login); + await screen.findByText(smallMockedPersonalDetails['1'].login); + await screen.findByText(smallMockedPersonalDetails['2'].login); }; const navigation = {addListener}; @@ -170,7 +171,7 @@ test('[Search Page] should render options list', async () => { Onyx.multiSet({ ...mockedReports, [ONYXKEYS.IS_SIDEBAR_LOADED]: true, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: smallMockedPersonalDetails, [ONYXKEYS.BETAS]: mockedBetas, [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), @@ -185,17 +186,13 @@ test('[Search Page] should search in options list', async () => { await screen.findByTestId('SearchPage'); const input = screen.getByTestId('options-selector-input'); - fireEvent.changeText(input, mockedPersonalDetails['5'].login); - await act(triggerTransitionEnd); - await screen.findByText(mockedPersonalDetails['5'].login); - - fireEvent.changeText(input, mockedPersonalDetails['8'].login); + fireEvent.changeText(input, mockedPersonalDetails['88'].login); await act(triggerTransitionEnd); - await screen.findByText(mockedPersonalDetails['8'].login); + await screen.findByText(mockedPersonalDetails['88'].login); - fireEvent.changeText(input, mockedPersonalDetails['2'].login); + fireEvent.changeText(input, mockedPersonalDetails['45'].login); await act(triggerTransitionEnd); - await screen.findByText(mockedPersonalDetails['2'].login); + await screen.findByText(mockedPersonalDetails['45'].login); }; const navigation = {addListener}; From bddd3b65aead29ca2352c66ea2ebd56a390fd2b2 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Tue, 16 Jan 2024 10:19:33 +0100 Subject: [PATCH 7/8] refactor: use createCollection for mocking colections --- tests/perf-test/SearchPage.perf-test.js | 39 ++++++++++--------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index 80a5f475bd82..337007f04c31 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -12,6 +12,7 @@ import {KeyboardStateProvider} from '../../src/components/withKeyboardState'; import {WindowDimensionsProvider} from '../../src/components/withWindowDimensions'; import CONST from '../../src/CONST'; import ONYXKEYS from '../../src/ONYXKEYS'; +import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomReport from '../utils/collections/reports'; import PusherHelper from '../utils/PusherHelper'; @@ -39,31 +40,23 @@ jest.mock('@react-navigation/native', () => { }; }); -const getMockedReportsMap = (length = 100) => { - const mockReports = Array.from({length}, (__, i) => { - const reportID = i + 1; - const report = createRandomReport(reportID); - const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; - - return {[reportKey]: report}; - }); - - return _.assign({}, ...mockReports); -}; - -const getMockedPersonalDetailsMap = (length) => { - const mockPersonalDetails = Array.from({length}, (__, i) => { - const personalDetailsKey = i + 1; - const personalDetails = createPersonalDetails(personalDetailsKey); - return {[personalDetailsKey]: personalDetails}; - }); +const getMockedReports = (length = 100) => + createCollection( + (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, + (index) => createRandomReport(index), + length, + ); - return _.assign({}, ...mockPersonalDetails); -}; +const getMockedPersonalDetails = (length = 100) => + createCollection( + (item) => item.accountID, + (index) => createPersonalDetails(index), + length, + ); -const mockedReports = getMockedReportsMap(600); +const mockedReports = getMockedReports(600); const mockedBetas = _.values(CONST.BETAS); -const mockedPersonalDetails = getMockedPersonalDetailsMap(100); +const mockedPersonalDetails = getMockedPersonalDetails(100); beforeAll(() => Onyx.init({ @@ -155,7 +148,7 @@ test('[Search Page] should interact when text input changes', async () => { test('[Search Page] should render options list', async () => { const {triggerTransitionEnd, addListener} = createAddListenerMock(); - const smallMockedPersonalDetails = getMockedPersonalDetailsMap(5); + const smallMockedPersonalDetails = getMockedPersonalDetails(5); const scenario = async () => { await screen.findByTestId('SearchPage'); From 34b2c49dd1f12eec57ef1bc4717a44e6be4bab37 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Fri, 19 Jan 2024 16:03:27 +0100 Subject: [PATCH 8/8] refactor: move createAddListenerMock to separate file, add comments --- src/pages/SearchPage.js | 5 ++++ tests/perf-test/SearchPage.perf-test.js | 34 +++---------------------- tests/utils/TestHelper.js | 25 +++++++++++++++++- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 35697fea661d..c420371f5a65 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -34,6 +34,11 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, + /** + * The navigation prop passed by the navigator. + * + * This is required because transitionEnd event doesn't trigger in the automated testing environment. + */ navigation: PropTypes.shape({}), }; diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index 337007f04c31..c1568bea5dcd 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -93,34 +93,8 @@ function SearchPageWrapper(args) { const runs = CONST.PERFORMANCE_TESTS.RUNS; -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * Same approach as in ReportScreen.perf-test.js - * - * P.S: This can't be moved to a utils file because Jest wants any external function to stay in the scope. - * - * @returns {Object} An object with two functions: triggerTransitionEnd and addListener - */ -const createAddListenerMock = () => { - const transitionEndListeners = []; - const triggerTransitionEnd = () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener = jest.fn().mockImplementation((listener, callback) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - _.filter(transitionEndListeners, (cb) => cb !== callback); - }; - }); - - return {triggerTransitionEnd, addListener}; -}; - test('[Search Page] should interact when text input changes', async () => { - const {addListener} = createAddListenerMock(); + const {addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { await screen.findByTestId('SearchPage'); @@ -147,7 +121,7 @@ test('[Search Page] should interact when text input changes', async () => { }); test('[Search Page] should render options list', async () => { - const {triggerTransitionEnd, addListener} = createAddListenerMock(); + const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); const smallMockedPersonalDetails = getMockedPersonalDetails(5); const scenario = async () => { @@ -173,7 +147,7 @@ test('[Search Page] should render options list', async () => { }); test('[Search Page] should search in options list', async () => { - const {triggerTransitionEnd, addListener} = createAddListenerMock(); + const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { await screen.findByTestId('SearchPage'); @@ -204,7 +178,7 @@ test('[Search Page] should search in options list', async () => { }); test('[Search Page] should click on list item', async () => { - const {triggerTransitionEnd, addListener} = createAddListenerMock(); + const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { await screen.findByTestId('SearchPage'); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 03f5416a92fb..dd95ab4efb67 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -216,4 +216,27 @@ function assertFormDataMatchesObject(formData, obj) { expect(_.reduce(Array.from(formData.entries()), (memo, x) => ({...memo, [x[0]]: x[1]}), {})).toEqual(expect.objectContaining(obj)); } -export {getGlobalFetchMock, signInWithTestUser, signOutTestUser, setPersonalDetails, buildPersonalDetails, buildTestReportComment, assertFormDataMatchesObject}; +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * + * @returns {Object} An object with two functions: triggerTransitionEnd and addListener + */ +const createAddListenerMock = () => { + const transitionEndListeners = []; + const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + }; + + const addListener = jest.fn().mockImplementation((listener, callback) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + _.filter(transitionEndListeners, (cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + +export {getGlobalFetchMock, signInWithTestUser, signOutTestUser, setPersonalDetails, buildPersonalDetails, buildTestReportComment, assertFormDataMatchesObject, createAddListenerMock};