Skip to content

Commit 30485e3

Browse files
committed
test(perps): add PerpsAlwaysOnProvider tests and fix Wallet visibility tests
- Add PerpsAlwaysOnProvider.test.tsx covering connect/disconnect lifecycle, AppState foreground/background handling, timer cancellation, and unmount cleanup - Update Wallet/index.test.tsx to reflect removal of isVisible/onVisibilityChange props from PerpsTabView (lifecycle now owned by PerpsAlwaysOnProvider) New code coverage: 81% on changed lines
1 parent 8fb8c97 commit 30485e3

2 files changed

Lines changed: 242 additions & 11 deletions

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import React from 'react';
2+
import { render, act } from '@testing-library/react-native';
3+
import { Text, AppState } from 'react-native';
4+
import { useSelector } from 'react-redux';
5+
import { PerpsAlwaysOnProvider } from './PerpsAlwaysOnProvider';
6+
import { PerpsConnectionManager } from '../services/PerpsConnectionManager';
7+
8+
jest.mock('react-redux', () => ({
9+
useSelector: jest.fn(),
10+
}));
11+
12+
jest.mock('../services/PerpsConnectionManager');
13+
14+
// Prevent PerpsStreamManager singleton from instantiating PERFORMANCE_CONFIG
15+
jest.mock('../providers/PerpsStreamManager', () => ({
16+
PerpsStreamProvider: ({ children }: { children: React.ReactNode }) =>
17+
children,
18+
}));
19+
20+
jest.mock('@metamask/perps-controller', () => ({
21+
PERPS_CONSTANTS: {
22+
FeatureName: 'perps',
23+
ReconnectionDelayAndroidMs: 500,
24+
},
25+
}));
26+
27+
jest.mock('../../../../util/Logger', () => ({
28+
error: jest.fn(),
29+
}));
30+
31+
jest.mock('../../../../util/errorUtils', () => ({
32+
ensureError: jest.fn((err) =>
33+
err instanceof Error ? err : new Error(String(err)),
34+
),
35+
}));
36+
37+
jest.mock('../index', () => ({
38+
selectPerpsEnabledFlag: jest.fn(),
39+
}));
40+
41+
const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
42+
const mockConnect = PerpsConnectionManager.connect as jest.Mock;
43+
const mockDisconnect = PerpsConnectionManager.disconnect as jest.Mock;
44+
45+
describe('PerpsAlwaysOnProvider', () => {
46+
let mockAppStateListener: ((state: string) => void) | null = null;
47+
let mockSubscriptionRemove: jest.Mock;
48+
let addEventListenerSpy: jest.SpyInstance;
49+
50+
beforeEach(() => {
51+
jest.clearAllMocks();
52+
jest.useFakeTimers();
53+
54+
mockConnect.mockResolvedValue(undefined);
55+
mockDisconnect.mockResolvedValue(undefined);
56+
57+
mockSubscriptionRemove = jest.fn();
58+
addEventListenerSpy = jest
59+
.spyOn(AppState, 'addEventListener')
60+
.mockImplementation((event, handler) => {
61+
if (event === 'change') {
62+
mockAppStateListener = handler as (state: string) => void;
63+
}
64+
return { remove: mockSubscriptionRemove };
65+
});
66+
67+
// Default: perps enabled
68+
mockUseSelector.mockReturnValue(true);
69+
});
70+
71+
afterEach(() => {
72+
act(() => {
73+
jest.runOnlyPendingTimers();
74+
});
75+
jest.useRealTimers();
76+
addEventListenerSpy.mockRestore();
77+
mockAppStateListener = null;
78+
});
79+
80+
it('renders children', () => {
81+
const { getByText } = render(
82+
<PerpsAlwaysOnProvider>
83+
<Text>child content</Text>
84+
</PerpsAlwaysOnProvider>,
85+
);
86+
expect(getByText('child content')).toBeTruthy();
87+
});
88+
89+
it('calls connect on mount when perps is enabled', () => {
90+
render(
91+
<PerpsAlwaysOnProvider>
92+
<Text>child</Text>
93+
</PerpsAlwaysOnProvider>,
94+
);
95+
expect(mockConnect).toHaveBeenCalledTimes(1);
96+
});
97+
98+
it('does not call connect on mount when perps is disabled', () => {
99+
mockUseSelector.mockReturnValue(false);
100+
101+
render(
102+
<PerpsAlwaysOnProvider>
103+
<Text>child</Text>
104+
</PerpsAlwaysOnProvider>,
105+
);
106+
107+
expect(mockConnect).not.toHaveBeenCalled();
108+
});
109+
110+
it('registers AppState listener when perps is enabled', () => {
111+
render(
112+
<PerpsAlwaysOnProvider>
113+
<Text>child</Text>
114+
</PerpsAlwaysOnProvider>,
115+
);
116+
expect(addEventListenerSpy).toHaveBeenCalledWith(
117+
'change',
118+
expect.any(Function),
119+
);
120+
});
121+
122+
it('does not register AppState listener when perps is disabled', () => {
123+
mockUseSelector.mockReturnValue(false);
124+
125+
render(
126+
<PerpsAlwaysOnProvider>
127+
<Text>child</Text>
128+
</PerpsAlwaysOnProvider>,
129+
);
130+
131+
expect(addEventListenerSpy).not.toHaveBeenCalled();
132+
});
133+
134+
it('calls disconnect when app goes to background', () => {
135+
render(
136+
<PerpsAlwaysOnProvider>
137+
<Text>child</Text>
138+
</PerpsAlwaysOnProvider>,
139+
);
140+
141+
act(() => {
142+
mockAppStateListener?.('background');
143+
});
144+
145+
expect(mockDisconnect).toHaveBeenCalledTimes(1);
146+
});
147+
148+
it('calls disconnect when app goes inactive', () => {
149+
render(
150+
<PerpsAlwaysOnProvider>
151+
<Text>child</Text>
152+
</PerpsAlwaysOnProvider>,
153+
);
154+
155+
act(() => {
156+
mockAppStateListener?.('inactive');
157+
});
158+
159+
expect(mockDisconnect).toHaveBeenCalledTimes(1);
160+
});
161+
162+
it('calls connect after delay when app returns to foreground', () => {
163+
render(
164+
<PerpsAlwaysOnProvider>
165+
<Text>child</Text>
166+
</PerpsAlwaysOnProvider>,
167+
);
168+
169+
// Clear the initial mount connect call
170+
mockConnect.mockClear();
171+
172+
act(() => {
173+
mockAppStateListener?.('background');
174+
});
175+
act(() => {
176+
mockAppStateListener?.('active');
177+
});
178+
179+
// Should not reconnect immediately — uses a timer delay
180+
expect(mockConnect).not.toHaveBeenCalled();
181+
182+
act(() => {
183+
jest.runAllTimers();
184+
});
185+
186+
expect(mockConnect).toHaveBeenCalledTimes(1);
187+
});
188+
189+
it('cancels pending reconnect timer if app goes background before timer fires', () => {
190+
render(
191+
<PerpsAlwaysOnProvider>
192+
<Text>child</Text>
193+
</PerpsAlwaysOnProvider>,
194+
);
195+
196+
mockConnect.mockClear();
197+
198+
// Goes active — schedules reconnect timer
199+
act(() => {
200+
mockAppStateListener?.('active');
201+
});
202+
203+
// Goes background before timer fires — cancels the pending reconnect
204+
act(() => {
205+
mockAppStateListener?.('background');
206+
});
207+
208+
act(() => {
209+
jest.runAllTimers();
210+
});
211+
212+
// connect should NOT have been called (timer was cancelled)
213+
expect(mockConnect).not.toHaveBeenCalled();
214+
expect(mockDisconnect).toHaveBeenCalledTimes(1);
215+
});
216+
217+
it('calls disconnect and removes AppState subscription on unmount', () => {
218+
const { unmount } = render(
219+
<PerpsAlwaysOnProvider>
220+
<Text>child</Text>
221+
</PerpsAlwaysOnProvider>,
222+
);
223+
224+
mockDisconnect.mockClear();
225+
226+
act(() => {
227+
unmount();
228+
});
229+
230+
expect(mockDisconnect).toHaveBeenCalledTimes(1);
231+
expect(mockSubscriptionRemove).toHaveBeenCalledTimes(1);
232+
});
233+
});

