Skip to content

Commit

Permalink
Merge pull request #33872 from callstack-internal/feat/SearchPage-rea…
Browse files Browse the repository at this point in the history
…ssure-perf-tests
  • Loading branch information
mountiny authored Jan 19, 2024
2 parents 41f051b + 34b2c49 commit bc3705e
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 2 deletions.
11 changes: 10 additions & 1 deletion src/pages/SearchPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,24 @@ 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({}),
};

const defaultProps = {
betas: [],
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: {},
Expand Down Expand Up @@ -165,6 +173,7 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) {
includeSafeAreaPaddingBottom={false}
testID={SearchPage.displayName}
onEntryTransitionEnd={updateOptions}
navigation={navigation}
>
{({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
<>
Expand Down
206 changes: 206 additions & 0 deletions tests/perf-test/SearchPage.perf-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
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 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 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';

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(),
};
});

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 = _.values(CONST.BETAS);
const mockedPersonalDetails = getMockedPersonalDetails(100);

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(args) {
return (
<ComposeProviders components={[OnyxProvider, CurrentReportIDContextProvider, KeyboardStateProvider, WindowDimensionsProvider, LocaleContextProvider]}>
<SearchPage
// eslint-disable-next-line react/jsx-props-no-spreading
{...args}
navigation={args.navigation}
/>
</ComposeProviders>
);
}

const runs = CONST.PERFORMANCE_TESTS.RUNS;

test('[Search Page] should interact when text input changes', async () => {
const {addListener} = TestHelper.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(() =>
Onyx.multiSet({
...mockedReports,
[ONYXKEYS.IS_SIDEBAR_LOADED]: true,
[ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails,
[ONYXKEYS.BETAS]: mockedBetas,
[ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true,
}),
)
.then(() => measurePerformance(<SearchPageWrapper navigation={navigation} />, {scenario, runs}));
});

test('[Search Page] should render options list', async () => {
const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock();
const smallMockedPersonalDetails = getMockedPersonalDetails(5);

const scenario = async () => {
await screen.findByTestId('SearchPage');
await act(triggerTransitionEnd);
await screen.findByText(smallMockedPersonalDetails['1'].login);
await screen.findByText(smallMockedPersonalDetails['2'].login);
};

const navigation = {addListener};

return waitForBatchedUpdates()
.then(() =>
Onyx.multiSet({
...mockedReports,
[ONYXKEYS.IS_SIDEBAR_LOADED]: true,
[ONYXKEYS.PERSONAL_DETAILS_LIST]: smallMockedPersonalDetails,
[ONYXKEYS.BETAS]: mockedBetas,
[ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true,
}),
)
.then(() => measurePerformance(<SearchPageWrapper navigation={navigation} />, {scenario, runs}));
});

test('[Search Page] should search in options list', async () => {
const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock();

const scenario = async () => {
await screen.findByTestId('SearchPage');
const input = screen.getByTestId('options-selector-input');

fireEvent.changeText(input, mockedPersonalDetails['88'].login);
await act(triggerTransitionEnd);
await screen.findByText(mockedPersonalDetails['88'].login);

fireEvent.changeText(input, mockedPersonalDetails['45'].login);
await act(triggerTransitionEnd);
await screen.findByText(mockedPersonalDetails['45'].login);
};

const navigation = {addListener};

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(<SearchPageWrapper navigation={navigation} />, {scenario, runs}));
});

test('[Search Page] should click on list item', async () => {
const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock();

const scenario = async () => {
await screen.findByTestId('SearchPage');
const input = screen.getByTestId('options-selector-input');

fireEvent.changeText(input, mockedPersonalDetails['6'].login);
await act(triggerTransitionEnd);
const optionButton = await screen.findByText(mockedPersonalDetails['6'].login);

fireEvent.press(optionButton);
};

const navigation = {addListener};
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(<SearchPageWrapper navigation={navigation} />, {scenario, runs}));
});
25 changes: 24 additions & 1 deletion tests/utils/TestHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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};

0 comments on commit bc3705e

Please sign in to comment.