Skip to content

Commit 15c0f52

Browse files
committed
feat: added test coverage
1 parent ffdec8d commit 15c0f52

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed

app/components/UI/Earn/components/EarnTransactionMonitor.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@ import EarnTransactionMonitor from './EarnTransactionMonitor';
44
import { useMusdConversionStatus } from '../hooks/useMusdConversionStatus';
55
import { useMusdConversionStaleApprovalCleanup } from '../hooks/useMusdConversionStaleApprovalCleanup';
66
import { useMerklClaimStatus } from '../hooks/useMerklClaimStatus';
7+
import { useEnsureMusdTokenRegistered } from '../hooks/useEnsureMusdTokenRegistered';
78

89
jest.mock('../hooks/useMusdConversionStatus');
910
jest.mock('../hooks/useMusdConversionStaleApprovalCleanup');
1011
jest.mock('../hooks/useMerklClaimStatus');
12+
jest.mock('../hooks/useEnsureMusdTokenRegistered');
1113

1214
describe('EarnTransactionMonitor', () => {
1315
const mockUseMusdConversionStatus = jest.mocked(useMusdConversionStatus);
1416
const mockUseMusdConversionStaleApprovalCleanup = jest.mocked(
1517
useMusdConversionStaleApprovalCleanup,
1618
);
1719
const mockUseMerklClaimStatus = jest.mocked(useMerklClaimStatus);
20+
const mockUseEnsureMusdTokenRegistered = jest.mocked(
21+
useEnsureMusdTokenRegistered,
22+
);
1823

1924
beforeEach(() => {
2025
jest.clearAllMocks();
@@ -48,6 +53,12 @@ describe('EarnTransactionMonitor', () => {
4853
expect(mockUseMerklClaimStatus).toHaveBeenCalledTimes(1);
4954
});
5055

56+
it('calls useEnsureMusdTokenRegistered hook', () => {
57+
render(<EarnTransactionMonitor />);
58+
59+
expect(mockUseEnsureMusdTokenRegistered).toHaveBeenCalledTimes(1);
60+
});
61+
5162
it('returns null', () => {
5263
const { toJSON } = render(<EarnTransactionMonitor />);
5364

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { renderHook, waitFor } from '@testing-library/react-native';
2+
import { useSelector } from 'react-redux';
3+
import Engine from '../../../../core/Engine';
4+
import Logger from '../../../../util/Logger';
5+
import { retryWithExponentialDelay } from '../../../../util/exponential-retry';
6+
import { ensureMusdTokenRegistered } from '../utils/musdConversionTransaction';
7+
import { useEnsureMusdTokenRegistered } from './useEnsureMusdTokenRegistered';
8+
9+
jest.mock('react-redux', () => ({
10+
useSelector: jest.fn(),
11+
}));
12+
13+
jest.mock('../../../../core/Engine', () => ({
14+
__esModule: true,
15+
default: {
16+
context: {
17+
NetworkController: {
18+
findNetworkClientIdByChainId: jest.fn(),
19+
},
20+
},
21+
},
22+
}));
23+
24+
jest.mock('../../../../util/Logger', () => ({
25+
__esModule: true,
26+
default: {
27+
error: jest.fn(),
28+
},
29+
}));
30+
31+
jest.mock('../../../../util/exponential-retry', () => ({
32+
retryWithExponentialDelay: jest.fn((fn: () => Promise<unknown>) => fn()),
33+
}));
34+
35+
jest.mock('../utils/musdConversionTransaction', () => ({
36+
ensureMusdTokenRegistered: jest.fn(),
37+
}));
38+
39+
jest.mock('../selectors/featureFlags', () => ({
40+
selectMusdTokenRegistrationChainIds: jest.fn(),
41+
}));
42+
43+
describe('useEnsureMusdTokenRegistered', () => {
44+
const mockUseSelector = jest.mocked(useSelector);
45+
const mockFindNetworkClientIdByChainId = jest.mocked(
46+
Engine.context.NetworkController.findNetworkClientIdByChainId,
47+
);
48+
const mockEnsureMusdTokenRegistered = jest.mocked(ensureMusdTokenRegistered);
49+
const mockRetryWithExponentialDelay = jest.mocked(retryWithExponentialDelay);
50+
const mockLoggerError = jest.mocked(Logger.error);
51+
52+
beforeEach(() => {
53+
jest.useFakeTimers();
54+
jest.clearAllMocks();
55+
mockRetryWithExponentialDelay.mockImplementation(
56+
(fn: () => Promise<unknown>) => fn(),
57+
);
58+
});
59+
60+
afterEach(() => {
61+
jest.useRealTimers();
62+
jest.restoreAllMocks();
63+
});
64+
65+
describe('happy path — registration', () => {
66+
it('calls ensureMusdTokenRegistered for each chain ID returned by the selector', async () => {
67+
mockUseSelector.mockReturnValue(['0x1', '0xe708']);
68+
mockFindNetworkClientIdByChainId
69+
.mockReturnValueOnce('mainnet')
70+
.mockReturnValueOnce('linea-mainnet');
71+
72+
renderHook(() => useEnsureMusdTokenRegistered());
73+
74+
await waitFor(() => {
75+
expect(mockEnsureMusdTokenRegistered).toHaveBeenCalledTimes(2);
76+
});
77+
78+
expect(mockEnsureMusdTokenRegistered).toHaveBeenCalledWith({
79+
chainId: '0x1',
80+
networkClientId: 'mainnet',
81+
});
82+
expect(mockEnsureMusdTokenRegistered).toHaveBeenCalledWith({
83+
chainId: '0xe708',
84+
networkClientId: 'linea-mainnet',
85+
});
86+
});
87+
88+
it('wraps each registration call with retryWithExponentialDelay using maxRetries 2', async () => {
89+
mockUseSelector.mockReturnValue(['0x1']);
90+
mockFindNetworkClientIdByChainId.mockReturnValue('mainnet');
91+
92+
renderHook(() => useEnsureMusdTokenRegistered());
93+
94+
await waitFor(() => {
95+
expect(mockRetryWithExponentialDelay).toHaveBeenCalledTimes(1);
96+
});
97+
98+
expect(mockRetryWithExponentialDelay).toHaveBeenCalledWith(
99+
expect.any(Function),
100+
2,
101+
);
102+
});
103+
});
104+
105+
describe('skipping chains without a network client', () => {
106+
it('skips registration for a chain when findNetworkClientIdByChainId returns null', async () => {
107+
mockUseSelector.mockReturnValue(['0x1', '0xe708']);
108+
mockFindNetworkClientIdByChainId
109+
.mockReturnValueOnce('mainnet')
110+
.mockReturnValueOnce(null as unknown as string);
111+
112+
renderHook(() => useEnsureMusdTokenRegistered());
113+
114+
await waitFor(() => {
115+
expect(mockFindNetworkClientIdByChainId).toHaveBeenCalledTimes(2);
116+
});
117+
118+
expect(mockEnsureMusdTokenRegistered).toHaveBeenCalledTimes(1);
119+
expect(mockEnsureMusdTokenRegistered).toHaveBeenCalledWith({
120+
chainId: '0x1',
121+
networkClientId: 'mainnet',
122+
});
123+
expect(mockEnsureMusdTokenRegistered).not.toHaveBeenCalledWith(
124+
expect.objectContaining({ chainId: '0xe708' }),
125+
);
126+
});
127+
128+
it('does not call ensureMusdTokenRegistered when all chains return null networkClientId', async () => {
129+
mockUseSelector.mockReturnValue(['0x1', '0xe708']);
130+
mockFindNetworkClientIdByChainId.mockReturnValue(
131+
null as unknown as string,
132+
);
133+
134+
renderHook(() => useEnsureMusdTokenRegistered());
135+
136+
await waitFor(() => {
137+
expect(mockFindNetworkClientIdByChainId).toHaveBeenCalledTimes(2);
138+
});
139+
140+
expect(mockEnsureMusdTokenRegistered).not.toHaveBeenCalled();
141+
});
142+
});
143+
144+
describe('error handling', () => {
145+
it('logs error via Logger.error when ensureMusdTokenRegistered fails after all retries', async () => {
146+
const registrationError = new Error('network error');
147+
mockUseSelector.mockReturnValue(['0x1']);
148+
mockFindNetworkClientIdByChainId.mockReturnValue('mainnet');
149+
mockRetryWithExponentialDelay.mockRejectedValue(registrationError);
150+
151+
renderHook(() => useEnsureMusdTokenRegistered());
152+
153+
await waitFor(() => {
154+
expect(mockLoggerError).toHaveBeenCalledTimes(1);
155+
});
156+
157+
expect(mockLoggerError).toHaveBeenCalledWith(
158+
registrationError,
159+
'[mUSD] Failed to register mUSD token for chain 0x1',
160+
);
161+
});
162+
163+
it('continues registering remaining chains after one chain fails', async () => {
164+
const registrationError = new Error('network error');
165+
mockUseSelector.mockReturnValue(['0x1', '0xe708']);
166+
mockFindNetworkClientIdByChainId
167+
.mockReturnValueOnce('mainnet')
168+
.mockReturnValueOnce('linea-mainnet');
169+
mockRetryWithExponentialDelay
170+
.mockRejectedValueOnce(registrationError)
171+
.mockImplementationOnce((fn: () => Promise<unknown>) => fn());
172+
173+
renderHook(() => useEnsureMusdTokenRegistered());
174+
175+
await waitFor(() => {
176+
expect(mockLoggerError).toHaveBeenCalledTimes(1);
177+
});
178+
179+
expect(mockLoggerError).toHaveBeenCalledWith(
180+
registrationError,
181+
'[mUSD] Failed to register mUSD token for chain 0x1',
182+
);
183+
expect(mockEnsureMusdTokenRegistered).toHaveBeenCalledTimes(1);
184+
expect(mockEnsureMusdTokenRegistered).toHaveBeenCalledWith({
185+
chainId: '0xe708',
186+
networkClientId: 'linea-mainnet',
187+
});
188+
});
189+
});
190+
191+
describe('effect re-run on chain IDs change', () => {
192+
it('re-runs registration when chainIdsToRegister changes', async () => {
193+
mockUseSelector.mockReturnValue(['0x1']);
194+
mockFindNetworkClientIdByChainId.mockReturnValue('mainnet');
195+
196+
const { rerender } = renderHook(() => useEnsureMusdTokenRegistered());
197+
198+
await waitFor(() => {
199+
expect(mockEnsureMusdTokenRegistered).toHaveBeenCalledTimes(1);
200+
});
201+
202+
mockUseSelector.mockReturnValue(['0x1', '0xe708']);
203+
mockFindNetworkClientIdByChainId
204+
.mockReturnValueOnce('mainnet')
205+
.mockReturnValueOnce('linea-mainnet');
206+
207+
rerender({});
208+
209+
await waitFor(() => {
210+
expect(mockEnsureMusdTokenRegistered).toHaveBeenCalledTimes(3);
211+
});
212+
});
213+
});
214+
215+
describe('empty chain list', () => {
216+
it('does not call ensureMusdTokenRegistered when selector returns an empty array', async () => {
217+
mockUseSelector.mockReturnValue([]);
218+
219+
renderHook(() => useEnsureMusdTokenRegistered());
220+
221+
// Allow any async effects to flush
222+
await waitFor(() => {
223+
expect(mockFindNetworkClientIdByChainId).not.toHaveBeenCalled();
224+
});
225+
226+
expect(mockEnsureMusdTokenRegistered).not.toHaveBeenCalled();
227+
expect(mockLoggerError).not.toHaveBeenCalled();
228+
});
229+
});
230+
});

0 commit comments

Comments
 (0)