Skip to content

Commit 7a54f51

Browse files
authored
test: Predict e2e remove and cv tests addition (#29913)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** Migrated part of Predict smoke E2E coverage to component-view tests and reduced redundant E2E surface while preserving key integration assurance. Main updates: - Removed E2E tests for API-down and existing-balance scenarios, and replaced their intent with CV assertions in `PredictFeed.view.test.tsx`. - Strengthened geoblock CV coverage to validate both tracking and navigation to Predict unavailable modal flows. - Kept a thin geoblock E2E canary (`predict-geo-restriction.spec.ts`) for native/timing/modal integration confidence. - Updated open-position E2E to enter Predict via Wallet homepage section (`scrollAndTapPredictionsSection`) so homepage wiring coverage is retained. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [MMQA-1762](https://consensyssoftware.atlassian.net/browse/MMQA-1762) ## **Manual testing steps** ```gherkin Feature: Predict E2E to CV migration Scenario: Validate retained geoblock integration canary Given a clean test fixture with Predict enabled and geo-blocked eligibility When user logs in, opens Actions, enters Predict, and taps Add funds Then the Predict unavailable modal is displayed and can be dismissed back to Predict balance Scenario: Validate wallet homepage wiring remains covered in E2E Given a clean test fixture with Predict enabled When user logs in and opens Predict from Wallet homepage Predictions section Then the open-position flow can proceed from Predict feed to market details Scenario: Validate CV replacements for removed E2E checks Given Predict component-view tests are executed When market feed requests fail in PredictFeed Then offline/error state appears after retries When balance fetch resolves to a mocked amount Then PredictFeed shows available balance label and corresponding formatted amount When an ineligible action is attempted Then geo-block tracking is fired and navigation targets Predict modals root ``` ## **Screenshots/Recordings** ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] 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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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. [MMQA-1762]: https://consensyssoftware.atlassian.net/browse/MMQA-1762?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it removes multiple Predict smoke E2E specs and replaces them with component-view tests, which may miss some native navigation/timing integration issues despite added route-based assertions. > > **Overview** > Shifts Predict test coverage away from Detox smoke E2E toward component-view tests by significantly expanding `PredictFeed.view.test.tsx` to cover feed fetch retry/offline recovery, tab switching and market card rendering, balance amount display, and geo-blocked "Add funds" navigation/analytics. > > Updates component-view infrastructure to better support navigation assertions (adds `NavigationContext`/`NavigationRouteContext` stubbing in `renderPredictFeedView`, broadens `renderComponentViewScreen` options typing), and adds new `PredictFeed` test IDs (`HEADER`, `TAB_BAR_CONTAINER`, per-tab IDs) to enable layout-driven tests. > > Consolidates geo-block analytics expectations into `geoBlockedCombinedExpectations`, adjusts Predict open-position analytics to expect `entry_point: 'homepage_positions'`, simplifies the geo-restriction smoke canary to cover feed action + cashout + add funds in one scenario, and deletes redundant Predict smoke specs for API-down and existing balance. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3f2d0c9. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 4d76783 commit 7a54f51

12 files changed

Lines changed: 431 additions & 320 deletions

File tree

app/components/UI/Predict/Predict.testIds.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@ export const getPredictMarketListSelector = {
4848
// ========================================
4949

5050
export const PredictFeedSelectorsIDs = {
51+
HEADER: 'predict-feed-header',
52+
TAB_BAR_CONTAINER: 'predict-feed-tab-bar-container',
5153
TABS: 'predict-feed-tabs',
5254
PAGER: 'predict-feed-pager',
5355
} as const;
5456

5557
export const getPredictFeedSelector = {
58+
tab: (index: number) => `${PredictFeedSelectorsIDs.TABS}-tab-${index}`,
5659
tabPage: (key: string) => `predict-feed-tab-page-${key}`,
5760
emptyState: (category: string) => `predict-empty-state-${category}`,
5861
skeletonLoading: (category: string, index: number) =>

app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ const AnimatedHeader: React.FC<AnimatedHeaderProps> = ({
195195
]}
196196
>
197197
<Animated.View
198+
testID={PredictFeedSelectorsIDs.HEADER}
198199
ref={headerRef}
199200
style={animatedBalanceStyle}
200201
onLayout={onHeaderLayout}
@@ -208,7 +209,11 @@ const AnimatedHeader: React.FC<AnimatedHeaderProps> = ({
208209
</Box>
209210
)}
210211
</Animated.View>
211-
<View ref={tabBarRef} onLayout={onTabBarLayout}>
212+
<View
213+
ref={tabBarRef}
214+
onLayout={onTabBarLayout}
215+
testID={PredictFeedSelectorsIDs.TAB_BAR_CONTAINER}
216+
>
212217
<PredictFeedTabBar
213218
tabs={tabs}
214219
activeIndex={activeIndex}

app/components/UI/Predict/views/PredictFeed/PredictFeed.view.test.tsx

Lines changed: 274 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,99 @@ import {
1818
PredictMarketListSelectorsIDs,
1919
PredictSearchSelectorsIDs,
2020
PredictBalanceSelectorsIDs,
21+
PredictBalanceSelectorsText,
22+
PredictFeedSelectorsIDs,
23+
getPredictMarketListSelector,
24+
getPredictFeedSelector,
2125
getPredictSearchSelector,
2226
} from '../../Predict.testIds';
2327
import Routes from '../../../../../constants/navigation/Routes';
2428
import { MOCK_PREDICT_MARKET } from '../../../../../../tests/component-view/fixtures/predict';
29+
import { PREDICT_OFFLINE_TEST_IDS } from '../../components/PredictOffline/PredictOffline.testIds';
30+
import type { PredictMarket } from '../../types';
2531

2632
const SEARCH_PLACEHOLDER = 'Search prediction markets';
2733
const CANCEL_TEXT = 'Cancel';
34+
const RETRY_TEXT = 'Retry';
35+
36+
const createPredictMarket = ({
37+
id,
38+
category,
39+
title,
40+
yesPrice,
41+
noPrice,
42+
volume,
43+
}: {
44+
id: string;
45+
category: PredictMarket['category'];
46+
title: string;
47+
yesPrice: number;
48+
noPrice: number;
49+
volume: number;
50+
}): PredictMarket => ({
51+
...MOCK_PREDICT_MARKET,
52+
id,
53+
slug: id,
54+
title,
55+
category,
56+
volume,
57+
outcomes: [
58+
{
59+
...MOCK_PREDICT_MARKET.outcomes[0],
60+
id: `${id}-outcome`,
61+
marketId: id,
62+
title,
63+
volume,
64+
tokens: [
65+
{ id: `${id}-yes`, title: 'Yes', price: yesPrice },
66+
{ id: `${id}-no`, title: 'No', price: noPrice },
67+
],
68+
},
69+
],
70+
});
71+
72+
const TRENDING_MARKETS = [
73+
createPredictMarket({
74+
id: 'market-btc-100k',
75+
category: 'trending',
76+
title: 'Will Bitcoin reach $100k?',
77+
yesPrice: 0.65,
78+
noPrice: 0.35,
79+
volume: 1_000_000,
80+
}),
81+
createPredictMarket({
82+
id: 'market-eth-10k',
83+
category: 'trending',
84+
title: 'Will Ethereum reach $10k?',
85+
yesPrice: 0.42,
86+
noPrice: 0.58,
87+
volume: 250_000,
88+
}),
89+
] as const;
90+
91+
const NEW_MARKET = createPredictMarket({
92+
id: 'market-new-fed-rate',
93+
category: 'new',
94+
title: 'Will the Fed cut rates this month?',
95+
yesPrice: 0.51,
96+
noPrice: 0.49,
97+
volume: 125_000,
98+
});
99+
100+
const layoutPredictFeed = async ({
101+
findByTestId,
102+
}: Pick<ReturnType<typeof renderPredictFeedView>, 'findByTestId'>) => {
103+
fireEvent(await findByTestId(PredictFeedSelectorsIDs.HEADER), 'layout', {
104+
nativeEvent: { layout: { height: 160 } },
105+
});
106+
fireEvent(
107+
await findByTestId(PredictFeedSelectorsIDs.TAB_BAR_CONTAINER),
108+
'layout',
109+
{
110+
nativeEvent: { layout: { height: 48 } },
111+
},
112+
);
113+
};
28114

29115
describe('PredictFeed', () => {
30116
describe('search interaction', () => {
@@ -186,6 +272,163 @@ describe('PredictFeed', () => {
186272
});
187273
});
188274

275+
describe('market feed error recovery', () => {
276+
it('shows the offline state without market cards when all feed fetch retries fail', async () => {
277+
const getMarketsSpy = jest.spyOn(
278+
Engine.context.PredictController,
279+
'getMarkets',
280+
);
281+
getMarketsSpy.mockRejectedValue(new Error('Network error'));
282+
283+
const { findByTestId, queryByTestId } = renderPredictFeedView();
284+
285+
await layoutPredictFeed({ findByTestId });
286+
287+
expect(
288+
await findByTestId(
289+
PREDICT_OFFLINE_TEST_IDS.ERROR_STATE,
290+
{},
291+
{ timeout: 10000 },
292+
),
293+
).toBeOnTheScreen();
294+
expect(
295+
queryByTestId(
296+
getPredictMarketListSelector.marketCardByCategory('trending', 2),
297+
),
298+
).not.toBeOnTheScreen();
299+
300+
getMarketsSpy.mockRestore();
301+
});
302+
303+
it('loads market cards when the user retries after a feed error', async () => {
304+
const getMarketsSpy = jest.spyOn(
305+
Engine.context.PredictController,
306+
'getMarkets',
307+
);
308+
getMarketsSpy.mockRejectedValue(new Error('Network error'));
309+
310+
const { findByTestId, findByText, queryByTestId } =
311+
renderPredictFeedView();
312+
313+
await layoutPredictFeed({ findByTestId });
314+
await findByTestId(
315+
PREDICT_OFFLINE_TEST_IDS.ERROR_STATE,
316+
{},
317+
{ timeout: 10000 },
318+
);
319+
const callCountBeforeRetry = getMarketsSpy.mock.calls.length;
320+
getMarketsSpy.mockResolvedValue([...TRENDING_MARKETS]);
321+
322+
fireEvent.press(await findByText(RETRY_TEXT));
323+
324+
await waitFor(() => {
325+
expect(getMarketsSpy.mock.calls.length).toBeGreaterThan(
326+
callCountBeforeRetry,
327+
);
328+
});
329+
expect(
330+
await findByTestId(
331+
getPredictMarketListSelector.marketCardByCategory('trending', 1),
332+
),
333+
).toBeOnTheScreen();
334+
expect(queryByTestId(PREDICT_OFFLINE_TEST_IDS.ERROR_STATE)).toBeNull();
335+
336+
getMarketsSpy.mockRestore();
337+
});
338+
});
339+
340+
describe('market feed data', () => {
341+
it('shows complete market data for every loaded trending market', async () => {
342+
const getMarketsSpy = jest.spyOn(
343+
Engine.context.PredictController,
344+
'getMarkets',
345+
);
346+
getMarketsSpy.mockImplementation(({ category }) =>
347+
Promise.resolve(category === 'trending' ? [...TRENDING_MARKETS] : []),
348+
);
349+
350+
const { findByTestId } = renderPredictFeedView();
351+
352+
await layoutPredictFeed({ findByTestId });
353+
354+
const bitcoinCard = await findByTestId(
355+
getPredictMarketListSelector.marketCardByCategory('trending', 1),
356+
);
357+
expect(
358+
within(bitcoinCard).getByText(TRENDING_MARKETS[0].title),
359+
).toBeOnTheScreen();
360+
expect(within(bitcoinCard).getByText('65%')).toBeOnTheScreen();
361+
expect(within(bitcoinCard).getByText('Yes')).toBeOnTheScreen();
362+
expect(within(bitcoinCard).getByText('No')).toBeOnTheScreen();
363+
expect(within(bitcoinCard).getByText('$1M Vol.')).toBeOnTheScreen();
364+
365+
const ethereumCard = await findByTestId(
366+
getPredictMarketListSelector.marketCardByCategory('trending', 2),
367+
);
368+
expect(
369+
within(ethereumCard).getByText(TRENDING_MARKETS[1].title),
370+
).toBeOnTheScreen();
371+
expect(within(ethereumCard).getByText('42%')).toBeOnTheScreen();
372+
expect(within(ethereumCard).getByText('Yes')).toBeOnTheScreen();
373+
expect(within(ethereumCard).getByText('No')).toBeOnTheScreen();
374+
expect(within(ethereumCard).getByText('$250k Vol.')).toBeOnTheScreen();
375+
376+
getMarketsSpy.mockRestore();
377+
});
378+
379+
it('loads the selected category after the user switches tabs', async () => {
380+
const getMarketsSpy = jest.spyOn(
381+
Engine.context.PredictController,
382+
'getMarkets',
383+
);
384+
getMarketsSpy.mockImplementation(({ category }) => {
385+
if (category === 'trending') {
386+
return Promise.resolve([...TRENDING_MARKETS]);
387+
}
388+
if (category === 'new') {
389+
return Promise.resolve([NEW_MARKET]);
390+
}
391+
return Promise.resolve([]);
392+
});
393+
394+
const { findByTestId, getByTestId } = renderPredictFeedView();
395+
396+
await layoutPredictFeed({ findByTestId });
397+
await findByTestId(
398+
getPredictMarketListSelector.marketCardByCategory('trending', 1),
399+
);
400+
401+
fireEvent.press(getByTestId(getPredictFeedSelector.tab(2)));
402+
403+
const newMarketCard = await findByTestId(
404+
getPredictMarketListSelector.marketCardByCategory('new', 1),
405+
);
406+
const newTabPage = getByTestId(getPredictFeedSelector.tabPage('new'));
407+
const newTabScope = within(newTabPage);
408+
409+
expect(
410+
within(newMarketCard).getByText(NEW_MARKET.title),
411+
).toBeOnTheScreen();
412+
expect(
413+
newTabScope.queryByTestId(
414+
getPredictMarketListSelector.marketCardByCategory('trending', 1),
415+
),
416+
).not.toBeOnTheScreen();
417+
expect(
418+
newTabScope.queryByTestId(
419+
getPredictMarketListSelector.marketCardByCategory('new', 2),
420+
),
421+
).not.toBeOnTheScreen();
422+
await waitFor(() => {
423+
expect(getMarketsSpy).toHaveBeenCalledWith(
424+
expect.objectContaining({ category: 'new' }),
425+
);
426+
});
427+
428+
getMarketsSpy.mockRestore();
429+
});
430+
});
431+
189432
describe('back navigation', () => {
190433
it('navigates to the wallet when the user presses back from the root feed', async () => {
191434
const { getByTestId, findByTestId } = renderPredictFeedViewWithRoutes({
@@ -219,13 +462,38 @@ describe('PredictFeed', () => {
219462
getBalanceSpy.mockRestore();
220463
});
221464

465+
it('uses PredictController balance response to display available balance amount', async () => {
466+
const getBalanceSpy = jest.spyOn(
467+
Engine.context.PredictController,
468+
'getBalance',
469+
);
470+
getBalanceSpy.mockResolvedValue(28.16);
471+
472+
const { findByTestId, findByText } = renderPredictFeedView();
473+
474+
expect(
475+
await findByTestId(PredictBalanceSelectorsIDs.BALANCE_CARD),
476+
).toBeOnTheScreen();
477+
await waitFor(() => {
478+
expect(getBalanceSpy).toHaveBeenCalledTimes(1);
479+
});
480+
expect(
481+
await findByText(PredictBalanceSelectorsText.AVAILABLE_BALANCE),
482+
).toBeOnTheScreen();
483+
expect(await findByText('$28.16')).toBeOnTheScreen();
484+
485+
getBalanceSpy.mockRestore();
486+
});
487+
222488
it('calls trackGeoBlockTriggered when the user presses Add Funds while ineligible', async () => {
223489
const trackGeoBlockSpy = jest.spyOn(
224490
Engine.context.PredictController,
225491
'trackGeoBlockTriggered',
226492
);
227493

228-
const { findByTestId, findByText } = renderPredictFeedView();
494+
const { findByTestId, findByText } = renderPredictFeedViewWithRoutes({
495+
extraRoutes: [{ name: Routes.PREDICT.MODALS.ROOT }],
496+
});
229497

230498
await findByTestId(PredictBalanceSelectorsIDs.BALANCE_CARD);
231499
fireEvent.press(await findByText('Add funds'));
@@ -235,6 +503,9 @@ describe('PredictFeed', () => {
235503
expect.objectContaining({ attemptedAction: 'deposit' }),
236504
);
237505
});
506+
expect(
507+
await findByTestId(`route-${Routes.PREDICT.MODALS.ROOT}`),
508+
).toBeOnTheScreen();
238509

239510
trackGeoBlockSpy.mockRestore();
240511
});
@@ -258,7 +529,7 @@ describe('PredictFeed', () => {
258529
// findByTestId waits until the error state appears after all retries exhaust.
259530
expect(
260531
await findByTestId(
261-
PredictSearchSelectorsIDs.ERROR_STATE,
532+
PREDICT_OFFLINE_TEST_IDS.ERROR_STATE,
262533
{},
263534
{ timeout: 10000 },
264535
),
@@ -281,7 +552,7 @@ describe('PredictFeed', () => {
281552
await findByPlaceholderText(SEARCH_PLACEHOLDER);
282553

283554
await findByTestId(
284-
PredictSearchSelectorsIDs.ERROR_STATE,
555+
PREDICT_OFFLINE_TEST_IDS.ERROR_STATE,
285556
{},
286557
{ timeout: 10000 },
287558
);

0 commit comments

Comments
 (0)