Skip to content

Commit cbc87c4

Browse files
gambinishabretonc7sracitores
authored
feat: Init Perps e2e pattern (MetaMask#18738)
## **Description** This PR introduces a comprehensive **E2E Perps Mocking System** that enables fully isolated end-to-end testing of Perps features without requiring testnet funds, wallet imports, or live Hyperliquid SDK calls. ### Motivation - Current E2E tests depend on external resources (testnet balances, live API calls, wallet imports) which introduce flakiness, rate limits, and longer execution times. - Mocking provides **predictable, repeatable, and realistic** test results. ### Solution - Added a centralized **`E2EMockService`** for mock state management. - Introduced **`E2EPerpsController`** to intercept controller methods with mocked logic. - Added **`e2eSetup` utilities** to handle setup/teardown for tests. - Mocking includes account balances, order placement, position management, market data, and WebSocket streaming. ### Getting running: Follow instructions in `docs/readme/expo-e2e-testing.md` to get expo testing setup `source .e2e.env && yarn test:e2e:ios:debug:run e2e/specs/perps/perps-position.spec.ts` ## **Changelog** CHANGELOG entry: `Added a full E2E Perps Mocking System to enable isolated testing without testnet funds or live SDK calls.` ## **Related issues** Fixes: N/A (new infrastructure for E2E testing) ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** Tests depended on external Hyperliquid SDK and testnet balances. Runs were slower and occasionally flaky due to network/API issues. ### **After** Fewer flakes related to 3rd party services, or testnet funds Positions and balances update predictably in test runs No external dependencies required, no external accounts or account private keys needed to be stored <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Arthur Breton <arthur.breton@consensys.net> Co-authored-by: abretonc7s <107169956+abretonc7s@users.noreply.github.com> Co-authored-by: Racitores <ramon.acitores@consensys.net> Co-authored-by: Ramon AC <36987446+racitores@users.noreply.github.com>
1 parent bd4688f commit cbc87c4

43 files changed

Lines changed: 2507 additions & 114 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/component-library/components/Skeleton/Skeleton.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ const Skeleton: React.FC<SkeletonProps> = ({
3030
});
3131

3232
const startAnimation = () => {
33-
// On E2E, we don't want to animate the skeleton otherwise recurring timers will be ON.
34-
if (isE2E) {
35-
return;
36-
}
3733
Animated.sequence([
3834
Animated.timing(opacityAnim, {
3935
toValue: 0.1,
@@ -56,7 +52,7 @@ const Skeleton: React.FC<SkeletonProps> = ({
5652

5753
useEffect(() => {
5854
// Only start animation if no children are present or if children should be hidden
59-
if (!children || hideChildren) {
55+
if (!isE2E && (!children || hideChildren)) {
6056
startAnimation();
6157
}
6258

app/components/UI/Perps/Views/PerpsBalanceModal/PerpsBalanceModal.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Routes from '../../../../../constants/navigation/Routes';
2020
import type { PerpsNavigationParamList } from '../../controllers/types';
2121
import { usePerpsTrading, usePerpsNetworkManagement } from '../../hooks';
2222
import createStyles from './PerpsBalanceModal.styles';
23+
import { PerpsTabViewSelectorsIDs } from '../../../../../../e2e/selectors/Perps/Perps.selectors';
2324

2425
interface PerpsBalanceModalProps {}
2526

@@ -89,6 +90,7 @@ const PerpsBalanceModal: React.FC<PerpsBalanceModalProps> = () => {
8990
onPress={handleAddFunds}
9091
style={styles.actionButton}
9192
startIconName={IconName.Add}
93+
testID={PerpsTabViewSelectorsIDs.ADD_FUNDS_BUTTON}
9294
/>
9395
<Button
9496
variant={ButtonVariants.Secondary}

app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.test.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import PerpsMarketListView from './PerpsMarketListView';
1010
import type { PerpsMarketData } from '../../controllers/types';
1111
import Routes from '../../../../../constants/navigation/Routes';
12+
import { PerpsMarketListViewSelectorsIDs } from '../../../../../../e2e/selectors/Perps/Perps.selectors';
1213
import renderWithProvider from '../../../../../util/test/renderWithProvider';
1314

1415
// Mock dependencies
@@ -324,7 +325,9 @@ describe('PerpsMarketListView', () => {
324325

325326
expect(screen.getByText('Perps')).toBeOnTheScreen();
326327
expect(
327-
screen.getByTestId('perps-market-list-search-toggle-button'),
328+
screen.getByTestId(
329+
PerpsMarketListViewSelectorsIDs.SEARCH_TOGGLE_BUTTON,
330+
),
328331
).toBeOnTheScreen();
329332
expect(screen.getByText('Volume')).toBeOnTheScreen();
330333
expect(screen.getByText('Price / 24h change')).toBeOnTheScreen();
@@ -343,7 +346,9 @@ describe('PerpsMarketListView', () => {
343346

344347
// Should have search toggle button and market rows
345348
expect(
346-
screen.getByTestId('perps-market-list-search-toggle-button'),
349+
screen.getByTestId(
350+
PerpsMarketListViewSelectorsIDs.SEARCH_TOGGLE_BUTTON,
351+
),
347352
).toBeOnTheScreen();
348353
expect(screen.getByTestId('market-row-BTC')).toBeOnTheScreen();
349354
expect(screen.getByTestId('market-row-ETH')).toBeOnTheScreen();
@@ -362,7 +367,7 @@ describe('PerpsMarketListView', () => {
362367

363368
// Click search toggle button
364369
const searchButton = screen.getByTestId(
365-
'perps-market-list-search-toggle-button',
370+
PerpsMarketListViewSelectorsIDs.SEARCH_TOGGLE_BUTTON,
366371
);
367372
act(() => {
368373
fireEvent.press(searchButton);
@@ -379,7 +384,7 @@ describe('PerpsMarketListView', () => {
379384

380385
// First toggle search visibility
381386
const searchButton = screen.getByTestId(
382-
'perps-market-list-search-toggle-button',
387+
PerpsMarketListViewSelectorsIDs.SEARCH_TOGGLE_BUTTON,
383388
);
384389
act(() => {
385390
fireEvent.press(searchButton);
@@ -400,7 +405,7 @@ describe('PerpsMarketListView', () => {
400405

401406
// First toggle search visibility
402407
const searchButton = screen.getByTestId(
403-
'perps-market-list-search-toggle-button',
408+
PerpsMarketListViewSelectorsIDs.SEARCH_TOGGLE_BUTTON,
404409
);
405410
act(() => {
406411
fireEvent.press(searchButton);
@@ -421,7 +426,7 @@ describe('PerpsMarketListView', () => {
421426

422427
// First toggle search visibility
423428
const searchButton = screen.getByTestId(
424-
'perps-market-list-search-toggle-button',
429+
PerpsMarketListViewSelectorsIDs.SEARCH_TOGGLE_BUTTON,
425430
);
426431
act(() => {
427432
fireEvent.press(searchButton);
@@ -436,7 +441,7 @@ describe('PerpsMarketListView', () => {
436441

437442
// Should show clear button when there's search text
438443
expect(
439-
screen.getByTestId('perps-market-list-search-clear-button'),
444+
screen.getByTestId(PerpsMarketListViewSelectorsIDs.SEARCH_CLEAR_BUTTON),
440445
).toBeOnTheScreen();
441446

442447
// Should only show the filtered market (BTC), not others
@@ -449,7 +454,7 @@ describe('PerpsMarketListView', () => {
449454

450455
// First toggle search visibility
451456
const searchButton = screen.getByTestId(
452-
'perps-market-list-search-toggle-button',
457+
PerpsMarketListViewSelectorsIDs.SEARCH_TOGGLE_BUTTON,
453458
);
454459
act(() => {
455460
fireEvent.press(searchButton);
@@ -468,7 +473,7 @@ describe('PerpsMarketListView', () => {
468473

469474
// Find and press clear button using testID
470475
const clearButton = screen.getByTestId(
471-
'perps-market-list-search-clear-button',
476+
PerpsMarketListViewSelectorsIDs.SEARCH_CLEAR_BUTTON,
472477
);
473478
act(() => {
474479
fireEvent.press(clearButton);
@@ -488,7 +493,7 @@ describe('PerpsMarketListView', () => {
488493

489494
// First toggle search visibility
490495
const searchButton = screen.getByTestId(
491-
'perps-market-list-search-toggle-button',
496+
PerpsMarketListViewSelectorsIDs.SEARCH_TOGGLE_BUTTON,
492497
);
493498
act(() => {
494499
fireEvent.press(searchButton);
@@ -636,7 +641,7 @@ describe('PerpsMarketListView', () => {
636641

637642
// Find the tutorial button
638643
const tutorialButton = screen.getByTestId(
639-
'perps-market-list-tutorial-button',
644+
PerpsMarketListViewSelectorsIDs.TUTORIAL_BUTTON,
640645
);
641646
act(() => {
642647
fireEvent.press(tutorialButton);
@@ -817,7 +822,7 @@ describe('PerpsMarketListView', () => {
817822

818823
// First toggle search visibility
819824
const searchButton = screen.getByTestId(
820-
'perps-market-list-search-toggle-button',
825+
PerpsMarketListViewSelectorsIDs.SEARCH_TOGGLE_BUTTON,
821826
);
822827
act(() => {
823828
fireEvent.press(searchButton);
@@ -839,7 +844,7 @@ describe('PerpsMarketListView', () => {
839844

840845
// First toggle search visibility
841846
const searchButton = screen.getByTestId(
842-
'perps-market-list-search-toggle-button',
847+
PerpsMarketListViewSelectorsIDs.SEARCH_TOGGLE_BUTTON,
843848
);
844849
act(() => {
845850
fireEvent.press(searchButton);

app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ const PerpsMarketListHeader = () => {
8989
const { styles } = useStyles(styleSheet, {});
9090

9191
return (
92-
<View style={styles.listHeader}>
92+
<View
93+
style={styles.listHeader}
94+
testID={PerpsMarketListViewSelectorsIDs.LIST_HEADER}
95+
>
9396
<View style={styles.listHeaderLeft}>
9497
<Text variant={TextVariant.BodySMMedium} color={TextColor.Alternative}>
9598
{strings('perps.volume')}
@@ -424,6 +427,7 @@ const PerpsMarketListView = ({
424427
iconName={IconName.Arrow2Left}
425428
size={ButtonIconSizes.Md}
426429
onPress={handleBackPressed}
430+
testID={PerpsMarketListViewSelectorsIDs.BACK_HEADER_BUTTON}
427431
/>
428432
</View>
429433
<Text

app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ import React, {
1414
} from 'react';
1515
import { ScrollView, TouchableOpacity, View } from 'react-native';
1616
import { SafeAreaView } from 'react-native-safe-area-context';
17+
import {
18+
PerpsOrderViewSelectorsIDs,
19+
PerpsGeneralSelectorsIDs,
20+
} from '../../../../../../e2e/selectors/Perps/Perps.selectors';
21+
1722
import { notificationAsync, NotificationFeedbackType } from 'expo-haptics';
18-
import { PerpsOrderViewSelectorsIDs } from '../../../../../../e2e/selectors/Perps/Perps.selectors';
23+
1924
import { strings } from '../../../../../../locales/i18n';
2025
import Button, {
2126
ButtonSize,
@@ -213,6 +218,7 @@ const PerpsOrderViewContentBase: React.FC = () => {
213218
label: strings('perps.order.error.dismiss'),
214219
variant: ButtonVariants.Secondary,
215220
onPress: () => toastRef?.current?.closeToast(),
221+
testID: PerpsGeneralSelectorsIDs.ORDER_SUCCESS_TOAST_DISMISS_BUTTON,
216222
},
217223
});
218224

@@ -855,7 +861,10 @@ const PerpsOrderViewContentBase: React.FC = () => {
855861

856862
{/* Combined TP/SL row */}
857863
<View style={[styles.detailItem, styles.detailItemLast]}>
858-
<TouchableOpacity onPress={() => setIsTPSLVisible(true)}>
864+
<TouchableOpacity
865+
onPress={() => setIsTPSLVisible(true)}
866+
testID={PerpsOrderViewSelectorsIDs.STOP_LOSS_BUTTON}
867+
>
859868
<ListItem>
860869
<ListItemColumn widthType={WidthType.Fill}>
861870
<View style={styles.detailLeft}>
@@ -968,7 +977,10 @@ const PerpsOrderViewContentBase: React.FC = () => {
968977
</ScrollView>
969978
{/* Keypad Section - Show when input is focused */}
970979
{isInputFocused && (
971-
<View style={styles.bottomSection}>
980+
<View
981+
style={styles.bottomSection}
982+
testID={PerpsOrderViewSelectorsIDs.KEYPAD}
983+
>
972984
<View style={styles.percentageButtonsContainer}>
973985
<Button
974986
variant={ButtonVariants.Secondary}

app/components/UI/Perps/Views/PerpsPositionsView/PerpsPositionsView.test.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
screen,
55
fireEvent,
66
waitFor,
7+
within,
78
} from '@testing-library/react-native';
89
import { useNavigation, useFocusEffect } from '@react-navigation/native';
910
import PerpsPositionsView from './PerpsPositionsView';
@@ -228,10 +229,13 @@ describe('PerpsPositionsView', () => {
228229

229230
// Assert
230231
await waitFor(() => {
231-
expect(screen.getByText('Open Positions')).toBeOnTheScreen();
232-
expect(screen.getByText('2 positions')).toBeOnTheScreen();
233-
expect(screen.getByText(/1\.50[\s\S]*ETH/)).toBeOnTheScreen();
234-
expect(screen.getByText(/0\.5000[\s\S]*BTC/)).toBeOnTheScreen();
232+
const section = screen.getByTestId('perps-positions-section');
233+
expect(within(section).getByText('Open Positions')).toBeOnTheScreen();
234+
expect(within(section).getByText('2 positions')).toBeOnTheScreen();
235+
expect(within(section).getByText(/1\.50[\s\S]*ETH/)).toBeOnTheScreen();
236+
expect(
237+
within(section).getByText(/0\.5000[\s\S]*BTC/),
238+
).toBeOnTheScreen();
235239
});
236240
});
237241

@@ -340,7 +344,8 @@ describe('PerpsPositionsView', () => {
340344

341345
// Assert
342346
await waitFor(() => {
343-
expect(screen.getByText('Open Positions')).toBeOnTheScreen();
347+
const section = screen.getByTestId('perps-positions-section');
348+
expect(within(section).getByText('Open Positions')).toBeOnTheScreen();
344349
});
345350
});
346351

@@ -350,7 +355,8 @@ describe('PerpsPositionsView', () => {
350355

351356
// Assert
352357
await waitFor(() => {
353-
expect(screen.getByText('Open Positions')).toBeOnTheScreen();
358+
const section = screen.getByTestId('perps-positions-section');
359+
expect(within(section).getByText('Open Positions')).toBeOnTheScreen();
354360
});
355361
});
356362
});

app/components/UI/Perps/Views/PerpsPositionsView/PerpsPositionsView.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { formatPnl, formatPrice } from '../../utils/formatUtils';
3030
import { calculateTotalPnL } from '../../utils/pnlCalculations';
3131
import { createStyles } from './PerpsPositionsView.styles';
3232
import { SafeAreaView } from 'react-native-safe-area-context';
33+
import { PerpsPositionsViewSelectorsIDs } from '../../../../../../e2e/selectors/Perps/Perps.selectors';
3334

3435
const PerpsPositionsView: React.FC = () => {
3536
const { styles } = useStyles(createStyles, {});
@@ -114,7 +115,10 @@ const PerpsPositionsView: React.FC = () => {
114115
}
115116

116117
return (
117-
<View style={styles.positionsSection}>
118+
<View
119+
style={styles.positionsSection}
120+
testID={PerpsPositionsViewSelectorsIDs.POSITIONS_SECTION}
121+
>
118122
<View style={styles.sectionHeader}>
119123
<Text variant={TextVariant.HeadingSM} color={TextColor.Default}>
120124
{strings('perps.position.list.open_positions')}
@@ -123,12 +127,24 @@ const PerpsPositionsView: React.FC = () => {
123127
{positionCountText}
124128
</Text>
125129
</View>
126-
{positions.map((position, index) => (
127-
<PerpsPositionCard
128-
key={`${position.coin}-${index}`}
129-
position={position}
130-
/>
131-
))}
130+
{positions.map((position, index) => {
131+
const sizeValue = parseFloat(position.size);
132+
const directionSegment = Number.isFinite(sizeValue)
133+
? sizeValue > 0
134+
? 'long'
135+
: sizeValue < 0
136+
? 'short'
137+
: 'unknown'
138+
: 'unknown';
139+
return (
140+
<View
141+
key={`${position.coin}-${index}`}
142+
testID={`${PerpsPositionsViewSelectorsIDs.POSITION_ITEM}-${position.coin}-${position.leverage.value}x-${directionSegment}-${index}`}
143+
>
144+
<PerpsPositionCard position={position} />
145+
</View>
146+
);
147+
})}
132148
</View>
133149
);
134150
};
@@ -197,7 +213,6 @@ const PerpsPositionsView: React.FC = () => {
197213
</Text>
198214
</View>
199215
</View>
200-
201216
{renderContent()}
202217
</ScrollView>
203218

app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ jest.mock('../../../../../../e2e/selectors/Perps/Perps.selectors', () => ({
147147
PerpsTabViewSelectorsIDs: {
148148
START_NEW_TRADE_CTA: 'perps-tab-view-start-new-trade-cta',
149149
},
150+
PerpsPositionsViewSelectorsIDs: {
151+
POSITIONS_SECTION_TITLE: 'perps-positions-section-title',
152+
POSITION_ITEM: 'perps-positions-item',
153+
},
150154
}));
151155

152156
jest.mock('../../components/PerpsBottomSheetTooltip', () => ({

0 commit comments

Comments
 (0)