diff --git a/help/index.md b/help/index.md index e5d075402ecb..b198c5e20781 100644 --- a/help/index.md +++ b/help/index.md @@ -1,5 +1,7 @@ --- title: New Expensify Help --- + Pages: -* [Expensify Superapp](/superapp.html) + +- [Expensify Superapp](/superapp.html) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 18a14c0ec719..e459be1815bc 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -14,6 +14,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import * as SearchActions from '@libs/actions/Search'; @@ -49,19 +50,26 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri return [item.keyForList, {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}]; } -function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean) { - return {...item, isSelected: selectedTransactions[item.keyForList]?.isSelected && canSelectMultiple}; +function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean, shouldAnimateInHighlight: boolean) { + return {...item, shouldAnimateInHighlight, isSelected: selectedTransactions[item.keyForList]?.isSelected && canSelectMultiple}; } -function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType | ReportActionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean) { +function mapToItemWithSelectionInfo( + item: TransactionListItemType | ReportListItemType | ReportActionListItemType, + selectedTransactions: SelectedTransactions, + canSelectMultiple: boolean, + shouldAnimateInHighlight: boolean, +) { if (SearchUtils.isReportActionListItemType(item)) { return item; } + return SearchUtils.isTransactionListItemType(item) - ? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple) + ? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight) : { ...item, - transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions, canSelectMultiple)), + shouldAnimateInHighlight, + transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight)), isSelected: item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected && canSelectMultiple), }; } @@ -90,6 +98,8 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr const {type, status, sortBy, sortOrder, hash} = queryJSON; const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); + const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); + const previousTransactions = usePrevious(transactions); const canSelectMultiple = isSmallScreenWidth ? !!selectionMode?.isEnabled : true; @@ -117,7 +127,6 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr } SearchActions.search({queryJSON, offset}); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isOffline, offset, queryJSON]); const getItemHeight = useCallback( @@ -156,6 +165,14 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr const searchResults = currentSearchResults?.data ? currentSearchResults : lastSearchResultsRef.current; + const {newSearchResultKey, handleSelectionListScroll} = useSearchHighlightAndScroll({ + searchResults, + transactions, + previousTransactions, + queryJSON, + offset, + }); + // There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded // we also need to check that the searchResults matches the type and status of the current search const isDataLoaded = searchResults?.data !== undefined && searchResults?.search?.type === type && searchResults?.search?.status === status; @@ -193,7 +210,20 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr const ListItem = SearchUtils.getListItem(type, status); const data = SearchUtils.getSections(type, status, searchResults.data, searchResults.search); const sortedData = SearchUtils.getSortedSections(type, status, data, sortBy, sortOrder); - const sortedSelectedData = sortedData.map((item) => mapToItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple)); + const sortedSelectedData = sortedData.map((item) => { + const baseKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`; + // Check if the base key matches the newSearchResultKey (TransactionListItemType) + const isBaseKeyMatch = baseKey === newSearchResultKey; + // Check if any transaction within the transactions array (ReportListItemType) matches the newSearchResultKey + const isAnyTransactionMatch = (item as ReportListItemType)?.transactions?.some((transaction) => { + const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`; + return transactionKey === newSearchResultKey; + }); + // Determine if either the base key or any transaction key matches + const shouldAnimateInHighlight = isBaseKeyMatch || isAnyTransactionMatch; + + return mapToItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight); + }); const shouldShowEmptyState = !isDataLoaded || data.length === 0; @@ -299,6 +329,7 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr return ( + ref={handleSelectionListScroll(sortedSelectedData)} sections={[{data: sortedSelectedData, isDisabled: false}]} turnOnSelectionModeOnLongPress={type !== CONST.SEARCH.DATA_TYPES.CHAT} onTurnOnSelectionMode={(item) => item && toggleTransaction(item)} diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 31207fdbf1d7..5f43f8088fc3 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -4,11 +4,13 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useHover from '@hooks/useHover'; import {useMouseContext} from '@hooks/useMouseContext'; import useSyncFocus from '@hooks/useSyncFocus'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {BaseListItemProps, ListItem} from './types'; @@ -34,6 +36,7 @@ function BaseListItem({ onFocus = () => {}, hoverStyle, onLongPressRow, + hasAnimateInHighlightStyle = false, }: BaseListItemProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -61,6 +64,13 @@ function BaseListItem({ return rightHandSideComponent; }; + const animatedHighlightStyle = useAnimatedHighlightStyle({ + borderRadius: variables.componentBorderRadius, + shouldHighlight: item?.shouldAnimateInHighlight ?? false, + highlightColor: theme.messageHighlightBG, + backgroundColor: theme.highlightBG, + }); + return ( onDismissError(item)} @@ -99,6 +109,7 @@ function BaseListItem({ onFocus={onFocus} onMouseLeave={handleMouseLeave} tabIndex={item.tabIndex} + wrapperStyle={hasAnimateInHighlightStyle ? [styles.mh5, animatedHighlightStyle] : []} > {typeof children === 'function' ? children(hovered) : children} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index ed68df243f71..02564a69f0f1 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -619,7 +619,7 @@ function BaseSelectionList( [flattenedSections.allOptions, setFocusedIndex, updateAndScrollToFocusedIndex], ); - useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect}), [scrollAndHighlightItem, clearInputAfterSelect]); + useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect, scrollToIndex}), [scrollAndHighlightItem, clearInputAfterSelect, scrollToIndex]); /** Selects row when pressing Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx index 2c23c3ede4c5..ed3106e4a885 100644 --- a/src/components/SelectionList/Search/ReportListItem.tsx +++ b/src/components/SelectionList/Search/ReportListItem.tsx @@ -77,6 +77,9 @@ function ReportListItem({ styles.overflowHidden, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive, + // Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle + {backgroundColor: 'unset'}, + styles.mh0, ]; const handleOnButtonPress = () => { @@ -140,6 +143,7 @@ function ReportListItem({ onFocus={onFocus} shouldSyncFocus={shouldSyncFocus} hoverStyle={item.isSelected && styles.activeComponentBG} + hasAnimateInHighlightStyle > {!isLargeScreenWidth && ( diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index 5043d6f8f562..39172711516e 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -23,7 +23,16 @@ function TransactionListItem({ const {isLargeScreenWidth} = useResponsiveLayout(); - const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, styles.ph3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive]; + const listItemPressableStyle = [ + styles.selectionListPressableItemWrapper, + styles.pv3, + styles.ph3, + item.isSelected && styles.activeComponentBG, + isFocused && styles.sidebarLinkActive, + // Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle + {backgroundColor: 'unset'}, + styles.mh0, + ]; const listItemWrapperStyle = [ styles.flex1, @@ -50,6 +59,7 @@ function TransactionListItem({ onLongPressRow={onLongPressRow} shouldSyncFocus={shouldSyncFocus} hoverStyle={item.isSelected && styles.activeComponentBG} + hasAnimateInHighlightStyle > = CommonListItemProps & { children?: ReactElement> | ((hovered: boolean) => ReactElement>); shouldSyncFocus?: boolean; hoverStyle?: StyleProp; + hasAnimateInHighlightStyle?: boolean; + /** Errors that this user may contain */ shouldDisplayRBR?: boolean; }; @@ -565,6 +570,7 @@ type BaseSelectionListProps = Partial & { type SelectionListHandle = { scrollAndHighlightItem?: (items: string[], timeout: number) => void; clearInputAfterSelect?: () => void; + scrollToIndex: (index: number, animated?: boolean) => void; }; type ItemLayout = { diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts index dd2df58fb1e2..e17f30fc60bf 100644 --- a/src/hooks/useAnimatedHighlightStyle/index.ts +++ b/src/hooks/useAnimatedHighlightStyle/index.ts @@ -10,7 +10,7 @@ type Props = { borderRadius: number; /** Height of the item that is to be faded */ - height: number; + height?: number; /** Delay before the highlighted item enters */ itemEnterDelay?: number; @@ -32,6 +32,15 @@ type Props = { /** Whether the item should be highlighted */ shouldHighlight: boolean; + + /** The base backgroundColor used for the highlight animation, defaults to theme.appBG + * @default theme.appBG + */ + backgroundColor?: string; + /** The base highlightColor used for the highlight animation, defaults to theme.border + * @default theme.border + */ + highlightColor?: string; }; /** @@ -47,6 +56,8 @@ export default function useAnimatedHighlightStyle({ highlightEndDelay = CONST.ANIMATED_HIGHLIGHT_END_DELAY, highlightEndDuration = CONST.ANIMATED_HIGHLIGHT_END_DURATION, height, + highlightColor, + backgroundColor, }: Props) { const [startHighlight, setStartHighlight] = useState(false); const repeatableProgress = useSharedValue(0); @@ -55,8 +66,8 @@ export default function useAnimatedHighlightStyle({ const theme = useTheme(); const highlightBackgroundStyle = useAnimatedStyle(() => ({ - backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], [theme.appBG, theme.border]), - height: interpolate(nonRepeatableProgress.value, [0, 1], [0, height]), + backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], [backgroundColor ?? theme.appBG, highlightColor ?? theme.border]), + height: height ? interpolate(nonRepeatableProgress.value, [0, 1], [0, height]) : 'auto', opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]), borderRadius, })); diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts new file mode 100644 index 000000000000..12228bbe7b86 --- /dev/null +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -0,0 +1,177 @@ +import {useCallback, useEffect, useRef, useState} from 'react'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {SearchQueryJSON} from '@components/Search/types'; +import type {ReportActionListItemType, ReportListItemType, SelectionListHandle, TransactionListItemType} from '@components/SelectionList/types'; +import * as SearchActions from '@libs/actions/Search'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {SearchResults, Transaction} from '@src/types/onyx'; +import usePrevious from './usePrevious'; + +type UseSearchHighlightAndScroll = { + searchResults: OnyxEntry; + transactions: OnyxCollection; + previousTransactions: OnyxCollection; + queryJSON: SearchQueryJSON; + offset: number; +}; + +/** + * Hook used to trigger a search when a new transaction is added and handle highlighting and scrolling. + */ +function useSearchHighlightAndScroll({searchResults, transactions, previousTransactions, queryJSON, offset}: UseSearchHighlightAndScroll) { + // Ref to track if the search was triggered by this hook + const triggeredByHookRef = useRef(false); + const searchTriggeredRef = useRef(false); + const previousSearchResults = usePrevious(searchResults?.data); + const [newSearchResultKey, setNewSearchResultKey] = useState(null); + const highlightedTransactionIDs = useRef>(new Set()); + const initializedRef = useRef(false); + + // Trigger search when a new transaction is added + useEffect(() => { + const previousTransactionsLength = previousTransactions && Object.keys(previousTransactions).length; + const transactionsLength = transactions && Object.keys(transactions).length; + + // Return early if search was already triggered or there's no change in transactions length + if (searchTriggeredRef.current || previousTransactionsLength === transactionsLength) { + return; + } + + // Check if a new transaction was added + if (transactionsLength && typeof previousTransactionsLength === 'number' && transactionsLength > previousTransactionsLength) { + // Set the flag indicating the search is triggered by the hook + triggeredByHookRef.current = true; + + // Trigger the search + SearchActions.search({queryJSON, offset}); + + // Set the ref to prevent further triggers until reset + searchTriggeredRef.current = true; + } + + // Reset the ref when transactions are updated + return () => { + searchTriggeredRef.current = false; + }; + }, [transactions, previousTransactions, queryJSON, offset]); + + // Initialize the set with existing transaction IDs only once + useEffect(() => { + if (initializedRef.current || !searchResults?.data) { + return; + } + + const existingTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data); + highlightedTransactionIDs.current = new Set(existingTransactionIDs); + initializedRef.current = true; + }, [searchResults?.data]); + + // Detect new transactions + useEffect(() => { + if (!previousSearchResults || !searchResults?.data) { + return; + } + + const previousTransactionIDs = extractTransactionIDsFromSearchResults(previousSearchResults); + const currentTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data); + + // Find new transaction IDs that are not in the previousTransactionIDs and not already highlighted + const newTransactionIDs = currentTransactionIDs.filter((id) => !previousTransactionIDs.includes(id) && !highlightedTransactionIDs.current.has(id)); + + if (!triggeredByHookRef.current || newTransactionIDs.length === 0) { + return; + } + + const newTransactionID = newTransactionIDs[0]; + const newTransactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${newTransactionID}`; + + setNewSearchResultKey(newTransactionKey); + highlightedTransactionIDs.current.add(newTransactionID); + }, [searchResults, previousSearchResults]); + + // Reset newSearchResultKey after it's been used + useEffect(() => { + if (newSearchResultKey === null) { + return; + } + + const timer = setTimeout(() => { + setNewSearchResultKey(null); + }, CONST.ANIMATED_HIGHLIGHT_START_DURATION); + + return () => clearTimeout(timer); + }, [newSearchResultKey]); + + /** + * Callback to handle scrolling to the new search result. + */ + const handleSelectionListScroll = useCallback( + (data: Array) => (ref: SelectionListHandle | null) => { + // Early return if there's no ref, new transaction wasn't brought in by this hook + // or there's no new search result key + if (!ref || !triggeredByHookRef.current || newSearchResultKey === null) { + return; + } + + // Extract the transaction ID from the newSearchResultKey + const newTransactionID = newSearchResultKey.replace(ONYXKEYS.COLLECTION.TRANSACTION, ''); + + // Find the index of the new transaction in the data array + const indexOfNewTransaction = data.findIndex((item) => { + // Handle TransactionListItemType + if ('transactionID' in item && item.transactionID === newTransactionID) { + return true; + } + + // Handle ReportListItemType with transactions array + if ('transactions' in item && Array.isArray(item.transactions)) { + return item.transactions.some((transaction) => transaction?.transactionID === newTransactionID); + } + + return false; + }); + + // Early return if the transaction is not found in the data array + if (indexOfNewTransaction <= 0) { + return; + } + + // Perform the scrolling action + ref.scrollToIndex(indexOfNewTransaction); + // Reset the trigger flag to prevent unintended future scrolls and highlights + triggeredByHookRef.current = false; + }, + [newSearchResultKey], + ); + + return {newSearchResultKey, handleSelectionListScroll}; +} + +/** + * Helper function to extract transaction IDs from search results data. + */ +function extractTransactionIDsFromSearchResults(searchResultsData: Partial): string[] { + const transactionIDs: string[] = []; + + Object.values(searchResultsData).forEach((item) => { + // Check for transactionID directly on the item (TransactionListItemType) + if ((item as TransactionListItemType)?.transactionID) { + transactionIDs.push((item as TransactionListItemType).transactionID); + } + + // Check for transactions array within the item (ReportListItemType) + if (Array.isArray((item as ReportListItemType)?.transactions)) { + (item as ReportListItemType).transactions.forEach((transaction) => { + if (!transaction?.transactionID) { + return; + } + transactionIDs.push(transaction.transactionID); + }); + } + }); + + return transactionIDs; +} + +export default useSearchHighlightAndScroll; diff --git a/tests/actions/OnyxUpdateManagerTest.ts b/tests/actions/OnyxUpdateManagerTest.ts index 3a4ff0779217..e1008064163b 100644 --- a/tests/actions/OnyxUpdateManagerTest.ts +++ b/tests/actions/OnyxUpdateManagerTest.ts @@ -27,6 +27,12 @@ jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates', () => { }; }); +jest.mock('@hooks/useScreenWrapperTransitionStatus', () => ({ + default: () => ({ + didScreenTransitionEnd: true, + }), +})); + const App = AppImport as AppActionsMock; const ApplyUpdates = ApplyUpdatesImport as ApplyUpdatesMock; const OnyxUpdateManagerUtils = OnyxUpdateManagerUtilsImport as OnyxUpdateManagerUtilsMock; diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index a099348257f0..03e844332c58 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -30,6 +30,12 @@ jest.mock('@src/libs/actions/Report', () => { }; }); +jest.mock('@hooks/useScreenWrapperTransitionStatus', () => ({ + default: () => ({ + didScreenTransitionEnd: true, + }), +})); + OnyxUpdateManager(); describe('actions/Report', () => { beforeAll(() => { diff --git a/tests/perf-test/SelectionList.perf-test.tsx b/tests/perf-test/SelectionList.perf-test.tsx index 07a9a4d6ce22..7552f9347862 100644 --- a/tests/perf-test/SelectionList.perf-test.tsx +++ b/tests/perf-test/SelectionList.perf-test.tsx @@ -2,7 +2,7 @@ import {fireEvent} from '@testing-library/react-native'; import type {RenderResult} from '@testing-library/react-native'; import React, {useState} from 'react'; import type {ComponentType} from 'react'; -import {measurePerformance} from 'reassure'; +import {measureRenders} from 'reassure'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import type {ListItem} from '@components/SelectionList/types'; @@ -76,6 +76,14 @@ jest.mock('../../src/hooks/useKeyboardState', () => ({ })), })); +jest.mock('../../src/hooks/useScreenWrapperTransitionStatus', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: jest.fn(() => ({ + didScreenTransitionEnd: true, + })), +})); + function SelectionListWrapper({canSelectMultiple}: SelectionListWrapperProps) { const [selectedIds, setSelectedIds] = useState([]); @@ -119,7 +127,7 @@ function SelectionListWrapper({canSelectMultiple}: SelectionListWrapperProps) { } test('[SelectionList] should render 1 section and a thousand items', () => { - measurePerformance(); + measureRenders(); }); test('[SelectionList] should press a list item', () => { @@ -128,7 +136,7 @@ test('[SelectionList] should press a list item', () => { fireEvent.press(screen.getByText('Item 5')); }; - measurePerformance(, {scenario}); + measureRenders(, {scenario}); }); test('[SelectionList] should render multiple selection and select 3 items', () => { @@ -139,7 +147,7 @@ test('[SelectionList] should render multiple selection and select 3 items', () = fireEvent.press(screen.getByText('Item 3')); }; - measurePerformance(, {scenario}); + measureRenders(, {scenario}); }); test('[SelectionList] should scroll and select a few items', () => { @@ -171,5 +179,5 @@ test('[SelectionList] should scroll and select a few items', () => { fireEvent.press(screen.getByText('Item 15')); }; - measurePerformance(, {scenario}); + measureRenders(, {scenario}); }); diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index 2cfd1eec10c7..7b0446641458 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -13,6 +13,12 @@ jest.mock('@libs/actions/App'); jest.mock('@libs/actions/OnyxUpdateManager/utils'); jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); +jest.mock('@hooks/useScreenWrapperTransitionStatus', () => ({ + default: () => ({ + didScreenTransitionEnd: true, + }), +})); + const App = AppImport as AppActionsMock; const ApplyUpdates = ApplyUpdatesImport as ApplyUpdatesMock; const OnyxUpdateManagerUtils = OnyxUpdateManagerUtilsImport as OnyxUpdateManagerUtilsMock;