Skip to content

Commit 1390c29

Browse files
authored
Merge pull request #52751 from margelo/e2e/money-request-flow
e2e: added money request flow e2e test
2 parents 56752a1 + 4a286aa commit 1390c29

33 files changed

+813
-567
lines changed

contributingGuides/PERFORMANCE_METRICS.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Project is using Firebase for tracking these metrics. However, not all of them a
2525
| `open_report_thread` || Time taken to open a thread in a report.<br><br>**Platforms:** All | Starts when user presses Report Action Item. | Stops when the `ReportActionsList` finishes laying out. |
2626
| `send_message` || Time taken to send a message.<br><br>**Platforms:** All | Starts when the new message is sent. | Stops when the message is being rendered in the chat. |
2727
| `pusher_ping_pong` || The time it takes to receive a PONG event through Pusher.<br><br>**Platforms:** All | Starts every minute and repeats on the minute. | Stops when the event is received from the server. |
28+
| `open_create_expense` || Time taken to open "Create expense" screen.<br><br>**Platforms:** All | Starts when the `Create expense` is pressed. | Stops when the `IOURequestStartPage` finishes laying out. |
29+
| `open_create_expense_contact` || Time taken to "Create expense" screen.<br><br>**Platforms:** All | Starts when the `Next` button on `Create expense` screen is pressed. | Stops when the `IOURequestStepParticipants` finishes laying out. |
30+
| `open_create_expense_approve` || Time taken to "Create expense" screen.<br><br>**Platforms:** All | Starts when the `Contact` on `Choose recipient` screen is selected. | Stops when the `IOURequestStepConfirmation` finishes laying out. |
2831

2932
## Documentation Maintenance
3033

@@ -38,7 +41,6 @@ To ensure this documentation remains accurate and useful, please adhere to the f
3841

3942
4. **Code Location Changes**: If the placement of a metric in the code changes, update the "Start time" and "End time" columns to reflect the new location.
4043

41-
4244
## Additional Resources
4345

4446
- [Firebase Documentation](https://firebase.google.com/docs)

src/CONST.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1519,6 +1519,9 @@ const CONST = {
15191519
SIDEBAR_LOADED: 'sidebar_loaded',
15201520
LOAD_SEARCH_OPTIONS: 'load_search_options',
15211521
SEND_MESSAGE: 'send_message',
1522+
OPEN_CREATE_EXPENSE: 'open_create_expense',
1523+
OPEN_CREATE_EXPENSE_CONTACT: 'open_create_expense_contact',
1524+
OPEN_CREATE_EXPENSE_APPROVE: 'open_create_expense_approve',
15221525
APPLY_AIRSHIP_UPDATES: 'apply_airship_updates',
15231526
APPLY_PUSHER_UPDATES: 'apply_pusher_updates',
15241527
APPLY_HTTPS_UPDATES: 'apply_https_updates',

src/components/BigNumberPad.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ function BigNumberPad({numberPressed, longPressHandlerStateChanged = () => {}, i
9797
e.preventDefault();
9898
}}
9999
isLongPressDisabled={isLongPressDisabled}
100+
testID={`button_${column}`}
100101
/>
101102
);
102103
})}

