From fba84c4b196ff5667b339d0a1db9d92d029806f8 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 19 Dec 2023 18:04:45 +0500 Subject: [PATCH 1/6] fix: use navigation from prop to support reassure testing --- src/components/ScreenWrapper/index.js | 9 +++++++-- src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx | 5 ++++- src/pages/home/ReportScreen.js | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index bd277ffa1ab8..f66fe2ab0c8d 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -1,4 +1,3 @@ -import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import React, {useEffect, useRef, useState} from 'react'; import {Keyboard, PanResponder, View} from 'react-native'; @@ -39,6 +38,13 @@ const ScreenWrapper = React.forwardRef( shouldDismissKeyboardBeforeClose, onEntryTransitionEnd, testID, + /** + * The navigation prop is passed by the navigator. It is used to trigger the onEntryTransitionEnd callback + * when the screen transition ends. + * + * This is required because transitionEnd event doesn't trigger in the testing environment. + */ + navigation, }, ref, ) => { @@ -48,7 +54,6 @@ const ScreenWrapper = React.forwardRef( const keyboardState = useKeyboardState(); const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); - const navigation = useNavigation(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; diff --git a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx index 20922fd785ce..c137af4485e3 100644 --- a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx +++ b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx @@ -13,7 +13,10 @@ function ReportScreenWrapper({route, navigation}: ReportScreenWrapperProps) { return ( <> {/* @ts-expect-error Error will be resolved after ReportScreen migration to TypeScript */} - + Date: Tue, 19 Dec 2023 18:05:23 +0500 Subject: [PATCH 2/6] test: add support for transitionEnd event for reassure --- tests/perf-test/ReportScreen.perf-test.js | 69 ++++++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js index 96514112cd05..3be414c148c2 100644 --- a/tests/perf-test/ReportScreen.perf-test.js +++ b/tests/perf-test/ReportScreen.perf-test.js @@ -1,4 +1,4 @@ -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'; @@ -101,6 +101,34 @@ afterEach(() => { PusherHelper.teardown(); }); +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate + * the transitionEnd event that is triggered when the screen transition animation is completed. + * + * 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 () => { + // eslint-disable-next-line rulesdir/prefer-underscore-method + transitionEndListeners.filter((cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + function ReportScreenWrapper(args) { return ( ); @@ -125,7 +154,12 @@ function ReportScreenWrapper(args) { const runs = CONST.PERFORMANCE_TESTS.RUNS; test('[ReportScreen] should render ReportScreen with composer interactions', () => { + const {triggerTransitionEnd, addListener} = createAddListenerMock(); const scenario = async () => { + await screen.findByTestId('ReportScreen'); + + await act(triggerTransitionEnd); + // Query for the report list await screen.findByTestId('report-actions-list'); @@ -158,6 +192,10 @@ test('[ReportScreen] should render ReportScreen with composer interactions', () const reportActions = ReportTestUtils.getMockedReportActionsMap(1000); const mockRoute = {params: {reportID: '1'}}; + const navigation = { + addListener, + }; + return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -172,11 +210,24 @@ test('[ReportScreen] should render ReportScreen with composer interactions', () }, }), ) - .then(() => measurePerformance(, {scenario, runs})); + .then(() => + measurePerformance( + , + {scenario, runs}, + ), + ); }); test('[ReportScreen] should press of the report item', () => { + const {triggerTransitionEnd, addListener} = createAddListenerMock(); const scenario = async () => { + await screen.findByTestId('ReportScreen'); + + await act(triggerTransitionEnd); + // Query for the report list await screen.findByTestId('report-actions-list'); @@ -201,6 +252,10 @@ test('[ReportScreen] should press of the report item', () => { const reportActions = ReportTestUtils.getMockedReportActionsMap(1000); const mockRoute = {params: {reportID: '2'}}; + const navigation = { + addListener, + }; + return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -215,5 +270,13 @@ test('[ReportScreen] should press of the report item', () => { }, }), ) - .then(() => measurePerformance(, {scenario, runs})); + .then(() => + measurePerformance( + , + {scenario, runs}, + ), + ); }); From e0e06a70de459009999076bf3805ded7a1deb997 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 20 Dec 2023 13:04:17 +0500 Subject: [PATCH 3/6] fix: typecheck --- src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx index c137af4485e3..f3295aadb888 100644 --- a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx +++ b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx @@ -12,8 +12,8 @@ function ReportScreenWrapper({route, navigation}: ReportScreenWrapperProps) { // until the reportID is loaded and set in the route param return ( <> - {/* @ts-expect-error Error will be resolved after ReportScreen migration to TypeScript */} From 5b824fb0d3ffffcc00ab09cfd9ff0f5e4ce69965 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 20 Dec 2023 14:01:48 +0500 Subject: [PATCH 4/6] fix: fallback to useNavigation --- src/components/ScreenWrapper/index.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index f66fe2ab0c8d..5605689e2702 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -1,3 +1,4 @@ +import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import React, {useEffect, useRef, useState} from 'react'; import {Keyboard, PanResponder, View} from 'react-native'; @@ -44,10 +45,19 @@ const ScreenWrapper = React.forwardRef( * * This is required because transitionEnd event doesn't trigger in the testing environment. */ - navigation, + navigation: navigationProp, }, ref, ) => { + /** + * We are only passing navigation as prop from + * ReportScreenWrapper -> ReportScreen -> ScreenWrapper + * + * so in other places where ScreenWrapper is used, we need to + * fallback to useNavigation. + */ + const navigationFallback = useNavigation(); + const navigation = navigationProp || navigationFallback; const {windowHeight, isSmallScreenWidth} = useWindowDimensions(); const {initialHeight} = useInitialDimensions(); const styles = useThemeStyles(); From d79f5ae429a3265b25c9022b934b58616acc69e1 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 21 Dec 2023 12:45:20 +0500 Subject: [PATCH 5/6] fix: linting --- src/components/ScreenWrapper/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index 5605689e2702..432139353c56 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -39,6 +39,7 @@ const ScreenWrapper = React.forwardRef( shouldDismissKeyboardBeforeClose, onEntryTransitionEnd, testID, + /** * The navigation prop is passed by the navigator. It is used to trigger the onEntryTransitionEnd callback * when the screen transition ends. From b8429c39e554d55e9ed2ade1514aebb79041a7e8 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 21 Dec 2023 12:46:02 +0500 Subject: [PATCH 6/6] fix: add comments and linting --- tests/perf-test/ReportScreen.perf-test.js | 26 ++++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js index 3be414c148c2..a82903762631 100644 --- a/tests/perf-test/ReportScreen.perf-test.js +++ b/tests/perf-test/ReportScreen.perf-test.js @@ -2,6 +2,7 @@ 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 ComposeProviders from '../../src/components/ComposeProviders'; import DragAndDropProvider from '../../src/components/DragAndDrop/Provider'; import {LocaleContextProvider} from '../../src/components/LocaleContextProvider'; @@ -121,8 +122,7 @@ const createAddListenerMock = () => { transitionEndListeners.push(callback); } return () => { - // eslint-disable-next-line rulesdir/prefer-underscore-method - transitionEndListeners.filter((cb) => cb !== callback); + _.filter(transitionEndListeners, (cb) => cb !== callback); }; }); @@ -156,6 +156,13 @@ const runs = CONST.PERFORMANCE_TESTS.RUNS; test('[ReportScreen] should render ReportScreen with composer interactions', () => { const {triggerTransitionEnd, addListener} = 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 act(triggerTransitionEnd); @@ -192,9 +199,7 @@ test('[ReportScreen] should render ReportScreen with composer interactions', () const reportActions = ReportTestUtils.getMockedReportActionsMap(1000); const mockRoute = {params: {reportID: '1'}}; - const navigation = { - addListener, - }; + const navigation = {addListener}; return waitForBatchedUpdates() .then(() => @@ -224,6 +229,13 @@ test('[ReportScreen] should render ReportScreen with composer interactions', () test('[ReportScreen] should press of the report item', () => { const {triggerTransitionEnd, addListener} = 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 act(triggerTransitionEnd); @@ -252,9 +264,7 @@ test('[ReportScreen] should press of the report item', () => { const reportActions = ReportTestUtils.getMockedReportActionsMap(1000); const mockRoute = {params: {reportID: '2'}}; - const navigation = { - addListener, - }; + const navigation = {addListener}; return waitForBatchedUpdates() .then(() =>