app/components/Views/Wallet/index.test.tsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,7 @@ describe('Wallet', () => {
10831083
mockPredictGTMModalEnabled = false; // Reset to default
10841084
});
10851085

1086-
it('should register visibility callback when Perps is enabled', () => {
1086+
it('should render PerpsTabView without visibility props when Perps is enabled', () => {
10871087
const state = {
10881088
...mockInitialState,
10891089
engine: {
@@ -1109,20 +1109,17 @@ describe('Wallet', () => {
11091109
{ state },
11101110
);
11111111

1112-
// Debug: Check if TabsList was rendered
11131112
expect(mockTabsListComponent).toHaveBeenCalled();
1114-
1115-
// Check that PerpsTabView was rendered
11161113
expect(mockPerpsTabView).toHaveBeenCalled();
11171114

1118-
// Check the props it was called with
1115+
// With PerpsAlwaysOnProvider managing lifecycle, PerpsTabView no longer
1116+
// receives visibility props — lifecycle is centralized at the wallet root.
11191117
const perpsTabViewProps = mockPerpsTabView.mock.calls[0][0];
1120-
expect(perpsTabViewProps.onVisibilityChange).toBeDefined();
1121-
expect(typeof perpsTabViewProps.onVisibilityChange).toBe('function');
1122-
expect(perpsTabViewProps.isVisible).toBe(false); // Initially not visible (tab 0 is selected)
1118+
expect(perpsTabViewProps.onVisibilityChange).toBeUndefined();
1119+
expect(perpsTabViewProps.isVisible).toBeUndefined();
11231120
});
11241121

1125-
it('should calculate correct perpsTabIndex when Perps is enabled', () => {
1122+
it('should render PerpsTabView with only tab-related props when Perps is enabled', () => {
11261123
const state = {
11271124
...mockInitialState,
11281125
engine: {
@@ -1148,9 +1145,10 @@ describe('Wallet', () => {
11481145
{ state },
11491146
);
11501147

1151-
// Perps should be at index 1 when enabled (after Tokens at index 0)
1148+
expect(mockPerpsTabView).toHaveBeenCalled();
1149+
// tabLabel and key are the only props passed — no isVisible or onVisibilityChange
11521150
const perpsTabViewProps = mockPerpsTabView.mock.calls[0][0];
1153-
expect(perpsTabViewProps.isVisible).toBe(false); // Initially not visible (tab 0 is selected)
1151+
expect(perpsTabViewProps.tabLabel).toBeDefined();
11541152
});
11551153

11561154
it('should not render PerpsTabView when Perps is disabled', () => {

0 commit comments

Comments
 (0)