Skip to content

Commit 1a0ed51

Browse files
authored
test: enhance DeFiSection tests with error handling and retry logic (#26773)
<!-- 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** The DeFi homepage section was returning null (hiding entirely) when the API responded with an error (e.g. 501). This matched the behavior for the empty-data case, but the acceptance criteria requires showing a retry UI on API failure — consistent with how the Predictions and Tokens sections handle errors. **This PR:** - Separates error and empty handling in DeFiSection: errors now render the shared ErrorState component with a retry button, while empty data (200 with 0 positions) still hides the section. - Replaces the no-op refresh function with a real one that calls DeFiPositionsController._executePoll(), so both the retry button and pull-to-refresh actually re-fetch. - Updates tests to assert the new error UI behavior and verify retry triggers _executePoll. ## **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: Fixed DeFi homepage section to show retry UI when API request fails instead of hiding the section ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TMCU-496 ## **Manual testing steps** ```gherkin Feature: DeFi section error handling on homepage Scenario: user sees retry UI when DeFi API fails Given the DeFi positions API returns a 501 error And the user navigates to the homepage When the homepage loads Then the DeFi section displays with a "Unable to load" message and a "Retry" button Scenario: user retries after API failure Given the DeFi section is showing the error/retry UI When user taps the "Retry" button Then the DeFi positions are re-fetched from the API Scenario: DeFi section hidden when no positions exist Given the DeFi positions API returns 200 with no positions When the homepage loads Then the DeFi section is not displayed ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <img width="377" height="613" alt="Screenshot 2026-03-02 at 13 21 18" src="https://github.com/user-attachments/assets/55d6b7e4-2187-4824-87c9-fa1161439e70" /> <!-- [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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes homepage DeFi rendering and refresh behavior to call `DeFiPositionsController._executePoll()`, which could affect polling frequency and error-handling paths. Uses a controller internal method, so regressions are possible if controller APIs change. > > **Overview** > DeFi homepage section no longer disappears on API errors: when `hasError` (and not loading) it now renders the shared `ErrorState` with a Retry button while still hiding the section for the *empty data* case. > > The section’s `refresh` handler (used by pull-to-refresh and Retry) is wired to `Engine.context.DeFiPositionsController._executePoll()` to actively re-fetch positions. > > Tests were updated to validate the new error UI and to assert that both Retry and the exposed `ref.refresh()` call `_executePoll()`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 296c330. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent be2b91c commit 1a0ed51

2 files changed

Lines changed: 53 additions & 8 deletions

File tree

app/components/Views/Homepage/Sections/DeFi/DeFiSection.test.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SectionRefreshHandle } from '../../types';
66
import Routes from '../../../../../constants/navigation/Routes';
77

88
const mockNavigate = jest.fn();
9+
const mockExecutePoll = jest.fn().mockResolvedValue(undefined);
910

