Skip to content

Commit ffc7e29

Browse files
test(perps): expand component view coverage for market details, list,… (#26083)
Added component view tests for Perps section on increase coverage <!-- 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** ## Summary Improves Perps component-view flow tests: less duplication, faster runs, and more components covered. ## Changes ### 1. i18n strings as variables (no hardcoded keys) - **PerpsOrderLifecycleFlow**: `ORDER_TYPE_TITLE`, `ORDER_TYPE_MARKET`, `ORDER_TYPE_LIMIT`, `QUOTE_NETWORK_FEE`, `QUOTE_ESTIMATED_TIME`, `QUOTE_RATE`, `DONE_BUTTON`, `LIQUIDATION_PRICE` in `beforeAll`. - **PerpsPortfolioFlow**: `SEE_ALL_PERPS`, `POSITIONS`, `ACCOUNT_SUMMARY_TITLE`, `EMPTY_TITLE`, `FIRST_TIME_DESCRIPTION`, `ADD_MARGIN`, `REDUCE_MARGIN`. - **PerpsActiveTraderFlow**: `MARKET_ORDERS`, `LEVERAGE_MODAL_TITLE`, `LIMIT_PRICE_MODAL_TITLE`, `LIMIT_PRICE_MID`, `LIMIT_PRICE_BID`, `CLOSE_ALL_TITLE`, `CANCEL_ALL_TITLE`, `ADJUST_MARGIN_TITLE`, `ADD_MARGIN`, `REDUCE_MARGIN`. - **PerpsErrorsAndInfoFlow**: `CONNECTION_FAILED_TITLE`, `CONNECTION_FAILED_RETRY`, `CONNECTION_FAILED_GO_BACK`, `GOT_IT_BUTTON`. - **PerpsMarketAndRiskFlow**: `NOTIFICATIONS_TITLE`, `SORT_SORT_BY`, `SORT_APPLY`. All of these are set once in `beforeAll` and reused in the tests instead of repeating `strings('perps....')`. ### 2. Test execution speed - **PerpsOrderLifecycleFlow**: `TIMEOUT_MS = 3000`; all `findBy*` use it instead of 10s/5s so failures are detected sooner. - **PerpsPortfolioFlow**: One `findBy*` per phase (with `TIMEOUT_MS`), then `getBy*` for the rest to avoid extra async waits. ### 3. New coverage in existing flow tests **PerpsOrderLifecycleFlow** - **Phase 13 – PerpsTransactionsView**: Renders Activity, asserts Trades/Orders/Funding/Deposits tabs and tab switching. - **Phase 14 – PerpsSelectOrderTypeView**: Renders order type view (route + params), asserts title, Market/Limit options, and Limit press. **PerpsMarketAndRiskFlow** - **Phase 8 – PerpsMarketSortFieldBottomSheet**: Renders sort sheet, asserts "Sort by" and "Apply", selects Volume and Apply, asserts `onOptionSelect`. - **Phase 9 – PerpsTransactionItem**: Renders item with sample transaction, asserts testID, title/subtitle, and `onPress` with transaction. - **Phase 10 – TradingViewChart**: Renders chart with `candleData: null`, asserts container testID. - **Phase 11 – PerpsNotificationTooltip**: Renders with `orderSuccess={false}`, asserts notification sheet title is not shown. ### 4. Components covered by these flows - **OrderLifecycleFlow**: PerpsClosePositionView, PerpsOrderBookView, PerpsOrderDetailsView, PerpsHeroCardView, PerpsWithdrawView, PerpsSelectProviderView, PerpsOrderTypeBottomSheet, PerpsQuoteDetailsCard, PerpsQuoteExpiredModal, PerpsAdjustMarginView, **PerpsTransactionsView**, **PerpsSelectOrderTypeView**. - **MarketAndRiskFlow**: PerpsLoader, LivePriceDisplay, PerpsMarketRowItem, PerpsMarketTradesList, PerpsStopLossPromptBanner, PerpsNotificationBottomSheet, PerpsTransactionDetailAssetHero, **PerpsMarketSortFieldBottomSheet**, **PerpsTransactionItem**, **TradingViewChart**, **PerpsNotificationTooltip**. ## Testing - Run flow tests: `yarn jest PerpsOrderLifecycleFlow.view.test PerpsMarketAndRiskFlow.view.test PerpsPortfolioFlow.view.test PerpsActiveTraderFlow.view.test PerpsErrorsAndInfoFlow.view.test --no-coverage` <!-- 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 adds/updates view tests, but it also changes shared Jest view-test setup and core Engine/controller mocks, which could affect many tests and introduce CI flakiness. > > **Overview** > Adds several new Perps *E2E-like* component-view flow tests covering trading (tabs/orders/modify/flip/leverage/limit/cross-margin), errors+info (connection recovery, tooltips, badges, fill tags), market+risk (loaders, market rows, stop-loss prompts, notifications, sorting, transactions, chart), order lifecycle (provider switching, order type, quotes, adjust margin, activity tabs), portfolio (tab/home/positions/geo-block/empty states), plus new standalone tests for `PerpsTPSLView` and `PerpsTransactionsView`. > > Expands the Perps view-test harness (`perpsViewRenderer`) with many new render helpers, default fixtures, order stream overrides, and a `topOfBook` channel shim; updates shared test setup/mocks (`testSetupView.js`, component-view `mocks.ts`, `perpsStatePreset`) to stub additional React Native propTypes, Engine controller methods/state, and missing Redux slices; removes older narrower view tests now covered by the flow suites and tweaks a single onboarding perf timer threshold. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 94bd6ef. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: javiergarciavera <javiergarciavera@users.noreply.github.com>
1 parent 9840ef0 commit ffc7e29

16 files changed

Lines changed: 2977 additions & 96 deletions

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

Lines changed: 497 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
/**
2+
* Errors, Recovery & Information Flow — E2E-like view test.
3+
*
4+
* Simulates a trader encountering connectivity issues, retrying through
5+
* multiple error states, recovering, and then browsing informational UI
6+
* (tooltips, market badges, fill tags).
7+
*
8+
* Components covered: PerpsLoadingSkeleton, PerpsConnectionErrorView,
9+
* PerpsErrorState, PerpsTooltipView, PerpsBadge, PerpsFillTag
10+
*/
11+
import '../../../../../tests/component-view/mocks';
12+
import React from 'react';
13+
import { cleanup, fireEvent, screen } from '@testing-library/react-native';
14+
import { strings } from '../../../../../locales/i18n';
15+
import {
16+
renderPerpsView,
17+
renderPerpsComponent,
18+
renderPerpsComponentDisconnected,
19+
renderPerpsTooltipView,
20+
} from '../../../../../tests/component-view/renderers/perpsViewRenderer';
21+
import PerpsConnectionErrorView from '../components/PerpsConnectionErrorView/PerpsConnectionErrorView';
22+
import PerpsErrorState, {
23+
PerpsErrorType,
24+
} from '../components/PerpsErrorState/PerpsErrorState';
25+
import PerpsLoadingSkeleton from '../components/PerpsLoadingSkeleton/PerpsLoadingSkeleton';
26+
import PerpsBadge from '../components/PerpsBadge/PerpsBadge';
27+
import PerpsFillTag from '../components/PerpsFillTag/PerpsFillTag';
28+
import { FillType, type PerpsTransaction } from '../types/transactionHistory';
29+
30+
type BadgeType = 'experimental' | 'equity' | 'commodity' | 'crypto' | 'forex';
31+
32+
const mockOnRetry = jest.fn();
33+
34+
const ConnectionErrorDefault: React.FC = () => (
35+
<PerpsConnectionErrorView error="Connection failed" onRetry={mockOnRetry} />
36+
);
37+
38+
const ConnectionErrorWithBack: React.FC = () => (
39+
<PerpsConnectionErrorView
40+
error="Connection failed"
41+
onRetry={mockOnRetry}
42+
retryAttempts={1}
43+
/>
44+
);
45+
46+
const ConnectionErrorRetrying: React.FC = () => (
47+
<PerpsConnectionErrorView
48+
error="Connection failed"
49+
onRetry={mockOnRetry}
50+
isRetrying
51+
/>
52+
);
53+
54+
const renderErrorState = (errorType: PerpsErrorType, onRetry?: () => void) =>
55+
renderPerpsComponent(
56+
PerpsErrorState as unknown as React.ComponentType<Record<string, unknown>>,
57+
{ errorType, onRetry },
58+
);
59+
60+
const renderBadge = (type: BadgeType, customLabel?: string) =>
61+
renderPerpsComponent(
62+
PerpsBadge as unknown as React.ComponentType<Record<string, unknown>>,
63+
{ type, customLabel },
64+
);
65+
66+
const baseTransaction: PerpsTransaction = {
67+
id: 'tx_1',
68+
type: 'trade',
69+
category: 'position_close',
70+
title: 'Closed long',
71+
subtitle: '2.5 ETH',
72+
timestamp: Date.now(),
73+
asset: 'ETH',
74+
fill: {
75+
shortTitle: 'Closed long',
76+
amount: '+$100',
77+
amountNumber: 100,
78+
isPositive: true,
79+
size: '2.5',
80+
entryPrice: '2000',
81+
points: '10',
82+
pnl: '+$100',
83+
fee: '$1',
84+
action: 'close',
85+
feeToken: 'USDC',
86+
fillType: FillType.Standard,
87+
},
88+
};
89+
90+
const renderFillTag = (fillType: FillType) =>
91+
renderPerpsComponent(
92+
PerpsFillTag as unknown as React.ComponentType<Record<string, unknown>>,
93+
{
94+
transaction: {
95+
...baseTransaction,
96+
fill: { ...baseTransaction.fill, fillType },
97+
},
98+
},
99+
);
100+
101+
describe('Errors, Recovery & Information Flow', () => {
102+
let CONNECTION_FAILED_TITLE: string;
103+
let CONNECTION_FAILED_RETRY: string;
104+
let CONNECTION_FAILED_GO_BACK: string;
105+
let GOT_IT_BUTTON: string;
106+
107+
beforeAll(() => {
108+
CONNECTION_FAILED_TITLE = strings('perps.errors.connectionFailed.title');
109+
CONNECTION_FAILED_RETRY = strings('perps.errors.connectionFailed.retry');
110+
CONNECTION_FAILED_GO_BACK = strings(
111+
'perps.errors.connectionFailed.go_back',
112+
);
113+
GOT_IT_BUTTON = strings('perps.tooltips.got_it_button');
114+
});
115+
116+
beforeEach(() => {
117+
mockOnRetry.mockClear();
118+
});
119+
120+
it('trader encounters connection issues, retries through error states, recovers, then browses tooltips, badges, and fill tags', async () => {
121+
// ── PHASE 1: Connection attempt ──────────────────────────────────────
122+
// Trader opens Perps, sees loading skeleton while connecting
123+
renderPerpsComponentDisconnected(
124+
PerpsLoadingSkeleton as unknown as React.ComponentType<
125+
Record<string, unknown>
126+
>,
127+
);
128+
expect(
129+
await screen.findByText(strings('perps.connection.connecting_to_perps')),
130+
).toBeOnTheScreen();
131+
132+
// ── PHASE 2: Connection failure and retry cycle ──────────────────────
133+
// Connection fails — trader sees error title and retry button, no go-back
134+
cleanup();
135+
renderPerpsView(ConnectionErrorDefault, 'ConnectionErrorTest');
136+
expect(await screen.findByText(CONNECTION_FAILED_TITLE)).toBeOnTheScreen();
137+
const retryButton = screen.getByText(CONNECTION_FAILED_RETRY);
138+
expect(retryButton).toBeOnTheScreen();
139+
expect(screen.queryByText(CONNECTION_FAILED_GO_BACK)).not.toBeOnTheScreen();
140+
141+
// Trader presses retry
142+
fireEvent.press(retryButton);
143+
expect(mockOnRetry).toHaveBeenCalledTimes(1);
144+
145+
// Retry fails — "Go back" button now appears alongside retry
146+
cleanup();
147+
mockOnRetry.mockClear();
148+
renderPerpsView(ConnectionErrorWithBack, 'ConnectionErrorTest');
149+
expect(
150+
await screen.findByText(CONNECTION_FAILED_GO_BACK),
151+
).toBeOnTheScreen();
152+
expect(screen.getByText(CONNECTION_FAILED_RETRY)).toBeOnTheScreen();
153+
154+
// Trader retries again — spinner replaces the retry label
155+
cleanup();
156+
renderPerpsView(ConnectionErrorRetrying, 'ConnectionErrorTest');
157+
await screen.findByText(CONNECTION_FAILED_TITLE);
158+
expect(screen.queryByText(CONNECTION_FAILED_RETRY)).not.toBeOnTheScreen();
159+
160+
// ── PHASE 3: Error state variants ────────────────────────────────────
161+
// CONNECTION_FAILED error state — retry fires callback
162+
cleanup();
163+
const retryFn1 = jest.fn();
164+
renderErrorState(PerpsErrorType.CONNECTION_FAILED, retryFn1);
165+
expect(await screen.findByText(CONNECTION_FAILED_TITLE)).toBeOnTheScreen();
166+
fireEvent.press(screen.getByText(CONNECTION_FAILED_RETRY));
167+
expect(retryFn1).toHaveBeenCalledTimes(1);
168+
169+
// NETWORK_ERROR — title, description, and retry
170+
cleanup();
171+
const retryFn2 = jest.fn();
172+
renderErrorState(PerpsErrorType.NETWORK_ERROR, retryFn2);
173+
expect(
174+
await screen.findByText(strings('perps.errors.networkError.title')),
175+
).toBeOnTheScreen();
176+
expect(
177+
screen.getByText(strings('perps.errors.networkError.description')),
178+
).toBeOnTheScreen();
179+
fireEvent.press(
180+
screen.getByText(strings('perps.errors.networkError.retry')),
181+
);
182+
expect(retryFn2).toHaveBeenCalledTimes(1);
183+
184+
// UNKNOWN without retry — no retry button
185+
cleanup();
186+
renderErrorState(PerpsErrorType.UNKNOWN);
187+
expect(
188+
await screen.findByText(strings('perps.errors.unknown.title')),
189+
).toBeOnTheScreen();
190+
expect(
191+
screen.getByText(strings('perps.errors.unknown.description')),
192+
).toBeOnTheScreen();
193+
expect(
194+
screen.queryByText(strings('perps.errors.unknown.retry')),
195+
).not.toBeOnTheScreen();
196+
197+
// UNKNOWN with retry — retry button fires callback
198+
cleanup();
199+
const retryFn3 = jest.fn();
200+
renderErrorState(PerpsErrorType.UNKNOWN, retryFn3);
201+
fireEvent.press(
202+
await screen.findByText(strings('perps.errors.unknown.retry')),
203+
);
204+
expect(retryFn3).toHaveBeenCalledTimes(1);
205+
206+
// ── PHASE 4: Trader recovers and browses tooltips ────────────────────
207+
// Trader reads the leverage tooltip, then dismisses
208+
cleanup();
209+
renderPerpsTooltipView({ contentKey: 'leverage' });
210+
expect(
211+
await screen.findByText(strings('perps.tooltips.leverage.title')),
212+
).toBeOnTheScreen();
213+
const gotItButton = screen.getByText(GOT_IT_BUTTON);
214+
fireEvent.press(gotItButton);
215+
216+
// Trader opens margin tooltip
217+
cleanup();
218+
renderPerpsTooltipView({ contentKey: 'margin' });
219+
expect(
220+
await screen.findByText(strings('perps.tooltips.margin.title')),
221+
).toBeOnTheScreen();
222+
223+
// Trader opens fees tooltip
224+
cleanup();
225+
renderPerpsTooltipView({ contentKey: 'fees' });
226+
expect(
227+
await screen.findByText(strings('perps.tooltips.fees.title')),
228+
).toBeOnTheScreen();
229+
expect(screen.getByText(GOT_IT_BUTTON)).toBeOnTheScreen();
230+
231+
// Trader opens liquidation price tooltip
232+
cleanup();
233+
renderPerpsTooltipView({ contentKey: 'liquidation_price' });
234+
expect(
235+
await screen.findByText(
236+
strings('perps.tooltips.liquidation_price.title'),
237+
),
238+
).toBeOnTheScreen();
239+
240+
// ── PHASE 5: Market badges across asset classes ──────────────────────
241+
const badgeTypes: BadgeType[] = [
242+
'experimental',
243+
'equity',
244+
'commodity',
245+
'crypto',
246+
'forex',
247+
];
248+
for (const type of badgeTypes) {
249+
cleanup();
250+
renderBadge(type);
251+
expect(
252+
await screen.findByText(strings(`perps.market.badge.${type}`)),
253+
).toBeOnTheScreen();
254+
}
255+
256+
// Custom label overrides default badge
257+
cleanup();
258+
renderBadge('crypto', 'CUSTOM');
259+
expect(await screen.findByText('CUSTOM')).toBeOnTheScreen();
260+
261+
// ── PHASE 6: Fill tags on past transactions ──────────────────────────
262+
// Standard fill — no tag visible
263+
cleanup();
264+
renderFillTag(FillType.Standard);
265+
expect(
266+
screen.queryByText(strings('perps.transactions.order.take_profit')),
267+
).not.toBeOnTheScreen();
268+
expect(
269+
screen.queryByText(strings('perps.transactions.order.stop_loss')),
270+
).not.toBeOnTheScreen();
271+
272+
// TakeProfit fill — tag visible
273+
cleanup();
274+
renderFillTag(FillType.TakeProfit);
275+
expect(
276+
await screen.findByText(strings('perps.transactions.order.take_profit')),
277+
).toBeOnTheScreen();
278+
279+
// StopLoss fill — tag visible
280+
cleanup();
281+
renderFillTag(FillType.StopLoss);
282+
expect(
283+
await screen.findByText(strings('perps.transactions.order.stop_loss')),
284+
).toBeOnTheScreen();
285+
286+
// AutoDeleveraging fill — tag visible
287+
cleanup();
288+
renderFillTag(FillType.AutoDeleveraging);
289+
expect(
290+
await screen.findByText(
291+
strings('perps.transactions.order.auto_deleveraging'),
292+
),
293+
).toBeOnTheScreen();
294+
});
295+
});

0 commit comments

Comments
 (0)