diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index 64b4536d9241..ebe31f41f3d8 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -33,6 +33,7 @@ jobs: npx reassure --baseline git switch --force --detach - git merge --no-commit --allow-unrelated-histories "$BASELINE_BRANCH" -X ours + git checkout --ours . npm install --force npx reassure --branch diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 9eb16099f535..25f54c668b24 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -123,7 +123,7 @@ Additionally if you want to discuss an idea with the open source community witho ``` 11. [Open a pull request](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork), and make sure to fill in the required fields. 12. An Expensify engineer and a member from the Contributor-Plus team will be assigned to your pull request automatically to review. -13. Daily updates on weekdays are highly recommended. If you know you won’t be able to provide updates for > 1 week, please comment on the PR or issue how long you plan to be out so that we may plan accordingly. We understand everyone needs a little vacation here and there. Any issue that doesn't receive an update for 1 full week may be considered abandoned and the original contract terminated. +13. Daily updates on weekdays are highly recommended. If you know you won’t be able to provide updates within 48 hours, please comment on the PR or issue stating how long you plan to be out so that we may plan accordingly. We understand everyone needs a little vacation here and there. Any issue that doesn't receive an update for 5 days (including weekend days) may be considered abandoned and the original contract terminated. #### Submit your pull request for final review 14. When you are ready to submit your pull request for final review, make sure the following checks pass: diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md index 30adac589dc0..b3f0ad3c6f6f 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md @@ -44,7 +44,7 @@ Expensify’s Budgets feature allows you to: {% include faq-begin.md %} ## Can I import budgets as a CSV? -At this time, you cannot import budgets via CSV since we don’t import categories or tags from direct accounting integrations. +At this time, you cannot import budgets via CSV. ## When will I be notified as a budget is hit? Notifications are sent twice: diff --git a/src/CONST.ts b/src/CONST.ts index ff3934c31943..5625381d2869 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3057,13 +3057,6 @@ const CONST = { */ MAX_OPTIONS_SELECTOR_PAGE_LENGTH: 500, - /** - * Performance test setup - run the same test multiple times to get a more accurate result - */ - PERFORMANCE_TESTS: { - RUNS: 20, - }, - /** * Bank account names */ diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index d97c47c84ee7..1d73873d836b 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -363,6 +363,11 @@ function BaseSelectionList( return; } + // scroll is unnecessary if multiple options cannot be selected + if (!canSelectMultiple) { + return; + } + // set the focus on the first item when the sections list is changed if (sections.length > 0) { updateAndScrollToFocusedIndex(0); diff --git a/src/hooks/useResponsiveLayout.ts b/src/hooks/useResponsiveLayout.ts index dd782a9dbba5..f00890116d47 100644 --- a/src/hooks/useResponsiveLayout.ts +++ b/src/hooks/useResponsiveLayout.ts @@ -1,25 +1,21 @@ -import type {ParamListBase, RouteProp} from '@react-navigation/native'; -import {useRoute} from '@react-navigation/native'; +import {navigationRef} from '@libs/Navigation/Navigation'; +import NAVIGATORS from '@src/NAVIGATORS'; import useWindowDimensions from './useWindowDimensions'; -type RouteParams = ParamListBase & { - params: {isInRHP?: boolean}; -}; type ResponsiveLayoutResult = { shouldUseNarrowLayout: boolean; + isSmallScreenWidth: boolean; + isInModal: boolean; }; /** - * Hook to determine if we are on mobile devices or in the RHP + * Hook to determine if we are on mobile devices or in the Modal Navigator */ export default function useResponsiveLayout(): ResponsiveLayoutResult { const {isSmallScreenWidth} = useWindowDimensions(); - try { - // eslint-disable-next-line react-hooks/rules-of-hooks - const {params} = useRoute>(); - return {shouldUseNarrowLayout: isSmallScreenWidth || (params?.isInRHP ?? false)}; - } catch (error) { - return { - shouldUseNarrowLayout: isSmallScreenWidth, - }; - } + const state = navigationRef?.getRootState(); + const lastRoute = state?.routes?.at(-1); + const lastRouteName = lastRoute?.name; + const isInModal = lastRouteName === NAVIGATORS.LEFT_MODAL_NAVIGATOR || lastRouteName === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; + const shouldUseNarrowLayout = isSmallScreenWidth || isInModal; + return {shouldUseNarrowLayout, isSmallScreenWidth, isInModal}; } diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 32ebca9afee8..94bba5d0d00c 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -14,6 +14,14 @@ function insertText(text: string, selection: Selection, textToInsert: string): s return text.slice(0, selection.start) + textToInsert + text.slice(selection.end, text.length); } +/** + * Insert a white space at given index of text + * @param text - text that needs whitespace to be appended to + */ +function insertWhiteSpaceAtIndex(text: string, index: number) { + return `${text.slice(0, index)} ${text.slice(index)}`; +} + /** * Check whether we can skip trigger hotkeys on some specific devices. */ @@ -23,4 +31,22 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo return (isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen()) || isKeyboardShown; } -export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys}; +/** + * Finds the length of common suffix between two texts + */ +function findCommonSuffixLength(str1: string, str2: string, cursorPosition: number) { + let commonSuffixLength = 0; + const minLength = Math.min(str1.length - cursorPosition, str2.length); + + for (let i = 1; i <= minLength; i++) { + if (str1.charAt(str1.length - i) === str2.charAt(str2.length - i)) { + commonSuffixLength++; + } else { + break; + } + } + + return commonSuffixLength; +} + +export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys, insertWhiteSpaceAtIndex, findCommonSuffixLength}; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 2621e4d7f12b..6332a57deec0 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1563,7 +1563,7 @@ function getOptions( if (includePersonalDetails) { // Next loop over all personal details removing any that are selectedUsers or recentChats allPersonalDetailsOptions.forEach((personalDetailOption) => { - if (optionsToExclude.some((optionToExclude) => optionToExclude.login === personalDetailOption.login)) { + if (optionsToExclude.some((optionToExclude) => optionToExclude.login === addSMSDomainIfPhoneNumber(personalDetailOption.login ?? ''))) { return; } const {searchText, participantsList, isChatRoom} = personalDetailOption; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index eb9541edcad2..e6aa9398c05b 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -311,6 +311,29 @@ function getReceiptError(receipt, filename, isScanRequest = true) { : ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source, filename}); } +/** + * Return the object to update hasOutstandingChildRequest + * @param {Object} [policy] + * @param {Boolean} needsToBeManuallySubmitted + * @returns {Object} + */ +function getOutstandingChildRequest(policy, needsToBeManuallySubmitted) { + if (!needsToBeManuallySubmitted) { + return { + hasOutstandingChildRequest: false, + }; + } + + if (PolicyUtils.isPolicyAdmin(policy)) { + return { + hasOutstandingChildRequest: true, + }; + } + + // We don't need to update hasOutstandingChildRequest in this case + return {}; +} + /** * Builds the Onyx data for a money request. * @@ -329,7 +352,7 @@ function getReceiptError(receipt, filename, isScanRequest = true) { * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) * @param {Array} policyTags * @param {Array} policyCategories - * @param {Boolean} hasOutstandingChildRequest + * @param {Boolean} needsToBeManuallySubmitted * @returns {Array} - An array containing the optimistic data, success data, and failure data. */ function buildOnyxDataForMoneyRequest( @@ -348,9 +371,10 @@ function buildOnyxDataForMoneyRequest( policy, policyTags, policyCategories, - hasOutstandingChildRequest = false, + needsToBeManuallySubmitted = true, ) { const isScanRequest = TransactionUtils.isScanRequest(transaction); + const outstandingChildRequest = getOutstandingChildRequest(needsToBeManuallySubmitted, policy); const optimisticData = [ { // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page @@ -361,7 +385,7 @@ function buildOnyxDataForMoneyRequest( lastReadTime: DateUtils.getDBTime(), lastMessageTranslationKey: '', iouReportID: iouReport.reportID, - hasOutstandingChildRequest, + ...outstandingChildRequest, ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), }, }, @@ -506,6 +530,7 @@ function buildOnyxDataForMoneyRequest( iouReportID: chatReport.iouReportID, lastReadTime: chatReport.lastReadTime, pendingFields: null, + hasOutstandingChildRequest: chatReport.hasOutstandingChildRequest, ...(isNewChatReport ? { errorFields: { @@ -687,7 +712,7 @@ function getMoneyRequestInformation( let iouReport = isNewIOUReport ? null : allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; // Check if the Scheduled Submit is enabled in case of expense report - let needsToBeManuallySubmitted = false; + let needsToBeManuallySubmitted = true; let isFromPaidPolicy = false; if (isPolicyExpenseChat) { isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy); @@ -806,10 +831,6 @@ function getMoneyRequestInformation( } : undefined; - // The policy expense chat should have the GBR only when its a paid policy and the scheduled submit is turned off - // so the employee has to submit to their manager manually. - const hasOutstandingChildRequest = isPolicyExpenseChat && needsToBeManuallySubmitted; - // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( chatReport, @@ -827,7 +848,7 @@ function getMoneyRequestInformation( policy, policyTags, policyCategories, - hasOutstandingChildRequest, + needsToBeManuallySubmitted, ); return { diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 3b6617aa3ed0..0f1e383522eb 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -1,5 +1,5 @@ -/* eslint-disable no-console */ -import type {View} from 'react-native'; +/* eslint-disable no-restricted-imports */ +import type {Text as RNText, View} from 'react-native'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; @@ -13,7 +13,7 @@ type AnchorOrigin = { /** * Gets the x,y position of the passed in component for the purpose of anchoring another component to it. */ -export default function calculateAnchorPosition(anchorComponent: View, anchorOrigin?: AnchorOrigin): Promise { +export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise { return new Promise((resolve) => { if (!anchorComponent) { return resolve({horizontal: 0, vertical: 0}); diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index 3f55ad6c0ff7..588a90e98649 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -77,7 +77,10 @@ function RoomInvitePage(props) { // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers = useMemo( - () => [...PersonalDetailsUtils.getLoginsByAccountIDs(lodashGet(props.report, 'visibleChatMemberAccountIDs', [])), ...CONST.EXPENSIFY_EMAILS], + () => + _.map([...PersonalDetailsUtils.getLoginsByAccountIDs(lodashGet(props.report, 'visibleChatMemberAccountIDs', [])), ...CONST.EXPENSIFY_EMAILS], (participant) => + OptionsListUtils.addSMSDomainIfPhoneNumber(participant), + ), [props.report], ); diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 3eecb74a048a..213d94f51f81 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -2,6 +2,8 @@ import lodashIsEqual from 'lodash/isEqual'; import type {MutableRefObject, RefObject} from 'react'; import React, {memo, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {GestureResponderEvent, Text as RNText, View as ViewType} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ContextMenuItemHandle} from '@components/ContextMenuItem'; @@ -12,15 +14,16 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as ReportUtils from '@libs/ReportUtils'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, ReportAction, ReportActions} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {ContextMenuActionPayload} from './ContextMenuActions'; +import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions'; import ContextMenuActions from './ContextMenuActions'; import type {ContextMenuType} from './ReportActionContextMenu'; -import {hideContextMenu} from './ReportActionContextMenu'; +import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuOnyxProps = { /** Beta features list */ @@ -78,7 +81,11 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { /** Content Ref */ contentRef?: RefObject; + /** Function to check if context menu is active */ checkIfContextMenuActive?: () => void; + + /** List of disabled actions */ + disabledActions?: ContextMenuAction[]; }; type MenuItemRefs = Record; @@ -100,6 +107,7 @@ function BaseReportActionContextMenu({ betas, reportActions, checkIfContextMenuActive, + disabledActions = [], }: BaseReportActionContextMenuProps) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -117,13 +125,22 @@ function BaseReportActionContextMenu({ }, [reportActions, reportActionID]); const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); - let filteredContextMenuActions = ContextMenuActions.filter((contextAction) => - contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini), + let filteredContextMenuActions = ContextMenuActions.filter( + (contextAction) => + !disabledActions.includes(contextAction) && + contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini), ); - filteredContextMenuActions = - isMini && filteredContextMenuActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS - ? ([...filteredContextMenuActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), filteredContextMenuActions.at(-1)] as typeof filteredContextMenuActions) - : filteredContextMenuActions; + + if (isMini) { + const menuAction = filteredContextMenuActions.at(-1); + const otherActions = filteredContextMenuActions.slice(0, -1); + if (otherActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS && menuAction) { + filteredContextMenuActions = otherActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1); + filteredContextMenuActions.push(menuAction); + } else { + filteredContextMenuActions = otherActions; + } + } // Context menu actions that are not rendered as menu items are excluded from arrow navigation const nonMenuItemActionIndexes = filteredContextMenuActions.map((contextAction, index) => @@ -172,6 +189,28 @@ function BaseReportActionContextMenu({ {isActive: shouldEnableArrowNavigation}, ); + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent) => { + const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); + const originalReport = ReportUtils.getReport(originalReportID); + showContextMenu( + CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, + event, + selection, + anchor?.current as ViewType | RNText | null, + reportID, + reportAction?.reportActionID, + originalReportID, + draftMessage, + checkIfContextMenuActive, + checkIfContextMenuActive, + ReportUtils.isArchivedRoom(originalReport), + ReportUtils.chatIncludesChronos(originalReport), + undefined, + undefined, + filteredContextMenuActions, + ); + }; + return ( (isVisible || shouldKeepOpen) && ( setShouldKeepOpen(false), openContextMenu: () => setShouldKeepOpen(true), interceptAnonymousUser, - anchor, - checkIfContextMenuActive, + openOverflowMenu, }; if ('renderContent' in contextAction) { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index ea25a00ee1d3..95300d4ed3f4 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -1,8 +1,7 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import type {MutableRefObject} from 'react'; import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import type {GestureResponderEvent, Text as RNText, View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -29,7 +28,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import type {Beta, ReportAction, ReportActionReactions} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import {hideContextMenu, showContextMenu, showDeleteModal} from './ReportActionContextMenu'; +import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; /** Gets the HTML version of the message in an action */ function getActionText(reportAction: OnyxEntry): string { @@ -70,8 +69,7 @@ type ContextMenuActionPayload = { close: () => void; openContextMenu: () => void; interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; - anchor?: MutableRefObject; - checkIfContextMenuActive?: () => void; + openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void; event?: GestureResponderEvent | MouseEvent | KeyboardEvent; }; @@ -240,7 +238,7 @@ const ContextMenuActions: ContextMenuAction[] = [ { isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.markAsUnread', - icon: Expensicons.Mail, + icon: Expensicons.ChatBubbleUnread, successIcon: Expensicons.Checkmark, shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat), @@ -502,27 +500,12 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.menu', icon: Expensicons.ThreeDots, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline, isMini) => isMini, - onPress: (closePopover, {reportAction, reportID, event, anchor, selection, draftMessage, checkIfContextMenuActive}) => { - const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); - const originalReport = ReportUtils.getReport(originalReportID); - showContextMenu( - CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - event as GestureResponderEvent | MouseEvent, - selection, - anchor?.current as View | RNText | null, - reportID, - reportAction.reportActionID, - originalReportID, - draftMessage, - checkIfContextMenuActive, - checkIfContextMenuActive, - ReportUtils.isArchivedRoom(originalReport), - ReportUtils.chatIncludesChronos(originalReport), - ); + onPress: (closePopover, {openOverflowMenu, event}) => { + openOverflowMenu(event as GestureResponderEvent | MouseEvent); }, getDescription: () => {}, }, ]; export default ContextMenuActions; -export type {ContextMenuActionPayload}; +export type {ContextMenuActionPayload, ContextMenuAction}; diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 46b783bca3f9..b28374fae04a 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,23 +1,23 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; + +/* eslint-disable no-restricted-imports */ +import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, Text as RNText, View} from 'react-native'; import {Dimensions} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import useLocalize from '@hooks/useLocalize'; +import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; +import type {ContextMenuAction} from './ContextMenuActions'; import type {ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; -type ContextMenuAnchorCallback = (x: number, y: number) => void; - -type ContextMenuAnchor = {measureInWindow: (callback: ContextMenuAnchorCallback) => void}; - type Location = { x: number; y: number; @@ -62,11 +62,12 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef([]); const contentRef = useRef(null); const anchorRef = useRef(null); const dimensionsEventListener = useRef(null); - const contextMenuAnchorRef = useRef(null); + const contextMenuAnchorRef = useRef(null); const contextMenuTargetNode = useRef(null); const onPopoverShow = useRef(() => {}); @@ -161,6 +162,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; @@ -171,16 +173,27 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { - popoverAnchorPosition.current = { - horizontal: pageX - x, - vertical: pageY - y, - }; - - popoverAnchorPosition.current = { - horizontal: pageX, - vertical: pageY, - }; + new Promise((resolve) => { + if (!pageX && !pageY && contextMenuAnchorRef.current) { + calculateAnchorPosition(contextMenuAnchorRef.current).then((position) => { + popoverAnchorPosition.current = position; + resolve(); + }); + } else { + getContextMenuMeasuredLocation().then(({x, y}) => { + cursorRelativePosition.current = { + horizontal: pageX - x, + vertical: pageY - y, + }; + popoverAnchorPosition.current = { + horizontal: pageX, + vertical: pageY, + }; + resolve(); + }); + } + }).then(() => { + setDisabledActions(disabledOptions); typeRef.current = type; reportIDRef.current = reportID ?? '0'; reportActionIDRef.current = reportActionID ?? '0'; @@ -310,6 +323,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef void; @@ -30,6 +31,7 @@ type ShowContextMenu = ( isChronosReport?: boolean, isPinnedChat?: boolean, isUnreadChat?: boolean, + disabledOptions?: ContextMenuAction[], ) => void; type ReportActionContextMenu = { @@ -108,6 +110,7 @@ function showContextMenu( isChronosReport = false, isPinnedChat = false, isUnreadChat = false, + disabledActions: ContextMenuAction[] = [], ) { if (!contextMenuRef.current) { return; @@ -134,6 +137,7 @@ function showContextMenu( isChronosReport, isPinnedChat, isUnreadChat, + disabledActions, ); } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index a98a717e13d0..90c2ba0b42cf 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -122,6 +122,7 @@ function ComposerWithSuggestions({ return draft; }); const commentRef = useRef(value); + const lastTextRef = useRef(value); const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; @@ -189,6 +190,47 @@ function ComposerWithSuggestions({ [], ); + /** + * Find the newly added characters between the previous text and the new text based on the selection. + * + * @param {string} prevText - The previous text. + * @param {string} newText - The new text. + * @returns {object} An object containing information about the newly added characters. + * @property {number} startIndex - The start index of the newly added characters in the new text. + * @property {number} endIndex - The end index of the newly added characters in the new text. + * @property {string} diff - The newly added characters. + */ + const findNewlyAddedChars = useCallback( + (prevText, newText) => { + let startIndex = -1; + let endIndex = -1; + let currentIndex = 0; + + // Find the first character mismatch with newText + while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText.charAt(currentIndex) && selection.start > currentIndex) { + currentIndex++; + } + + if (currentIndex < newText.length) { + startIndex = currentIndex; + const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText, selection.end); + // if text is getting pasted over find length of common suffix and subtract it from new text length + if (commonSuffixLength > 0 || selection.end - selection.start > 0) { + endIndex = newText.length - commonSuffixLength; + } else { + endIndex = currentIndex + newText.length; + } + } + + return { + startIndex, + endIndex, + diff: newText.substring(startIndex, endIndex), + }; + }, + [selection.start, selection.end], + ); + /** * Update the value of the comment in Onyx * @@ -198,7 +240,13 @@ function ComposerWithSuggestions({ const updateComment = useCallback( (commentValue, shouldDebounceSaveComment) => { raiseIsScrollLikelyLayoutTriggered(); - const {text: newComment, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale); + const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); + const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); + const { + text: newComment, + emojis, + cursorPosition, + } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue, preferredSkinTone, preferredLocale); if (!_.isEmpty(emojis)) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (!_.isEmpty(newEmojis)) { @@ -255,6 +303,7 @@ function ComposerWithSuggestions({ }, [ debouncedUpdateFrequentlyUsedEmojis, + findNewlyAddedChars, preferredLocale, preferredSkinTone, reportID, @@ -309,17 +358,10 @@ function ComposerWithSuggestions({ /** * Callback to add whatever text is chosen into the main input (used f.e as callback for the emoji picker) * @param {String} text - * @param {Boolean} shouldAddTrailSpace */ const replaceSelectionWithText = useCallback( - (text, shouldAddTrailSpace = true) => { - const updatedText = shouldAddTrailSpace ? `${text} ` : text; - const selectionSpaceLength = shouldAddTrailSpace ? CONST.SPACE_LENGTH : 0; - updateComment(ComposerUtils.insertText(commentRef.current, selection, updatedText)); - setSelection((prevSelection) => ({ - start: prevSelection.start + text.length + selectionSpaceLength, - end: prevSelection.start + text.length + selectionSpaceLength, - })); + (text) => { + updateComment(ComposerUtils.insertText(commentRef.current, selection, text)); }, [selection, updateComment], ); @@ -524,6 +566,10 @@ function ComposerWithSuggestions({ [blur, focus, prepareCommentAndResetComposer, replaceSelectionWithText], ); + useEffect(() => { + lastTextRef.current = value; + }, [value]); + useEffect(() => { onValueChange(value); }, [onValueChange, value]); diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index d68fbf889f29..5f0e3ea4b72c 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -9,7 +9,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; -import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; +import type {OriginalMessageAddComment} from '@src/types/onyx/OriginalMessage'; import TextCommentFragment from './comment/TextCommentFragment'; import ReportActionItemFragment from './ReportActionItemFragment'; @@ -78,7 +78,7 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid iouMessage={iouMessage} isThreadParentMessage={ReportActionsUtils.isThreadParentMessage(action, reportID)} pendingAction={action.pendingAction} - source={action.originalMessage as OriginalMessageSource} + source={(action.originalMessage as OriginalMessageAddComment['originalMessage'])?.source} accountID={action.actorAccountID ?? 0} style={style} displayAsGroup={displayAsGroup} diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index be3d6ad50eeb..15f98205839e 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -17,6 +17,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -54,8 +55,8 @@ const propTypes = { /** The request type, ie. manual, scan, distance */ iouRequestType: PropTypes.oneOf(_.values(CONST.IOU.REQUEST_TYPE)).isRequired, - /** Whether we are searching for reports in the server */ - isSearchingForReports: PropTypes.bool, + /** Whether the parent screen transition has ended */ + didScreenTransitionEnd: PropTypes.bool, }; const defaultProps = { @@ -63,7 +64,7 @@ const defaultProps = { safeAreaPaddingBottomStyle: {}, reports: {}, betas: [], - isSearchingForReports: false, + didScreenTransitionEnd: false, }; function MoneyTemporaryForRefactorRequestParticipantsSelector({ @@ -75,7 +76,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ safeAreaPaddingBottomStyle, iouType, iouRequestType, - isSearchingForReports, + didScreenTransitionEnd, }) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -95,6 +96,9 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ */ const [sections, newChatOptions] = useMemo(() => { const newSections = []; + if (!didScreenTransitionEnd) { + return [newSections, {}]; + } let indexOffset = 0; const chatOptions = OptionsListUtils.getFilteredOptions( @@ -169,7 +173,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, translate]); + }, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, translate]); /** * Adds a single participant to the request @@ -328,11 +332,13 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [addParticipantToSelection, isAllowedToSplit, styles, translate], ); + const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); + return ( 0 ? safeAreaPaddingBottomStyle : {}]}> @@ -359,8 +365,4 @@ export default withOnyx({ betas: { key: ONYXKEYS.BETAS, }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, })(MoneyTemporaryForRefactorRequestParticipantsSelector); diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 730441035d08..4846c3c4c8a4 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -87,13 +87,16 @@ function IOURequestStepParticipants({ testID={IOURequestStepParticipants.displayName} includeSafeAreaPaddingBottom > - + {({didScreenTransitionEnd}) => ( + + )} ); } diff --git a/tests/perf-test/ModifiedExpenseMessage.perf-test.ts b/tests/perf-test/ModifiedExpenseMessage.perf-test.ts index 5aa842155cb5..6e9ab5394cf9 100644 --- a/tests/perf-test/ModifiedExpenseMessage.perf-test.ts +++ b/tests/perf-test/ModifiedExpenseMessage.perf-test.ts @@ -11,8 +11,6 @@ import createRandomReportAction from '../utils/collections/reportActions'; import createRandomReport from '../utils/collections/reports'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -const runs = CONST.PERFORMANCE_TESTS.RUNS; - beforeAll(() => Onyx.init({ keys: ONYXKEYS, @@ -39,10 +37,10 @@ const getMockedPolicies = (length = 500) => length, ); -const mockedReportsMap = getMockedReports(5000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>; -const mockedPoliciesMap = getMockedPolicies(5000) as Record<`${typeof ONYXKEYS.COLLECTION.POLICY}`, Policy>; +const mockedReportsMap = getMockedReports(1000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>; +const mockedPoliciesMap = getMockedPolicies(1000) as Record<`${typeof ONYXKEYS.COLLECTION.POLICY}`, Policy>; -test('[ModifiedExpenseMessage] getForReportAction on 5k reports and policies', async () => { +test('[ModifiedExpenseMessage] getForReportAction on 1k reports and policies', async () => { const reportAction = { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, @@ -60,5 +58,5 @@ test('[ModifiedExpenseMessage] getForReportAction on 5k reports and policies', a }); await waitForBatchedUpdates(); - await measureFunction(() => ModifiedExpenseMessage.getForReportAction(reportAction), {runs}); + await measureFunction(() => ModifiedExpenseMessage.getForReportAction(reportAction)); }); diff --git a/tests/perf-test/OptionsSelector.perf-test.js b/tests/perf-test/OptionsSelector.perf-test.js index b30169b8b53f..6104ded05c6a 100644 --- a/tests/perf-test/OptionsSelector.perf-test.js +++ b/tests/perf-test/OptionsSelector.perf-test.js @@ -3,7 +3,6 @@ import React from 'react'; import {measurePerformance} from 'reassure'; import _ from 'underscore'; import OptionsSelector from '@src/components/OptionsSelector'; -import CONST from '@src/CONST'; import variables from '@src/styles/variables'; jest.mock('../../src/components/withLocalize', () => (Component) => { @@ -65,8 +64,6 @@ function OptionsSelectorWrapper(args) { ); } -const runs = CONST.PERFORMANCE_TESTS.RUNS; - test('[OptionsSelector] should render text input with interactions', () => { const scenario = (screen) => { const textInput = screen.getByTestId('options-selector-input'); @@ -75,16 +72,16 @@ test('[OptionsSelector] should render text input with interactions', () => { fireEvent.changeText(textInput, 'test3'); }; - measurePerformance(, {scenario, runs}); + measurePerformance(, {scenario}); }); test('[OptionsSelector] should render 1 section', () => { - measurePerformance(, {runs}); + measurePerformance(); }); test('[OptionsSelector] should render multiple sections', () => { const sections = generateSections(mutlipleSectionsConfig); - measurePerformance(, {runs}); + measurePerformance(); }); test('[OptionsSelector] should press a list items', () => { @@ -94,7 +91,7 @@ test('[OptionsSelector] should press a list items', () => { fireEvent.press(screen.getByText('Item 10')); }; - measurePerformance(, {scenario, runs}); + measurePerformance(, {scenario}); }); test('[OptionsSelector] should scroll and press few items', () => { @@ -126,5 +123,5 @@ test('[OptionsSelector] should scroll and press few items', () => { fireEvent.press(screen.getByText('Item 200')); }; - measurePerformance(, {scenario, runs}); + measurePerformance(, {scenario}); }); diff --git a/tests/perf-test/ReportActionCompose.perf-test.js b/tests/perf-test/ReportActionCompose.perf-test.js index de34c139a361..27d83cb885da 100644 --- a/tests/perf-test/ReportActionCompose.perf-test.js +++ b/tests/perf-test/ReportActionCompose.perf-test.js @@ -7,7 +7,6 @@ import {LocaleContextProvider} from '../../src/components/LocaleContextProvider' import OnyxProvider from '../../src/components/OnyxProvider'; 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 ReportActionCompose from '../../src/pages/home/report/ReportActionCompose/ReportActionCompose'; @@ -51,6 +50,22 @@ jest.mock('../../src/libs/actions/EmojiPickerAction', () => { }; }); +jest.mock('../../src/components/withNavigationFocus', () => (Component) => { + function WithNavigationFocus(props) { + return ( + + ); + } + + WithNavigationFocus.displayName = 'WithNavigationFocus'; + + return WithNavigationFocus; +}); + beforeAll(() => Onyx.init({ keys: ONYXKEYS, @@ -64,8 +79,6 @@ beforeEach(() => { Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); }); -const runs = CONST.PERFORMANCE_TESTS.RUNS; - function ReportActionComposeWrapper() { return ( @@ -96,7 +109,7 @@ test('[ReportActionCompose] should render Composer with text input interactions' fireEvent.press(composer); }; - return waitForBatchedUpdates().then(() => measurePerformance(, {scenario, runs})); + return waitForBatchedUpdates().then(() => measurePerformance(, {scenario})); }); test('[ReportActionCompose] should press add attachemnt button', async () => { @@ -108,7 +121,7 @@ test('[ReportActionCompose] should press add attachemnt button', async () => { fireEvent.press(attachmentButton, mockEvent); }; - return waitForBatchedUpdates().then(() => measurePerformance(, {scenario, runs})); + return waitForBatchedUpdates().then(() => measurePerformance(, {scenario})); }); test('[ReportActionCompose] should press add emoji button', async () => { @@ -120,7 +133,7 @@ test('[ReportActionCompose] should press add emoji button', async () => { fireEvent.press(emojiButton); }; - return waitForBatchedUpdates().then(() => measurePerformance(, {scenario, runs})); + return waitForBatchedUpdates().then(() => measurePerformance(, {scenario})); }); test('[ReportActionCompose] should press send message button', async () => { @@ -132,7 +145,7 @@ test('[ReportActionCompose] should press send message button', async () => { fireEvent.press(sendButton); }; - return waitForBatchedUpdates().then(() => measurePerformance(, {scenario, runs})); + return waitForBatchedUpdates().then(() => measurePerformance(, {scenario})); }); test('[ReportActionCompose] render composer with attachement modal interactions', async () => { @@ -152,5 +165,5 @@ test('[ReportActionCompose] render composer with attachement modal interactions' fireEvent.press(assignTaskButton, mockEvent); }; - return waitForBatchedUpdates().then(() => measurePerformance(, {scenario, runs})); + return waitForBatchedUpdates().then(() => measurePerformance(, {scenario})); }); diff --git a/tests/perf-test/ReportActionsList.perf-test.js b/tests/perf-test/ReportActionsList.perf-test.js index 8e3312cfa4c7..34b127c217e4 100644 --- a/tests/perf-test/ReportActionsList.perf-test.js +++ b/tests/perf-test/ReportActionsList.perf-test.js @@ -5,7 +5,6 @@ import ComposeProviders from '../../src/components/ComposeProviders'; import {LocaleContextProvider} from '../../src/components/LocaleContextProvider'; import OnyxProvider from '../../src/components/OnyxProvider'; import {WindowDimensionsProvider} from '../../src/components/withWindowDimensions'; -import CONST from '../../src/CONST'; import * as Localize from '../../src/libs/Localize'; import ONYXKEYS from '../../src/ONYXKEYS'; import ReportActionsList from '../../src/pages/home/report/ReportActionsList'; @@ -97,8 +96,6 @@ function ReportActionsListWrapper() { ); } -const runs = CONST.PERFORMANCE_TESTS.RUNS; - test('[ReportActionsList] should render ReportActionsList with 500 reportActions stored', () => { const scenario = async () => { await screen.findByTestId('report-actions-list'); @@ -113,7 +110,7 @@ test('[ReportActionsList] should render ReportActionsList with 500 reportActions [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, }), ) - .then(() => measurePerformance(, {scenario, runs})); + .then(() => measurePerformance(, {scenario})); }); test('[ReportActionsList] should scroll and click some of the reports', () => { @@ -151,5 +148,5 @@ test('[ReportActionsList] should scroll and click some of the reports', () => { [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, }), ) - .then(() => measurePerformance(, {scenario, runs})); + .then(() => measurePerformance(, {scenario})); }); diff --git a/tests/perf-test/ReportActionsUtils.perf-test.ts b/tests/perf-test/ReportActionsUtils.perf-test.ts index 2a96a5959942..f194cd32bbf4 100644 --- a/tests/perf-test/ReportActionsUtils.perf-test.ts +++ b/tests/perf-test/ReportActionsUtils.perf-test.ts @@ -35,8 +35,6 @@ const reportActions = createCollection( const reportId = '1'; -const runs = CONST.PERFORMANCE_TESTS.RUNS; - describe('ReportActionsUtils', () => { beforeAll(() => { Onyx.init({ @@ -65,7 +63,7 @@ describe('ReportActionsUtils', () => { */ test('[ReportActionsUtils] getLastVisibleAction on 10k reportActions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId), {runs}); + await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId)); }); test('[ReportActionsUtils] getLastVisibleAction on 10k reportActions with actionsToMerge', async () => { @@ -91,19 +89,19 @@ describe('ReportActionsUtils', () => { } as unknown as ReportActions; await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId, actionsToMerge), {runs}); + await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId, actionsToMerge)); }); test('[ReportActionsUtils] getMostRecentIOURequestActionID on 10k ReportActions', async () => { const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions); await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActionsArray), {runs}); + await measureFunction(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActionsArray)); }); test('[ReportActionsUtils] getLastVisibleMessage on 10k ReportActions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId), {runs}); + await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId)); }); test('[ReportActionsUtils] getLastVisibleMessage on 10k ReportActions with actionsToMerge', async () => { @@ -129,21 +127,21 @@ describe('ReportActionsUtils', () => { } as unknown as ReportActions; await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId, actionsToMerge), {runs}); + await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId, actionsToMerge)); }); test('[ReportActionsUtils] getSortedReportActionsForDisplay on 10k ReportActions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions), {runs}); + await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions)); }); test('[ReportActionsUtils] getLastClosedReportAction on 10k ReportActions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getLastClosedReportAction(reportActions), {runs}); + await measureFunction(() => ReportActionsUtils.getLastClosedReportAction(reportActions)); }); test('[ReportActionsUtils] getMostRecentReportActionLastModified', async () => { await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getMostRecentReportActionLastModified(), {runs}); + await measureFunction(() => ReportActionsUtils.getMostRecentReportActionLastModified()); }); }); diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js index d58f71fa7ab4..5a144e715f5b 100644 --- a/tests/perf-test/ReportScreen.perf-test.js +++ b/tests/perf-test/ReportScreen.perf-test.js @@ -152,8 +152,6 @@ function ReportScreenWrapper(args) { ); } -const runs = CONST.PERFORMANCE_TESTS.RUNS; - test.skip('[ReportScreen] should render ReportScreen with composer interactions', () => { const {triggerTransitionEnd, addListener} = createAddListenerMock(); const scenario = async () => { @@ -222,7 +220,7 @@ test.skip('[ReportScreen] should render ReportScreen with composer interactions' navigation={navigation} route={mockRoute} />, - {scenario, runs}, + {scenario}, ), ); }); @@ -287,7 +285,7 @@ test.skip('[ReportScreen] should press of the report item', () => { navigation={navigation} route={mockRoute} />, - {scenario, runs}, + {scenario}, ), ); }); diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index 953fc88a99cf..ae3429bb9c01 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -12,7 +12,6 @@ import createRandomReport from '../utils/collections/reports'; import createRandomTransaction from '../utils/collections/transaction'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -const runs = CONST.PERFORMANCE_TESTS.RUNS; const getMockedReports = (length = 500) => createCollection( (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, @@ -33,8 +32,8 @@ const personalDetails = createCollection( 1000, ); -const mockedReportsMap = getMockedReports(5000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>; -const mockedPoliciesMap = getMockedPolicies(5000) as Record<`${typeof ONYXKEYS.COLLECTION.POLICY}`, Policy>; +const mockedReportsMap = getMockedReports(1000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>; +const mockedPoliciesMap = getMockedPolicies(1000) as Record<`${typeof ONYXKEYS.COLLECTION.POLICY}`, Policy>; const participantAccountIDs = Array.from({length: 1000}, (v, i) => i + 1); describe('ReportUtils', () => { @@ -62,15 +61,15 @@ describe('ReportUtils', () => { const openOnAdminRoom = true; await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom), {runs}); + await measureFunction(() => ReportUtils.findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom)); }); - test('[ReportUtils] canDeleteReportAction on 5k reports and policies', async () => { + test('[ReportUtils] canDeleteReportAction on 1k reports and policies', async () => { const reportID = '1'; const reportAction = {...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT} as unknown as ReportAction; await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.canDeleteReportAction(reportAction, reportID), {runs}); + await measureFunction(() => ReportUtils.canDeleteReportAction(reportAction, reportID)); }); test('[ReportUtils] getReportRecipientAccountID on 1k participants', async () => { @@ -78,14 +77,14 @@ describe('ReportUtils', () => { const currentLoginAccountID = 1; await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getReportRecipientAccountIDs(report, currentLoginAccountID), {runs}); + await measureFunction(() => ReportUtils.getReportRecipientAccountIDs(report, currentLoginAccountID)); }); test('[ReportUtils] getIconsForParticipants on 1k participants', async () => { const participants = Array.from({length: 1000}, (v, i) => i + 1); await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getIconsForParticipants(participants, personalDetails), {runs}); + await measureFunction(() => ReportUtils.getIconsForParticipants(participants, personalDetails)); }); test('[ReportUtils] getIcons on 1k participants', async () => { @@ -96,7 +95,7 @@ describe('ReportUtils', () => { const defaultIconId = -1; await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getIcons(report, personalDetails, defaultIcon, defaultName, defaultIconId, policy), {runs}); + await measureFunction(() => ReportUtils.getIcons(report, personalDetails, defaultIcon, defaultName, defaultIconId, policy)); }); test('[ReportUtils] getDisplayNamesWithTooltips 1k participants', async () => { @@ -104,10 +103,10 @@ describe('ReportUtils', () => { const shouldFallbackToHidden = true; await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getDisplayNamesWithTooltips(personalDetails, isMultipleParticipantReport, shouldFallbackToHidden), {runs}); + await measureFunction(() => ReportUtils.getDisplayNamesWithTooltips(personalDetails, isMultipleParticipantReport, shouldFallbackToHidden)); }); - test('[ReportUtils] getReportPreviewMessage on 5k policies', async () => { + test('[ReportUtils] getReportPreviewMessage on 1k policies', async () => { const reportAction = createRandomReportAction(1); const report = createRandomReport(1); const policy = createRandomPolicy(1); @@ -115,7 +114,7 @@ describe('ReportUtils', () => { const isPreviewMessageForParentChatReport = true; await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getReportPreviewMessage(report, reportAction, shouldConsiderReceiptBeingScanned, isPreviewMessageForParentChatReport, policy), {runs}); + await measureFunction(() => ReportUtils.getReportPreviewMessage(report, reportAction, shouldConsiderReceiptBeingScanned, isPreviewMessageForParentChatReport, policy)); }); test('[ReportUtils] getReportName on 1k participants', async () => { @@ -123,7 +122,7 @@ describe('ReportUtils', () => { const policy = createRandomPolicy(1); await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getReportName(report, policy), {runs}); + await measureFunction(() => ReportUtils.getReportName(report, policy)); }); test('[ReportUtils] canShowReportRecipientLocalTime on 1k participants', async () => { @@ -131,7 +130,7 @@ describe('ReportUtils', () => { const accountID = 1; await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.canShowReportRecipientLocalTime(personalDetails, report, accountID), {runs}); + await measureFunction(() => ReportUtils.canShowReportRecipientLocalTime(personalDetails, report, accountID)); }); test('[ReportUtils] shouldReportBeInOptionList on 1k participant', async () => { @@ -142,20 +141,17 @@ describe('ReportUtils', () => { const policies = getMockedPolicies(); await waitForBatchedUpdates(); - await measureFunction( - () => ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInGSDMode, betas, policies, doesReportHaveViolations: false, excludeEmptyChats: false}), - { - runs, - }, + await measureFunction(() => + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInGSDMode, betas, policies, doesReportHaveViolations: false, excludeEmptyChats: false}), ); }); - test('[ReportUtils] getWorkspaceIcon on 5k policies', async () => { + test('[ReportUtils] getWorkspaceIcon on 1k policies', async () => { const report = createRandomReport(1); const policy = createRandomPolicy(1); await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getWorkspaceIcon(report, policy), {runs}); + await measureFunction(() => ReportUtils.getWorkspaceIcon(report, policy)); }); test('[ReportUtils] getMoneyRequestOptions on 1k participants', async () => { @@ -164,32 +160,32 @@ describe('ReportUtils', () => { const reportParticipants = Array.from({length: 1000}, (v, i) => i + 1); await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getMoneyRequestOptions(report, policy, reportParticipants), {runs}); + await measureFunction(() => ReportUtils.getMoneyRequestOptions(report, policy, reportParticipants)); }); - test('[ReportUtils] getWorkspaceAvatar on 5k policies', async () => { + test('[ReportUtils] getWorkspaceAvatar on 1k policies', async () => { const report = createRandomReport(1); await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getWorkspaceAvatar(report), {runs}); + await measureFunction(() => ReportUtils.getWorkspaceAvatar(report)); }); - test('[ReportUtils] getWorkspaceChat on 5k policies', async () => { + test('[ReportUtils] getWorkspaceChat on 1k policies', async () => { const policyID = '1'; const accountsID = Array.from({length: 20}, (v, i) => i + 1); await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getWorkspaceChats(policyID, accountsID), {runs}); + await measureFunction(() => ReportUtils.getWorkspaceChats(policyID, accountsID)); }); - test('[ReportUtils] getTransactionDetails on 5k reports', async () => { + test('[ReportUtils] getTransactionDetails on 1k reports', async () => { const transaction = createRandomTransaction(1); await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getTransactionDetails(transaction, 'yyyy-MM-dd'), {runs}); + await measureFunction(() => ReportUtils.getTransactionDetails(transaction, 'yyyy-MM-dd')); }); - test('[ReportUtils] getIOUReportActionDisplayMessage on 5k policies', async () => { + test('[ReportUtils] getIOUReportActionDisplayMessage on 1k policies', async () => { const reportAction = { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.IOU, @@ -205,6 +201,6 @@ describe('ReportUtils', () => { }; await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getIOUReportActionDisplayMessage(reportAction), {runs}); + await measureFunction(() => ReportUtils.getIOUReportActionDisplayMessage(reportAction)); }); }); diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index e948ade014dc..b27a2e2115be 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -36,6 +36,22 @@ jest.mock('@react-navigation/native', () => { }; }); +jest.mock('../../src/components/withNavigationFocus', () => (Component) => { + function WithNavigationFocus(props) { + return ( + + ); + } + + WithNavigationFocus.displayName = 'WithNavigationFocus'; + + return WithNavigationFocus; +}); + const getMockedReports = (length = 100) => createCollection( (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, @@ -87,8 +103,6 @@ function SearchPageWrapper(args) { ); } -const runs = CONST.PERFORMANCE_TESTS.RUNS; - test('[Search Page] should interact when text input changes', async () => { const {addListener} = TestHelper.createAddListenerMock(); @@ -113,7 +127,7 @@ test('[Search Page] should interact when text input changes', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => measurePerformance(, {scenario, runs})); + .then(() => measurePerformance(, {scenario})); }); test('[Search Page] should render options list', async () => { @@ -139,7 +153,7 @@ test('[Search Page] should render options list', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => measurePerformance(, {scenario, runs})); + .then(() => measurePerformance(, {scenario})); }); test('[Search Page] should search in options list', async () => { @@ -166,7 +180,7 @@ test('[Search Page] should search in options list', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => measurePerformance(, {scenario, runs})); + .then(() => measurePerformance(, {scenario})); }); test('[Search Page] should click on list item', async () => { @@ -194,5 +208,5 @@ test('[Search Page] should click on list item', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => measurePerformance(, {scenario, runs})); + .then(() => measurePerformance(, {scenario})); }); diff --git a/tests/perf-test/SelectionList.perf-test.js b/tests/perf-test/SelectionList.perf-test.js index b9f05c33e925..9decc4361612 100644 --- a/tests/perf-test/SelectionList.perf-test.js +++ b/tests/perf-test/SelectionList.perf-test.js @@ -3,7 +3,6 @@ import React, {useState} from 'react'; import {measurePerformance} from 'reassure'; import _ from 'underscore'; import SelectionList from '../../src/components/SelectionList'; -import CONST from '../../src/CONST'; import variables from '../../src/styles/variables'; jest.mock('../../src/components/Icon/Expensicons'); @@ -93,8 +92,6 @@ function SelectionListWrapper(args) { ); } -const runs = CONST.PERFORMANCE_TESTS.RUNS; - test('[SelectionList] should render 1 section and a thousand items', () => { measurePerformance(); }); @@ -104,7 +101,7 @@ test('[SelectionList] should press a list item', () => { fireEvent.press(screen.getByText('Item 5')); }; - measurePerformance(, {scenario, runs}); + measurePerformance(, {scenario}); }); test('[SelectionList] should render multiple selection and select 3 items', () => { @@ -114,7 +111,7 @@ test('[SelectionList] should render multiple selection and select 3 items', () = fireEvent.press(screen.getByText('Item 3')); }; - measurePerformance(, {scenario, runs}); + measurePerformance(, {scenario}); }); test('[SelectionList] should scroll and select a few items', () => { @@ -145,5 +142,5 @@ test('[SelectionList] should scroll and select a few items', () => { fireEvent.press(screen.getByText('Item 15')); }; - measurePerformance(, {scenario, runs}); + measurePerformance(, {scenario}); }); diff --git a/tests/perf-test/SidebarLinks.perf-test.js b/tests/perf-test/SidebarLinks.perf-test.js index 8109c00c0cea..0b10718fd0c4 100644 --- a/tests/perf-test/SidebarLinks.perf-test.js +++ b/tests/perf-test/SidebarLinks.perf-test.js @@ -31,8 +31,6 @@ const getMockedReportsMap = (length = 100) => { const mockedResponseMap = getMockedReportsMap(500); -const runs = CONST.PERFORMANCE_TESTS.RUNS; - describe('SidebarLinks', () => { beforeAll(() => { Onyx.init({ @@ -73,7 +71,7 @@ describe('SidebarLinks', () => { }; await waitForBatchedUpdates(); - await measurePerformance(, {scenario, runs}); + await measurePerformance(, {scenario}); }); test('[SidebarLinks] should scroll and click some of the items', async () => { @@ -108,6 +106,6 @@ describe('SidebarLinks', () => { await waitForBatchedUpdates(); - await measurePerformance(, {scenario, runs}); + await measurePerformance(, {scenario}); }); }); diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 7b2fd873f3de..3aa65331b9c2 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -32,8 +32,7 @@ const personalDetails = createCollection( (index) => createPersonalDetails(index), ); -const mockedResponseMap = getMockedReports(5000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>; -const runs = CONST.PERFORMANCE_TESTS.RUNS; +const mockedResponseMap = getMockedReports(1000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>; describe('SidebarUtils', () => { beforeAll(() => { @@ -51,7 +50,7 @@ describe('SidebarUtils', () => { Onyx.clear(); }); - test('[SidebarUtils] getOptionData on 5k reports', async () => { + test('[SidebarUtils] getOptionData on 1k reports', async () => { const report = createRandomReport(1); const preferredLocale = 'en'; const policy = createRandomPolicy(1); @@ -59,22 +58,20 @@ describe('SidebarUtils', () => { await waitForBatchedUpdates(); - await measureFunction( - () => - SidebarUtils.getOptionData({ - report, - reportActions, - personalDetails, - preferredLocale, - policy, - parentReportAction, - hasViolations: false, - }), - {runs}, + await measureFunction(() => + SidebarUtils.getOptionData({ + report, + reportActions, + personalDetails, + preferredLocale, + policy, + parentReportAction, + hasViolations: false, + }), ); }); - test('[SidebarUtils] getOrderedReportIDs on 5k reports', async () => { + test('[SidebarUtils] getOrderedReportIDs on 1k reports', async () => { const currentReportId = '1'; const allReports = getMockedReports(); const betas = [CONST.BETAS.DEFAULT_ROOMS]; @@ -104,8 +101,6 @@ describe('SidebarUtils', () => { ) as unknown as OnyxCollection; await waitForBatchedUpdates(); - await measureFunction(() => SidebarUtils.getOrderedReportIDs(currentReportId, allReports, betas, policies, CONST.PRIORITY_MODE.DEFAULT, allReportActions, transactionViolations), { - runs, - }); + await measureFunction(() => SidebarUtils.getOrderedReportIDs(currentReportId, allReports, betas, policies, CONST.PRIORITY_MODE.DEFAULT, allReportActions, transactionViolations)); }); });