Skip to content

Commit 6d9165f

Browse files
authored
test: MMQA-1637 move all perps e2e tests to smoke (#27931)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Move all perps e2e tests to smoke for PR run <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **Related issues** Fixes: ## **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** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Mostly test/E2E infrastructure changes, but it touches perps controller mocking, network proxy mocks, and testIDs; regressions could break CI stability or mask real integration issues. > > **Overview** > **Moves Perps E2E coverage into the smoke suite and improves stability of those flows.** Several Perps smoke specs are updated/added to use consistent fixtures (Arbitrum RPC + USDC token), geolocation mocking for eligibility, and more resilient navigation/retry logic. > > **Hardens Perps E2E mocks and selectors to reduce flakiness.** Updates TP/SL testIDs (separating screen container vs Set action), adjusts agentic flows accordingly, adds `waitForStableEnabledIOS` to avoid premature taps on iOS, and expands Perps controller + HTTP proxy mocks (deposit transaction stubs, `getMarkets`/HyperLiquid `meta` payload, forced provider “connected” state, and improved TP/SL-trigger fill/position-close behavior in price push mocks). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b1ea8b7. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 2b1ea46 commit 6d9165f

File tree

23 files changed

+1218
-243
lines changed

23 files changed

+1218
-243
lines changed

app/components/UI/Perps/Perps.testIds.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export const PerpsPositionDetailsViewSelectorsIDs = {
262262
export const PerpsTPSLViewSelectorsIDs = {
263263
BACK_BUTTON: 'back-button',
264264
BOTTOM_SHEET: 'perps-tpsl-bottomsheet',
265-
SET_BUTTON: 'bottomsheetfooter-button',
265+
SET_BUTTON: 'perps-tpsl-set-button',
266266
TAKE_PROFIT_PRICE_INPUT: 'perps-tpsl-tp-input',
267267
STOP_LOSS_PRICE_INPUT: 'perps-tpsl-sl-input',
268268
} as const;
@@ -677,8 +677,8 @@ export const getPerpsHeroCardViewSelector = {
677677
// ========================================
678678

679679
export const PerpsGeneralSelectorsIDs = {
680-
// TPSL bottom sheet primary action button ("Set" / "Updating")
681-
BOTTOM_SHEET_FOOTER_BUTTON: 'perps-tpsl-bottomsheet',
680+
// TPSL screen primary action ("Set" / "Updating"); same id as PerpsTPSLViewSelectorsIDs.SET_BUTTON
681+
BOTTOM_SHEET_FOOTER_BUTTON: 'perps-tpsl-set-button',
682682
// Order success toast dismiss button on PerpsOrderView
683683
ORDER_SUCCESS_TOAST_DISMISS_BUTTON:
684684
'perps-order-success-toast-dismiss-button',

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,11 @@ const PerpsTPSLView: React.FC = () => {
445445
}, [focusedInput, dismissKeypad, handleStopLossOff]);
446446

447447
return (
448-
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
448+
<SafeAreaView
449+
style={styles.container}
450+
edges={['top', 'bottom']}
451+
testID={PerpsTPSLViewSelectorsIDs.BOTTOM_SHEET}
452+
>
449453
{/* Simple header with back button and title */}
450454
<View style={styles.header}>
451455
<View style={styles.headerBackButton}>
@@ -918,7 +922,7 @@ const PerpsTPSLView: React.FC = () => {
918922
onPress={handleConfirm}
919923
isDisabled={confirmDisabled}
920924
loading={isUpdating}
921-
testID={PerpsTPSLViewSelectorsIDs.BOTTOM_SHEET}
925+
testID={PerpsTPSLViewSelectorsIDs.SET_BUTTON}
922926
/>
923927
</View>
924928
</View>

app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.view.test.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { renderPerpsTPSLView } from '../../../../../../tests/component-view/rend
99
import { PerpsTPSLViewSelectorsIDs } from '../../Perps.testIds';
1010

1111
describe('PerpsTPSLView', () => {
12-
it('renders back button and bottom sheet when params are provided', async () => {
12+
it('renders back button, TPSL screen container, and Set button when params are provided', async () => {
1313
renderPerpsTPSLView();
1414

1515
expect(
@@ -26,5 +26,12 @@ describe('PerpsTPSLView', () => {
2626
{ timeout: 5000 },
2727
),
2828
).toBeOnTheScreen();
29+
expect(
30+
await screen.findByTestId(
31+
PerpsTPSLViewSelectorsIDs.SET_BUTTON,
32+
{},
33+
{ timeout: 5000 },
34+
),
35+
).toBeOnTheScreen();
2936
});
3037
});

scripts/perps/agentic/teams/perps/flows/tpsl-create.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
},
9191
"press-set-tpsl": {
9292
"action": "press",
93-
"test_id": "perps-tpsl-bottomsheet",
93+
"test_id": "perps-tpsl-set-button",
9494
"next": "wait-tpsl-created"
9595
},
9696
"wait-tpsl-created": {

scripts/perps/agentic/teams/perps/flows/tpsl-edit.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
},
9191
"press-set-tpsl": {
9292
"action": "press",
93-
"test_id": "perps-tpsl-bottomsheet",
93+
"test_id": "perps-tpsl-set-button",
9494
"next": "wait-tpsl-updated"
9595
},
9696
"wait-tpsl-updated": {

tests/api-mocking/mock-responses/defaults/perps-hyperliquid.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ export const PERPS_HYPERLIQUID_MOCKS: MockEventsObject = {
1414
urlEndpoint: hyperliquidInfoEndpoint,
1515
requestBody: { type: 'meta' },
1616
responseCode: 200,
17-
response: {},
17+
response: {
18+
universe: [
19+
{ name: 'BTC', szDecimals: 3, maxLeverage: 50, marginTableId: 0 },
20+
{ name: 'ETH', szDecimals: 4, maxLeverage: 50, marginTableId: 0 },
21+
{ name: 'SOL', szDecimals: 2, maxLeverage: 50, marginTableId: 0 },
22+
],
23+
},
1824
},
1925
{
2026
urlEndpoint: hyperliquidInfoEndpoint,

tests/api-mocking/mock-responses/perps-arbitrum-mocks.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type TestSpecificMock,
1414
RampsRegion,
1515
} from '../../framework';
16+
import { safeGetBodyText } from '../MockServerE2E.ts';
1617

1718
const logger = createLogger({
1819
name: 'PerpsArbitrumMocks',
@@ -64,6 +65,19 @@ const MOCK_COIN_SVG = `<svg width="24" height="24" xmlns="http://www.w3.org/2000
6465
<text x="12" y="16" text-anchor="middle" font-size="12" fill="gray">$</text>
6566
</svg>`;
6667

68+
/**
69+
* HyperLiquid POST /info `type: "meta"` payload. Required for getMarkets when E2E
70+
* controller overrides are not applied (e.g. provider-only paths). Default
71+
* PERPS_HYPERLIQUID_MOCKS returned `{}` and produced empty markets → "Invalid asset".
72+
*/
73+
const HYPERLIQUID_E2E_META_BODY = {
74+
universe: [
75+
{ name: 'BTC', szDecimals: 3, maxLeverage: 50, marginTableId: 0 },
76+
{ name: 'ETH', szDecimals: 4, maxLeverage: 50, marginTableId: 0 },
77+
{ name: 'SOL', szDecimals: 2, maxLeverage: 50, marginTableId: 0 },
78+
],
79+
} as const;
80+
6781
/**
6882
* TestSpecificMock function for Perps testing
6983
* Sets up mocks to prevent live network requests to Arbitrum during E2E tests
@@ -227,6 +241,42 @@ export const PERPS_ARBITRUM_MOCKS: TestSpecificMock = async (
227241
};
228242
});
229243

244+
// HyperLiquid market metadata (POST /info) — proxied as POST /proxy?url=.../info
245+
await mockServer
246+
.forPost('/proxy')
247+
.matching((request) => {
248+
const urlParam = new URL(request.url).searchParams.get('url') || '';
249+
return urlParam.includes('api.hyperliquid.xyz/info');
250+
})
251+
.asPriority(1001)
252+
.thenCallback(async (request) => {
253+
const bodyText = await safeGetBodyText(request);
254+
let body: { type?: string } = {};
255+
try {
256+
body = bodyText ? JSON.parse(bodyText) : {};
257+
} catch {
258+
/* ignore */
259+
}
260+
261+
if (body.type === 'meta') {
262+
logger.info('[Perps E2E Mock] Intercepted HyperLiquid info (meta)');
263+
return {
264+
statusCode: 200,
265+
body: JSON.stringify(HYPERLIQUID_E2E_META_BODY),
266+
headers: { 'Content-Type': 'application/json' },
267+
};
268+
}
269+
270+
logger.info(
271+
`[Perps E2E Mock] Intercepted HyperLiquid info (type=${body.type ?? 'unknown'})`,
272+
);
273+
return {
274+
statusCode: 200,
275+
body: JSON.stringify({}),
276+
headers: { 'Content-Type': 'application/json' },
277+
};
278+
});
279+
230280
// Mock Rewards API for perps fee discount through the mobile proxy
231281
await mockServer
232282
.forGet('/proxy')

tests/controller-mocking/mock-config/perps-controller-mixin.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,41 @@ import {
2020
type Funding,
2121
type UpdatePositionTPSLParams,
2222
type PerpsControllerState,
23+
type GetMarketsParams,
24+
type MarketInfo,
25+
WebSocketConnectionState,
2326
} from '@metamask/perps-controller';
2427

2528
// Interface for controller with update method access
2629
interface ControllerWithUpdate {
2730
update: (fn: (state: PerpsControllerState) => void) => void;
2831
}
2932

33+
type PerpsDepositTransactionType = 'perpsDepositAndOrder' | 'perpsDeposit';
34+
35+
type AddTransactionMessengerResult =
36+
| {
37+
result?: Promise<string>;
38+
transactionMeta?: { id?: string };
39+
transactionMetaId?: string;
40+
id?: string;
41+
}
42+
| undefined;
43+
44+
const PERPS_DEPOSIT_ADD_TRANSACTION_TIMEOUT_MS = 1200;
45+
46+
/** Size decimals aligned with typical HyperLiquid main-DEX markets (used by order form). */
47+
const E2E_MARKET_SZ_DECIMALS: Record<string, number> = {
48+
BTC: 3,
49+
ETH: 4,
50+
SOL: 2,
51+
};
52+
53+
function parseMockMaxLeverageFormatted(maxLeverage: string): number {
54+
const match = /^(\d+)/.exec(maxLeverage.trim());
55+
return match ? Number.parseInt(match[1], 10) : 40;
56+
}
57+
3058
/**
3159
* E2E Controller Method Overrides
3260
* These methods replace controller methods when applied via applyE2EPerpsControllerMocks
@@ -58,6 +86,136 @@ export class E2EControllerOverrides {
5886
return result;
5987
}
6088

89+
/**
90+
* Best effort: if messenger returns a real transaction id, use it.
91+
* Never throw here; fallback keeps E2E stable.
92+
*/
93+
private async mockPerpsDepositTransaction(params: {
94+
fallbackTxId: string;
95+
transactionType: PerpsDepositTransactionType;
96+
}): Promise<{ result: Promise<string> }> {
97+
const { fallbackTxId, transactionType } = params;
98+
const controllerRecord = this.controller as Record<string, unknown>;
99+
const messenger = controllerRecord.messenger as
100+
| { call: (...args: unknown[]) => unknown }
101+
| undefined;
102+
103+
let resolvedTxId = fallbackTxId;
104+
105+
if (messenger) {
106+
try {
107+
const selectedAccounts = messenger.call(
108+
'AccountTreeController:getAccountsFromSelectedAccountGroup',
109+
) as { address?: string }[];
110+
const fromAddress = selectedAccounts?.[0]?.address;
111+
const networkClientId = messenger.call(
112+
'NetworkController:findNetworkClientIdByChainId',
113+
'0xa4b1',
114+
) as string | undefined;
115+
116+
if (fromAddress && networkClientId) {
117+
const recipientPadded = fromAddress
118+
.toLowerCase()
119+
.replace('0x', '')
120+
.padStart(64, '0');
121+
const amountPadded = '0'.padStart(64, '0');
122+
const transferData = `0xa9059cbb${recipientPadded}${amountPadded}`;
123+
124+
const addTransactionCall = Promise.resolve(
125+
messenger.call(
126+
'TransactionController:addTransaction',
127+
{
128+
from: fromAddress,
129+
to: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
130+
value: '0x0',
131+
data: transferData,
132+
gas: '0x493e0',
133+
},
134+
{
135+
networkClientId,
136+
origin: 'metamask',
137+
skipInitialGasEstimate: true,
138+
type: transactionType,
139+
},
140+
),
141+
) as Promise<AddTransactionMessengerResult>;
142+
143+
const addTransactionSafe = addTransactionCall.catch(
144+
(): AddTransactionMessengerResult => undefined,
145+
);
146+
147+
const timeout = new Promise<undefined>((resolve) =>
148+
setTimeout(
149+
() => resolve(undefined),
150+
PERPS_DEPOSIT_ADD_TRANSACTION_TIMEOUT_MS,
151+
),
152+
);
153+
const maybeAddResult = await Promise.race([
154+
addTransactionSafe,
155+
timeout,
156+
]);
157+
158+
resolvedTxId =
159+
maybeAddResult?.transactionMeta?.id ??
160+
maybeAddResult?.transactionMetaId ??
161+
maybeAddResult?.id ??
162+
fallbackTxId;
163+
}
164+
} catch {
165+
resolvedTxId = fallbackTxId;
166+
}
167+
}
168+
169+
(this.controller as ControllerWithUpdate).update(
170+
(state: PerpsControllerState) => {
171+
state.lastDepositTransactionId = resolvedTxId;
172+
state.lastUpdateTimestamp = Date.now();
173+
state.lastError = null;
174+
},
175+
);
176+
177+
return { result: Promise.resolve(resolvedTxId) };
178+
}
179+
180+
/**
181+
* Return MarketInfo for mock markets so usePerpsMarketData finds the asset by symbol
182+
* (MarketInfo.name is the universe / ticker, e.g. ETH). Without this, default HyperLiquid
183+
* HTTP mocks return empty meta and the order form shows "Invalid asset" before placing a trade.
184+
*/
185+
async getMarkets(params?: GetMarketsParams): Promise<MarketInfo[]> {
186+
const mockMarkets = this.mockService.getMockMarkets();
187+
const marketInfos: MarketInfo[] = mockMarkets.map((m) => ({
188+
name: m.symbol,
189+
szDecimals: E2E_MARKET_SZ_DECIMALS[m.symbol] ?? 4,
190+
maxLeverage: parseMockMaxLeverageFormatted(m.maxLeverage),
191+
marginTableId: 1,
192+
}));
193+
194+
const symbols = params?.symbols;
195+
if (symbols?.length) {
196+
const wanted = new Set(symbols.map((s) => s.toUpperCase()));
197+
return marketInfos.filter((mi) => wanted.has(mi.name.toUpperCase()));
198+
}
199+
200+
return marketInfos;
201+
}
202+
203+
// Mock one-click trade deposit initialization used by navigateToOrder()
204+
async depositWithOrder(): Promise<{ result: Promise<string> }> {
205+
return this.mockPerpsDepositTransaction({
206+
fallbackTxId: `0xmock_perps_deposit_with_order_${Date.now()}`,
207+
transactionType: 'perpsDepositAndOrder',
208+
});
209+
}
210+
211+
// Mock generic deposit initialization used by add-funds flows
212+
async depositWithConfirmation(): Promise<{ result: Promise<string> }> {
213+
return this.mockPerpsDepositTransaction({
214+
fallbackTxId: `0xmock_perps_deposit_with_confirmation_${Date.now()}`,
215+
transactionType: 'perpsDeposit',
216+
});
217+
}
218+
61219
// Mock liquidation price calculation: return entry price (0% distance scenario)
62220
async calculateLiquidationPrice(
63221
params: LiquidationPriceParams,
@@ -254,9 +412,12 @@ export function applyE2EPerpsControllerMocks(controller: unknown): void {
254412
// Override key methods with E2E mocks
255413
const methodsToOverride = [
256414
'placeOrder',
415+
'depositWithOrder',
416+
'depositWithConfirmation',
257417
'cancelOrder',
258418
'getAccountState',
259419
'getPositions',
420+
'getMarkets',
260421
'closePosition',
261422
'updatePositionTPSL',
262423
'calculateLiquidationPrice',
@@ -298,6 +459,17 @@ export function applyE2EPerpsControllerMocks(controller: unknown): void {
298459
: {};
299460
const providerRecord = provider as Record<string, unknown>;
300461

462+
// Keep connection manager healthy in E2E by forcing provider health/status to connected.
463+
providerRecord.ping = async () => undefined;
464+
providerRecord.getWebSocketConnectionState = () =>
465+
WebSocketConnectionState.Connected;
466+
providerRecord.subscribeToConnectionState = (
467+
listener: (state: WebSocketConnectionState, attempt: number) => void,
468+
) => {
469+
setTimeout(() => listener(WebSocketConnectionState.Connected, 0), 0);
470+
return () => undefined;
471+
};
472+
301473
// Patch only the methods we need for Activity history
302474
providerRecord.getOrders = (
303475
overrides.getOrders as (...args: unknown[]) => unknown
@@ -308,6 +480,9 @@ export function applyE2EPerpsControllerMocks(controller: unknown): void {
308480
providerRecord.getFunding = (
309481
overrides.getFunding as (...args: unknown[]) => unknown
310482
).bind(overrides);
483+
providerRecord.getMarkets = (
484+
overrides.getMarkets as (...args: unknown[]) => unknown
485+
).bind(overrides);
311486
providerRecord.getUserHistory = () => {
312487
const service = PerpsE2EMockService.getInstance();
313488
return service.getMockUserHistory();

0 commit comments

Comments
 (0)