src/components/Button/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,6 @@ type ButtonProps = Partial<ChildrenProps> & {
123123
/** Id to use for this button */
124124
id?: string;
125125

126-
/** Used to locate this button in ui tests */
127-
testID?: string;
128-
129126
/** Accessibility label for the component */
130127
accessibilityLabel?: string;
131128

@@ -147,6 +144,9 @@ type ButtonProps = Partial<ChildrenProps> & {
147144
/** Whether the Enter keyboard listening is active whether or not the screen that contains the button is focused */
148145
isPressOnEnterActive?: boolean;
149146

147+
/** The testID of the button. Used to locate this view in end-to-end tests. */
148+
testID?: string;
149+
150150
/** Whether is a nested button inside other button, since nesting buttons isn't valid html */
151151
isNested?: boolean;
152152

src/components/FloatingActionButton.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role, isTo
106106
onLongPress={() => {}}
107107
role={role}
108108
shouldUseHapticsOnLongPress={false}
109+
testID="floating-action-button"
109110
>
110111
<Animated.View style={[styles.floatingActionButton, {borderRadius}, animatedStyle]}>
111112
<Svg

src/components/MoneyRequestAmountInput.tsx

+18-15
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} fr
33
import type {NativeSyntheticEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
44
import useLocalize from '@hooks/useLocalize';
55
import {useMouseContext} from '@hooks/useMouseContext';
6-
import * as Browser from '@libs/Browser';
7-
import * as CurrencyUtils from '@libs/CurrencyUtils';
6+
import {isMobileSafari} from '@libs/Browser';
7+
import {convertToFrontendAmountAsString, getCurrencyDecimals} from '@libs/CurrencyUtils';
88
import getOperatingSystem from '@libs/getOperatingSystem';
9-
import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
9+
import {replaceAllDigits, replaceCommasWithPeriod, stripCommaFromAmount, stripDecimalsFromAmount, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils';
1010
import shouldIgnoreSelectionWhenUpdatedManually from '@libs/shouldIgnoreSelectionWhenUpdatedManually';
1111
import CONST from '@src/CONST';
1212
import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused';
@@ -92,6 +92,9 @@ type MoneyRequestAmountInputProps = {
9292

9393
/** The width of inner content */
9494
contentWidth?: number;
95+
96+
/** The testID of the input. Used to locate this view in end-to-end tests. */
97+
testID?: string;
9598
} & Pick<TextInputWithCurrencySymbolProps, 'autoGrowExtraSpace'>;
9699

97100
type Selection = {
@@ -107,7 +110,7 @@ const getNewSelection = (oldSelection: Selection, prevLength: number, newLength:
107110
return {start: cursorPosition, end: cursorPosition};
108111
};
109112

110-
const defaultOnFormatAmount = (amount: number, currency?: string) => CurrencyUtils.convertToFrontendAmountAsString(amount, currency ?? CONST.CURRENCY.USD);
113+
const defaultOnFormatAmount = (amount: number, currency?: string) => convertToFrontendAmountAsString(amount, currency ?? CONST.CURRENCY.USD);
111114

112115
function MoneyRequestAmountInput(
113116
{
@@ -129,6 +132,7 @@ function MoneyRequestAmountInput(
129132
autoGrow = true,
130133
autoGrowExtraSpace,
131134
contentWidth,
135+
testID,
132136
...props
133137
}: MoneyRequestAmountInputProps,
134138
forwardedRef: ForwardedRef<BaseTextInputRef>,
@@ -139,7 +143,7 @@ function MoneyRequestAmountInput(
139143

140144
const amountRef = useRef<string | undefined>(undefined);
141145

142-
const decimals = CurrencyUtils.getCurrencyDecimals(currency);
146+
const decimals = getCurrencyDecimals(currency);
143147
const selectedAmountAsString = amount ? onFormatAmount(amount, currency) : '';
144148

145149
const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString);
@@ -161,13 +165,11 @@ function MoneyRequestAmountInput(
161165
(newAmount: string) => {
162166
// Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value
163167
// More info: https://github.com/Expensify/App/issues/16974
164-
const newAmountWithoutSpaces = MoneyRequestUtils.stripSpacesFromAmount(newAmount);
165-
const finalAmount = newAmountWithoutSpaces.includes('.')
166-
? MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces)
167-
: MoneyRequestUtils.replaceCommasWithPeriod(newAmountWithoutSpaces);
168+
const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount);
169+
const finalAmount = newAmountWithoutSpaces.includes('.') ? stripCommaFromAmount(newAmountWithoutSpaces) : replaceCommasWithPeriod(newAmountWithoutSpaces);
168170
// Use a shallow copy of selection to trigger setSelection
169171
// More info: https://github.com/Expensify/App/issues/16385
170-
if (!MoneyRequestUtils.validateAmount(finalAmount, decimals)) {
172+
if (!validateAmount(finalAmount, decimals)) {
171173
setSelection((prevSelection) => ({...prevSelection}));
172174
return;
173175
}
@@ -176,7 +178,7 @@ function MoneyRequestAmountInput(
176178

177179
willSelectionBeUpdatedManually.current = true;
178180
let hasSelectionBeenSet = false;
179-
const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(finalAmount);
181+
const strippedAmount = stripCommaFromAmount(finalAmount);
180182
amountRef.current = strippedAmount;
181183
setCurrentAmount((prevAmount) => {
182184
const isForwardDelete = prevAmount.length > strippedAmount.length && forwardDeletePressedRef.current;
@@ -233,12 +235,12 @@ function MoneyRequestAmountInput(
233235
// Modifies the amount to match the decimals for changed currency.
234236
useEffect(() => {
235237
// If the changed currency supports decimals, we can return
236-
if (MoneyRequestUtils.validateAmount(currentAmount, decimals)) {
238+
if (validateAmount(currentAmount, decimals)) {
237239
return;
238240
}
239241

240242
// If the changed currency doesn't support decimals, we can strip the decimals
241-
setNewAmount(MoneyRequestUtils.stripDecimalsFromAmount(currentAmount));
243+
setNewAmount(stripDecimalsFromAmount(currentAmount));
242244

243245
// we want to update only when decimals change (setNewAmount also changes when decimals change).
244246
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
@@ -249,7 +251,7 @@ function MoneyRequestAmountInput(
249251
*/
250252
const textInputKeyPress = ({nativeEvent}: NativeSyntheticEvent<KeyboardEvent>) => {
251253
const key = nativeEvent?.key.toLowerCase();
252-
if (Browser.isMobileSafari() && key === CONST.PLATFORM_SPECIFIC_KEYS.CTRL.DEFAULT) {
254+
if (isMobileSafari() && key === CONST.PLATFORM_SPECIFIC_KEYS.CTRL.DEFAULT) {
253255
// Optimistically anticipate forward-delete on iOS Safari (in cases where the Mac Accessiblity keyboard is being
254256
// used for input). If the Control-D shortcut doesn't get sent, the ref will still be reset on the next key press.
255257
forwardDeletePressedRef.current = true;
@@ -276,7 +278,7 @@ function MoneyRequestAmountInput(
276278
});
277279
}, [amount, currency, onFormatAmount, formatAmountOnBlur, maxLength]);
278280

279-
const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit);
281+
const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit);
280282

281283
const {setMouseDown, setMouseUp} = useMouseContext();
282284
const handleMouseDown = (e: React.MouseEvent<Element, MouseEvent>) => {
@@ -340,6 +342,7 @@ function MoneyRequestAmountInput(
340342
onMouseDown={handleMouseDown}
341343
onMouseUp={handleMouseUp}
342344
contentWidth={contentWidth}
345+
testID={testID}
343346
/>
344347
);
345348
}

src/components/PopoverMenu.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ type PopoverMenuItem = MenuItemProps & {
6161
rightIcon?: React.FC<SvgProps>;
6262

6363
key?: string;
64+
65+
/** Test identifier used to find elements in unit and e2e tests */
66+
testID?: string;
6467
};
6568

6669
type PopoverModalProps = Pick<ModalProps, 'animationIn' | 'animationOut' | 'animationInTiming' | 'animationOutTiming'> &
@@ -198,8 +201,8 @@ function PopoverMenu({
198201
shouldUpdateFocusedIndex = true,
199202
shouldUseModalPaddingStyle,
200203
shouldUseNewModal,
201-
testID,
202204
shouldAvoidSafariException = false,
205+
testID,
203206
}: PopoverMenuProps) {
204207
const styles = useThemeStyles();
205208
const theme = useTheme();
@@ -278,7 +281,7 @@ function PopoverMenu({
278281
};
279282

280283
const renderedMenuItems = currentMenuItems.map((item, menuIndex) => {
281-
const {text, onSelected, subMenuItems, shouldCallAfterModalHide, key, ...menuItemProps} = item;
284+
const {text, onSelected, subMenuItems, shouldCallAfterModalHide, key, testID: menuItemTestID, ...menuItemProps} = item;
282285
return (
283286
<OfflineWithFeedback
284287
// eslint-disable-next-line react/no-array-index-key
@@ -288,7 +291,7 @@ function PopoverMenu({
288291
<FocusableMenuItem
289292
// eslint-disable-next-line react/no-array-index-key
290293
key={key ?? `${item.text}_${menuIndex}`}
291-
pressableTestID={`PopoverMenuItem-${item.text}`}
294+
pressableTestID={menuItemTestID ?? `PopoverMenuItem-${item.text}`}
292295
title={text}
293296
onPress={() => selectItem(menuIndex)}
294297
focused={focusedIndex === menuIndex}

src/components/Pressable/GenericPressable/index.e2e.tsx

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import React, {forwardRef, useEffect} from 'react';
2+
import {DeviceEventEmitter} from 'react-native';
23
import GenericPressable from './implementation';
34
import type {PressableRef} from './types';
45
import type PressableProps from './types';
56

67
const pressableRegistry = new Map<string, PressableProps>();
78

8-
function getPressableProps(nativeID: string): PressableProps | undefined {
9-
return pressableRegistry.get(nativeID);
9+
function getPressableProps(testId: string): PressableProps | undefined {
10+
return pressableRegistry.get(testId);
1011
}
1112

1213
function E2EGenericPressableWrapper(props: PressableProps, ref: PressableRef) {
1314
useEffect(() => {
14-
const nativeId = props.nativeID;
15-
if (!nativeId) {
15+
const testId = props.testID;
16+
if (!testId) {
1617
return;
1718
}
18-
console.debug(`[E2E] E2EGenericPressableWrapper: Registering pressable with nativeID: ${nativeId}`);
19-
pressableRegistry.set(nativeId, props);
19+
console.debug(`[E2E] E2EGenericPressableWrapper: Registering pressable with testID: ${testId}`);
20+
pressableRegistry.set(testId, props);
21+
22+
DeviceEventEmitter.emit('onBecameVisible', testId);
2023
}, [props]);
2124

2225
return (

src/components/Search/SearchRouter/SearchButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function SearchButton({style}: SearchButtonProps) {
2828
<Tooltip text={translate('common.search')}>
2929
<PressableWithoutFeedback
3030
ref={pressableRef}
31-
nativeID="searchButton"
31+
testID="searchButton"
3232
accessibilityLabel={translate('common.search')}
3333
style={[styles.flexRow, styles.touchableButtonImage, style]}
3434
// eslint-disable-next-line react-compiler/react-compiler

src/components/SelectionList/BaseListItem.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function BaseListItem<TItem extends ListItem>({
3939
onFocus = () => {},
4040
hoverStyle,
4141
onLongPressRow,
42+
testID,
4243
}: BaseListItemProps<TItem>) {
4344
const theme = useTheme();
4445
const styles = useThemeStyles();
@@ -112,6 +113,7 @@ function BaseListItem<TItem extends ListItem>({
112113
onMouseLeave={handleMouseLeave}
113114
tabIndex={item.tabIndex}
114115
wrapperStyle={pressableWrapperStyle}
116+
testID={testID}
115117
>
116118
<View
117119
testID={`${CONST.BASE_LIST_ITEM_TEST_ID}${item.keyForList}`}

src/components/SelectionList/InviteMemberListItem.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ function InviteMemberListItem<TItem extends ListItem>({
9292
onFocus={onFocus}
9393
shouldSyncFocus={shouldSyncFocus}
9494
shouldDisplayRBR={!shouldShowCheckBox}
95+
testID={item.text}
9596
>
9697
{(hovered?: boolean) => (
9798
<EducationalTooltip

src/components/SelectionList/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,8 @@ type BaseListItemProps<TItem extends ListItem> = CommonListItemProps<TItem> & {
329329
hoverStyle?: StyleProp<ViewStyle>;
330330
/** Errors that this user may contain */
331331
shouldDisplayRBR?: boolean;
332+
/** Test ID of the component. Used to locate this view in end-to-end tests. */
333+
testID?: string;
332334
};
333335

334336
type UserListItemProps<TItem extends ListItem> = ListItemProps<TItem> & {

src/components/TabSelector/TabSelector.tsx

+13-11
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,30 @@ type TabSelectorProps = MaterialTopTabBarProps & {
2424
shouldShowLabelWhenInactive?: boolean;
2525
};
2626

27-
type IconAndTitle = {
27+
type IconTitleAndTestID = {
2828
icon: IconAsset;
2929
title: string;
30+
testID?: string;
3031
};
3132

32-
function getIconAndTitle(route: string, translate: LocaleContextProps['translate']): IconAndTitle {
33+
function getIconTitleAndTestID(route: string, translate: LocaleContextProps['translate']): IconTitleAndTestID {
3334
switch (route) {
3435
case CONST.TAB_REQUEST.MANUAL:
35-
return {icon: Expensicons.Pencil, title: translate('tabSelector.manual')};
36+
return {icon: Expensicons.Pencil, title: translate('tabSelector.manual'), testID: 'manual'};
3637
case CONST.TAB_REQUEST.SCAN:
37-
return {icon: Expensicons.ReceiptScan, title: translate('tabSelector.scan')};
38+
return {icon: Expensicons.ReceiptScan, title: translate('tabSelector.scan'), testID: 'scan'};
3839
case CONST.TAB.NEW_CHAT:
39-
return {icon: Expensicons.User, title: translate('tabSelector.chat')};
40+
return {icon: Expensicons.User, title: translate('tabSelector.chat'), testID: 'chat'};
4041
case CONST.TAB.NEW_ROOM:
41-
return {icon: Expensicons.Hashtag, title: translate('tabSelector.room')};
42+
return {icon: Expensicons.Hashtag, title: translate('tabSelector.room'), testID: 'room'};
4243
case CONST.TAB_REQUEST.DISTANCE:
43-
return {icon: Expensicons.Car, title: translate('common.distance')};
44+
return {icon: Expensicons.Car, title: translate('common.distance'), testID: 'distance'};
4445
case CONST.TAB.SHARE.SHARE:
45-
return {icon: Expensicons.UploadAlt, title: translate('common.share')};
46+
return {icon: Expensicons.UploadAlt, title: translate('common.share'), testID: 'share'};
4647
case CONST.TAB.SHARE.SUBMIT:
47-
return {icon: Expensicons.Receipt, title: translate('common.submit')};
48+
return {icon: Expensicons.Receipt, title: translate('common.submit'), testID: 'submit'};
4849
case CONST.TAB_REQUEST.PER_DIEM:
49-
return {icon: Expensicons.CalendarSolid, title: translate('common.perDiem')};
50+
return {icon: Expensicons.CalendarSolid, title: translate('common.perDiem'), testID: 'perDiem'};
5051
default:
5152
throw new Error(`Route ${route} has no icon nor title set.`);
5253
}
@@ -74,7 +75,7 @@ function TabSelector({state, navigation, onTabPress = () => {}, position, onFocu
7475
const activeOpacity = getOpacity({routesLength: state.routes.length, tabIndex: index, active: true, affectedTabs: affectedAnimatedTabs, position, isActive});
7576
const inactiveOpacity = getOpacity({routesLength: state.routes.length, tabIndex: index, active: false, affectedTabs: affectedAnimatedTabs, position, isActive});
7677
const backgroundColor = getBackgroundColor({routesLength: state.routes.length, tabIndex: index, affectedTabs: affectedAnimatedTabs, theme, position, isActive});
77-
const {icon, title} = getIconAndTitle(route.name, translate);
78+
const {icon, title, testID} = getIconTitleAndTestID(route.name, translate);
7879
const onPress = () => {
7980
if (isActive) {
8081
return;
@@ -106,6 +107,7 @@ function TabSelector({state, navigation, onTabPress = () => {}, position, onFocu
106107
inactiveOpacity={inactiveOpacity}
107108
backgroundColor={backgroundColor}
108109
isActive={isActive}
110+
testID={testID}
109111
shouldShowLabelWhenInactive={shouldShowLabelWhenInactive}
110112
/>
111113
);

src/components/TabSelector/TabSelectorItem.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ type TabSelectorItemProps = {
3535

3636
/** Whether to show the label when the tab is inactive */
3737
shouldShowLabelWhenInactive?: boolean;
38+
39+
/** Test identifier used to find elements in unit and e2e tests */
40+
testID?: string;
3841
};
3942

4043
function TabSelectorItem({
@@ -46,6 +49,7 @@ function TabSelectorItem({
4649
inactiveOpacity = 1,
4750
isActive = false,
4851
shouldShowLabelWhenInactive = true,
52+
testID,
4953
}: TabSelectorItemProps) {
5054
const styles = useThemeStyles();
5155
const [isHovered, setIsHovered] = useState(false);
@@ -64,6 +68,7 @@ function TabSelectorItem({
6468
onHoverOut={() => setIsHovered(false)}
6569
role={CONST.ROLE.BUTTON}
6670
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
71+
testID={testID}
6772
>
6873
<TabIcon
6974
icon={icon}

0 commit comments

Comments
 (0)