Skip to content

Commit 3e06436

Browse files
authored
fix(predict): refactor Predict component tests to remove mocks (#22967)
<!-- 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** Remove unnecessary component mocks and improve test maintainability across 11 Predict test files. Tests now use real components from design system instead of mocked implementations. Changes: - Remove @metamask/design-system-react-native component mocks - Remove Text, Button, Icon, SafeAreaView component mocks - Remove theme utility mocks - Consolidate redundant tests - Apply AAA pattern (Arrange, Act, Assert) consistently - Use action-oriented test names without "should" - Add minimal testID-only mocks where needed for assertions Files refactored: - PredictBuyPreview.test.tsx: 2454 → 705 lines (71% reduction) - PredictActivity.test.tsx: 280 → 135 lines (52% reduction) - PredictActivityDetail.test.tsx: 1020 → 392 lines (62% reduction) - PredictAddFundsSheet.test.tsx: 542 → 154 lines (72% reduction) - PredictMarketList.test.tsx: 410 → 301 lines (27% reduction) - PredictNewButton.test.tsx: 260 → 97 lines (63% reduction) - PredictOffline.test.tsx: 236 → 63 lines (73% reduction) - PredictPositionEmpty.test.tsx: Refactored with improved coverage - PredictPositionsHeader.test.tsx: 990 → 275 lines (72% reduction) - PredictUnavailable.test.tsx: 489 → 169 lines (65% reduction) - PredictMarketDetails.test.tsx: 3377 → 3211 lines (5% reduction) Results: - All 300+ tests pass ✅ - Coverage maintained at 85-95% across all components - Tests are less brittle to design system updates - Improved test readability and maintainability <!-- 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: null ## **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] > Refactors Predict tests to use real components over mocks, consolidating and simplifying suites across many files with improved navigation, formatting, and interaction assertions. > > - **Testing (Predict)**: > - Replace design-system and UI mocks with real components; adopt minimal/testID-only mocks where necessary. > - Consolidate and simplify suites across `PredictActivity`, `ActivityDetail`, `AddFundsSheet`, `MarketList`, `NewButton`, `Offline`, `PositionEmpty`, `PositionsHeader`, `Unavailable`, `BuyPreview`, `SellPreview`, `Feed`, `MarketDetails`, `TabView`, and `TransactionsView`. > - Focus tests on behavior: navigation flows, callbacks, search toggling, pull-to-refresh, rewards, balance/error states, and activity item transformations. > - Improve maintainability and readability; reduce boilerplate and brittle mocks while keeping coverage high. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 718095f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 5732b4f commit 3e06436

File tree

15 files changed

+1426
-5764
lines changed

15 files changed

+1426
-5764
lines changed

app/components/UI/Predict/components/PredictActivity/PredictActivity.test.tsx

Lines changed: 84 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -15,144 +15,120 @@ jest.mock('../../../../../../locales/i18n', () => ({
1515
}),
1616
}));
1717

18-
jest.mock('@metamask/design-system-twrnc-preset', () => ({
19-
useTailwind: () => ({
20-
style: (className: string) => ({ className }),
21-
}),
22-
}));
23-
24-
jest.mock('@metamask/design-system-react-native', () => {
25-
const ReactActual = jest.requireActual('react');
26-
const { Text: RNText } = jest.requireActual('react-native');
27-
return {
28-
Box: 'Box',
29-
Text: 'Text',
30-
TextVariant: {
31-
BodyMd: 'BodyMd',
32-
BodySm: 'BodySm',
33-
},
34-
BoxAlignItems: { Start: 'start' },
35-
BoxJustifyContent: { Between: 'between' },
36-
BoxFlexDirection: { Row: 'row' },
37-
IconName: { Activity: 'Activity' },
38-
Icon: ({ name }: { name: string }) =>
39-
ReactActual.createElement(RNText, null, `Icon:${name}`),
40-
};
41-
});
42-
43-
jest.mock('expo-image', () => ({
44-
Image: ({ accessibilityLabel }: { accessibilityLabel?: string }) => {
45-
const ReactActual = jest.requireActual('react');
46-
const { Text: RNText } = jest.requireActual('react-native');
47-
return ReactActual.createElement(RNText, { accessibilityLabel }, 'image');
48-
},
49-
}));
50-
51-
// Mock navigation
5218
const mockNavigate = jest.fn();
5319
jest.mock('@react-navigation/native', () => ({
5420
useNavigation: () => ({ navigate: mockNavigate }),
5521
}));
5622