1011
jest.mock('@react-navigation/native', () => ({
1112
...jest.requireActual('@react-navigation/native'),
@@ -14,6 +15,14 @@ jest.mock('@react-navigation/native', () => ({
1415
}),
1516
}));
1617

18+
jest.mock('../../../../../core/Engine', () => ({
19+
context: {
20+
DeFiPositionsController: {
21+
_executePoll: (...args: unknown[]) => mockExecutePoll(...args),
22+
},
23+
},
24+
}));
25+
1726
jest.mock(
1827
'../../../../../selectors/featureFlagController/assetsDefiPositions',
1928
() => ({
@@ -120,17 +129,36 @@ describe('DeFiSection', () => {
120129
expect(toJSON()).toBeNull();
121130
});
122131

123-
it('returns null when there is an error and not loading', () => {
132+
it('renders error state with retry when there is an error and not loading', () => {
124133
mockUseDeFiPositionsForHomepage.mockReturnValue({
125134
positions: [],
126135
isLoading: false,
127136
hasError: true,
128137
isEmpty: false,
129138
});
130139

131-
const { toJSON } = renderWithProvider(<DeFiSection />);
140+
renderWithProvider(<DeFiSection />);
132141

133-
expect(toJSON()).toBeNull();
142+
expect(screen.getByText('DeFi')).toBeOnTheScreen();
143+
expect(screen.getByText(/unable to load/i)).toBeOnTheScreen();
144+
expect(screen.getByText(/retry/i)).toBeOnTheScreen();
145+
});
146+
147+
it('calls _executePoll on retry button press', async () => {
148+
mockUseDeFiPositionsForHomepage.mockReturnValue({
149+
positions: [],
150+
isLoading: false,
151+
hasError: true,
152+
isEmpty: false,
153+
});
154+
155+
renderWithProvider(<DeFiSection />);
156+
157+
await act(async () => {
158+
fireEvent.press(screen.getByText(/retry/i));
159+
});
160+
161+
expect(mockExecutePoll).toHaveBeenCalled();
134162
});
135163

136164
it('renders skeleton when loading', () => {
@@ -195,6 +223,6 @@ describe('DeFiSection', () => {
195223
await ref.current?.refresh();
196224
});
197225

198-
// Refresh is a no-op for DeFi (data comes from controller)
226+
expect(mockExecutePoll).toHaveBeenCalled();
199227
});
200228
});

app/components/Views/Homepage/Sections/DeFi/DeFiSection.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { useTailwind } from '@metamask/design-system-twrnc-preset';
88
import { useTheme } from '../../../../../util/theme';
99
import SectionTitle from '../../components/SectionTitle';
1010
import SectionRow from '../../components/SectionRow';
11+
import ErrorState from '../../components/ErrorState';
1112
import { SectionRefreshHandle } from '../../types';
1213
import { useDeFiPositionsForHomepage, DeFiPositionEntry } from './hooks';
1314
import { selectPrivacyMode } from '../../../../../selectors/preferencesController';
1415
import DeFiPositionsListItem from '../../../../UI/DeFiPositions/DeFiPositionsListItem';
1516
import { selectAssetsDefiPositionsEnabled } from '../../../../../selectors/featureFlagController/assetsDefiPositions';
1617
import { strings } from '../../../../../../locales/i18n';
1718
import Routes from '../../../../../constants/navigation/Routes';
19+
import Engine from '../../../../../core/Engine';
1820

1921
const MAX_POSITIONS_DISPLAYED = 5;
2022

@@ -71,9 +73,9 @@ const DeFiSection = forwardRef<SectionRefreshHandle>((_, ref) => {
7173
navigation.navigate(Routes.WALLET.DEFI_FULL_VIEW as never);
7274
}, [navigation]);
7375

74-
// DeFi positions come from Redux selectors - no async refresh needed
7576
const refresh = useCallback(async () => {
76-
// Data refreshes automatically via DeFiPositionsController
77+
const controller = Engine.context.DeFiPositionsController;
78+
await controller._executePoll();
7779
}, []);
7880

7981
useImperativeHandle(ref, () => ({ refresh }), [refresh]);
@@ -83,11 +85,26 @@ const DeFiSection = forwardRef<SectionRefreshHandle>((_, ref) => {
8385
return null;
8486
}
8587

86-
// Don't render if error or empty (and not loading)
87-
if (!isLoading && (hasError || isEmpty)) {
88+
// Don't render if empty and not loading (200 with no data)
89+
if (!isLoading && isEmpty) {
8890
return null;
8991
}
9092

93+
// Show retry UI on error
94+
if (!isLoading && hasError) {
95+
return (
96+
<Box gap={3}>
97+
<SectionTitle title={title} onPress={handleViewAllDeFi} />
98+
<ErrorState
99+
title={strings('homepage.error.unable_to_load', {
100+
section: title.toLowerCase(),
101+
})}
102+
onRetry={refresh}
103+
/>
104+
</Box>
105+
);
106+
}
107+
91108
return (
92109
<Box gap={3}>
93110
<SectionTitle title={title} onPress={handleViewAllDeFi} />

0 commit comments

Comments
 (0)