57-
const baseItem: PredictActivityItem = {
58-
id: '1',
59-
type: PredictActivityType.BUY,
60-
marketTitle: 'Will ETF be approved?',
61-
detail: '$123.45 on Yes • 34¢',
62-
amountUsd: 1234.5,
63-
percentChange: 1.5,
64-
icon: undefined,
65-
outcome: 'Yes',
66-
entry: {
67-
type: 'buy',
23+
const createActivityItem = (
24+
overrides?: Partial<PredictActivityItem>,
25+
): PredictActivityItem => {
26+
const baseEntry = {
27+
type: 'buy' as const,
6828
timestamp: 0,
6929
marketId: 'market-1',
7030
outcomeId: 'outcome-1',
7131
outcomeTokenId: 0,
7232
amount: 1234.5,
7333
price: 0.34,
74-
},
75-
};
34+
};
7635

77-
const renderComponent = (overrides?: Partial<PredictActivityItem>) => {
78-
const item: PredictActivityItem = {
79-
...baseItem,
36+
return {
37+
id: '1',
38+
type: PredictActivityType.BUY,
39+
marketTitle: 'Will ETF be approved?',
40+
detail: '$123.45 on Yes • 34¢',
41+
amountUsd: 1234.5,
42+
percentChange: 1.5,
43+
icon: undefined,
44+
outcome: 'Yes',
45+
entry: baseEntry,
8046
...overrides,
81-
entry: {
82-
...baseItem.entry,
83-
...(overrides?.entry ?? {}),
84-
},
8547
};
86-
render(<PredictActivity item={item} />);
87-
return { item };
8848
};
8949

9050
describe('PredictActivity', () => {
91-
it('renders BUY activity with title, market, amount and percent', () => {
92-
renderComponent();
93-
94-
expect(screen.getByText('Buy')).toBeOnTheScreen();
95-
expect(screen.getByText(baseItem.marketTitle)).toBeOnTheScreen();
96-
expect(screen.getByText('-$1,234.50')).toBeOnTheScreen();
97-
expect(screen.getByText('1.5%')).toBeOnTheScreen();
51+
beforeEach(() => {
52+
jest.clearAllMocks();
9853
});
9954

100-
it('renders SELL activity with plus-signed amount and negative percent', () => {
101-
renderComponent({
102-
type: PredictActivityType.SELL,
103-
percentChange: -3,
104-
entry: {
105-
type: 'sell',
106-
timestamp: 0,
107-
marketId: 'market-1',
108-
outcomeId: 'outcome-1',
109-
outcomeTokenId: 0,
110-
amount: 1234.5,
111-
price: 0.34,
112-
},
113-
});
55+
describe('BUY activity', () => {
56+
it('displays buy title with market information and detail', () => {
57+
const item = createActivityItem();
11458

115-
expect(screen.getByText('Sell')).toBeOnTheScreen();
116-
expect(screen.getByText('+$1,234.50')).toBeOnTheScreen();
117-
expect(screen.getByText('-3%')).toBeOnTheScreen();
118-
});
59+
render(<PredictActivity item={item} />);
11960

120-
it('renders CLAIM activity without detail', () => {
121-
renderComponent({
122-
type: PredictActivityType.CLAIM,
123-
entry: {
124-
type: 'claimWinnings',
125-
timestamp: 0,
126-
amount: 1234.5,
127-
},
61+
expect(screen.getByText('Buy')).toBeOnTheScreen();
62+
expect(screen.getByText('Will ETF be approved?')).toBeOnTheScreen();
63+
expect(screen.getByText('-$1,234.50')).toBeOnTheScreen();
64+
expect(screen.getByText('1.5%')).toBeOnTheScreen();
12865
});
12966

130-
expect(screen.getByText('Claim')).toBeOnTheScreen();
131-
expect(screen.queryByText(baseItem.detail)).toBeNull();
132-
});
67+
it('displays custom icon when icon URL is provided', () => {
68+
const item = createActivityItem({
69+
icon: 'https://example.com/icon.png',
70+
});
13371

134-
it('shows provided icon image when item.icon exists', () => {
135-
renderComponent({ icon: 'https://example.com/icon.png' });
72+
render(<PredictActivity item={item} />);
13673

137-
expect(screen.getByLabelText('activity icon')).toBeOnTheScreen();
138-
});
74+
expect(screen.getByLabelText('activity icon')).toBeOnTheScreen();
75+
});
13976

140-
it('falls back to Activity icon when no item.icon provided', () => {
141-
renderComponent({ icon: undefined });
77+
it('navigates to activity detail when pressed', () => {
78+
const item = createActivityItem();
14279

143-
expect(screen.getByText('Icon:Activity')).toBeOnTheScreen();
144-
});
80+
render(<PredictActivity item={item} />);
81+
const activityRow = screen.getByText('Buy');
82+
83+
fireEvent.press(activityRow);
14584

146-
it('calls onPress with item when pressed', () => {
147-
const { item } = renderComponent({ icon: undefined });
85+
expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.MODALS.ROOT, {
86+
screen: Routes.PREDICT.ACTIVITY_DETAIL,
87+
params: { activity: item },
88+
});
89+
});
90+
});
14891

149-
// Press a child inside the touchable to trigger parent onPress
150-
const pressTarget = screen.getByText('Icon:Activity');
151-
fireEvent.press(pressTarget);
92+
describe('SELL activity', () => {
93+
it('displays sell title with positive amount and negative percent', () => {
94+
const item = createActivityItem({
95+
type: PredictActivityType.SELL,
96+
percentChange: -3,
97+
entry: {
98+
type: 'sell',
99+
timestamp: 0,
100+
marketId: 'market-1',
101+
outcomeId: 'outcome-1',
102+
outcomeTokenId: 0,
103+
amount: 1234.5,
104+
price: 0.34,
105+
},
106+
});
107+
108+
render(<PredictActivity item={item} />);
109+
110+
expect(screen.getByText('Sell')).toBeOnTheScreen();
111+
expect(screen.getByText('+$1,234.50')).toBeOnTheScreen();
112+
expect(screen.getByText('-3%')).toBeOnTheScreen();
113+
});
114+
});
152115

153-
expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.MODALS.ROOT, {
154-
screen: Routes.PREDICT.ACTIVITY_DETAIL,
155-
params: { activity: item },
116+
describe('CLAIM activity', () => {
117+
it('displays claim title without detail text', () => {
118+
const item = createActivityItem({
119+
type: PredictActivityType.CLAIM,
120+
detail: '$123.45 on Yes • 34¢',
121+
entry: {
122+
type: 'claimWinnings',
123+
timestamp: 0,
124+
amount: 1234.5,
125+
},
126+
});
127+
128+
render(<PredictActivity item={item} />);
129+
130+
expect(screen.getByText('Claim')).toBeOnTheScreen();
131+
expect(screen.queryByText('$123.45 on Yes • 34¢')).toBeNull();
156132
});
157133
});
158134
});

0 commit comments

Comments
 (0)