Skip to content

Commit aea7b5c

Browse files
committed
Implement Shield Subscription Provider memoization
1 parent 00976f3 commit aea7b5c

File tree

2 files changed

+347
-31
lines changed

2 files changed

+347
-31
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import React, { useEffect, useRef } from 'react';
2+
import { render, waitFor } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import * as redux from 'react-redux';
5+
import * as useSubscription from '../../hooks/subscription/useSubscription';
6+
import * as useSubscriptionMetrics from '../../hooks/shield/metrics/useSubscriptionMetrics';
7+
import * as selectors from '../../selectors';
8+
import * as authSelectors from '../../selectors/identity/authentication';
9+
import * as subscriptionSelectors from '../../selectors/subscription';
10+
import * as metamaskDucks from '../../ducks/metamask/metamask';
11+
import * as environment from '../../../shared/modules/environment';
12+
import {
13+
ShieldSubscriptionProvider,
14+
useShieldSubscriptionContext,
15+
} from './shield-subscription';
16+
17+
jest.mock('../../hooks/subscription/useSubscription');
18+
jest.mock('../../hooks/shield/metrics/useSubscriptionMetrics');
19+
jest.mock('../../store/actions', () => ({
20+
assignUserToCohort: jest.fn(),
21+
setShowShieldEntryModalOnce: jest.fn(),
22+
subscriptionsStartPolling: jest.fn(),
23+
}));
24+
25+
describe('ShieldSubscriptionProvider', () => {
26+
const mockDispatch = jest.fn();
27+
const mockGetSubscriptionEligibility = jest.fn();
28+
const mockCaptureShieldEligibilityCohortEvent = jest.fn();
29+
30+
beforeEach(() => {
31+
jest.clearAllMocks();
32+
33+
// Mock Redux hooks
34+
jest.spyOn(redux, 'useDispatch').mockReturnValue(mockDispatch);
35+
jest.spyOn(redux, 'useSelector').mockImplementation((selector) => {
36+
if (selector === selectors.getUseExternalServices) {
37+
return true;
38+
}
39+
if (selector === metamaskDucks.getIsUnlocked) {
40+
return true;
41+
}
42+
if (selector === authSelectors.selectIsSignedIn) {
43+
return true;
44+
}
45+
if (selector === subscriptionSelectors.getIsActiveShieldSubscription) {
46+
return false;
47+
}
48+
if (selector === subscriptionSelectors.getHasShieldEntryModalShownOnce) {
49+
return false;
50+
}
51+
return false;
52+
});
53+
54+
// Mock environment
55+
jest
56+
.spyOn(environment, 'getIsMetaMaskShieldFeatureEnabled')
57+
.mockReturnValue(true);
58+
59+
// Mock hooks
60+
jest.spyOn(useSubscription, 'useSubscriptionEligibility').mockReturnValue({
61+
getSubscriptionEligibility: mockGetSubscriptionEligibility,
62+
isLoading: false,
63+
error: null,
64+
data: null,
65+
});
66+
67+
jest
68+
.spyOn(useSubscriptionMetrics, 'useSubscriptionMetrics')
69+
.mockReturnValue({
70+
captureShieldEligibilityCohortEvent:
71+
mockCaptureShieldEligibilityCohortEvent,
72+
});
73+
});
74+
75+
it('renders children correctly', () => {
76+
const { getByTestId } = render(
77+
<ShieldSubscriptionProvider>
78+
<div data-testid="child">Child Component</div>
79+
</ShieldSubscriptionProvider>,
80+
);
81+
82+
expect(getByTestId('child')).toBeInTheDocument();
83+
});
84+
85+
it('provides context with evaluateCohortEligibility function', () => {
86+
const TestConsumer = () => {
87+
const { evaluateCohortEligibility } = useShieldSubscriptionContext();
88+
return (
89+
<div data-testid="consumer">
90+
{typeof evaluateCohortEligibility === 'function' ? 'function' : 'not'}
91+
</div>
92+
);
93+
};
94+
95+
const { getByTestId } = render(
96+
<ShieldSubscriptionProvider>
97+
<TestConsumer />
98+
</ShieldSubscriptionProvider>,
99+
);
100+
101+
expect(getByTestId('consumer')).toHaveTextContent('function');
102+
});
103+
104+
describe('Memoization', () => {
105+
it('provides stable evaluateCohortEligibility callback across re-renders', () => {
106+
const callbacks: ((cohort: string) => Promise<void>)[] = [];
107+
108+
const TestConsumer = () => {
109+
const { evaluateCohortEligibility } = useShieldSubscriptionContext();
110+
const renderCount = useRef(0);
111+
112+
useEffect(() => {
113+
callbacks.push(evaluateCohortEligibility);
114+
renderCount.current += 1;
115+
});
116+
117+
return <div data-testid="consumer">Consumer</div>;
118+
};
119+
120+
const { rerender } = render(
121+
<ShieldSubscriptionProvider>
122+
<TestConsumer />
123+
</ShieldSubscriptionProvider>,
124+
);
125+
126+
// Force re-render
127+
rerender(
128+
<ShieldSubscriptionProvider>
129+
<TestConsumer />
130+
</ShieldSubscriptionProvider>,
131+
);
132+
133+
// The callback should be the same reference across renders
134+
expect(callbacks.length).toBeGreaterThanOrEqual(2);
135+
expect(callbacks[0]).toBe(callbacks[1]);
136+
});
137+
138+
it('provides stable context value object across re-renders', () => {
139+
const contexts: {
140+
evaluateCohortEligibility: (cohort: string) => Promise<void>;
141+
}[] = [];
142+
143+
const TestConsumer = () => {
144+
const context = useShieldSubscriptionContext();
145+
146+
useEffect(() => {
147+
contexts.push(context);
148+
});
149+
150+
return <div data-testid="consumer">Consumer</div>;
151+
};
152+
153+
const { rerender } = render(
154+
<ShieldSubscriptionProvider>
155+
<TestConsumer />
156+
</ShieldSubscriptionProvider>,
157+
);
158+
159+
// Force re-render
160+
rerender(
161+
<ShieldSubscriptionProvider>
162+
<TestConsumer />
163+
</ShieldSubscriptionProvider>,
164+
);
165+
166+
// The context object should be the same reference across renders
167+
expect(contexts.length).toBeGreaterThanOrEqual(2);
168+
expect(contexts[0]).toBe(contexts[1]);
169+
});
170+
});
171+
172+
describe('evaluateCohortEligibility', () => {
173+
it('can be called successfully', async () => {
174+
mockGetSubscriptionEligibility.mockResolvedValue({
175+
canSubscribe: true,
176+
canViewEntryModal: true,
177+
cohorts: [],
178+
assignedCohort: null,
179+
hasAssignedCohortExpired: false,
180+
modalType: 'entry',
181+
});
182+
183+
let evaluateFn: ((cohort: string) => Promise<void>) | null = null;
184+
185+
const TestConsumer = () => {
186+
const { evaluateCohortEligibility } = useShieldSubscriptionContext();
187+
evaluateFn = evaluateCohortEligibility;
188+
return <div data-testid="consumer">Consumer</div>;
189+
};
190+
191+
render(
192+
<ShieldSubscriptionProvider>
193+
<TestConsumer />
194+
</ShieldSubscriptionProvider>,
195+
);
196+
197+
// Call the function after render
198+
await evaluateFn?.('wallet_home');
199+
200+
await waitFor(() => {
201+
expect(mockGetSubscriptionEligibility).toHaveBeenCalled();
202+
});
203+
});
204+
205+
it('accesses current values even with stable callback', async () => {
206+
// Initially return false for basic functionality
207+
let isBasicFunctionalityEnabled = false;
208+
209+
jest.spyOn(redux, 'useSelector').mockImplementation((selector) => {
210+
if (selector === selectors.getUseExternalServices) {
211+
return isBasicFunctionalityEnabled;
212+
}
213+
if (selector === metamaskDucks.getIsUnlocked) {
214+
return true;
215+
}
216+
if (selector === authSelectors.selectIsSignedIn) {
217+
return true;
218+
}
219+
if (selector === subscriptionSelectors.getIsActiveShieldSubscription) {
220+
return false;
221+
}
222+
if (
223+
selector === subscriptionSelectors.getHasShieldEntryModalShownOnce
224+
) {
225+
return false;
226+
}
227+
return false;
228+
});
229+
230+
mockGetSubscriptionEligibility.mockResolvedValue({
231+
canSubscribe: true,
232+
canViewEntryModal: true,
233+
cohorts: [],
234+
assignedCohort: null,
235+
hasAssignedCohortExpired: false,
236+
modalType: 'entry',
237+
});
238+
239+
let evaluateFn: ((cohort: string) => Promise<void>) | null = null;
240+
241+
const TestConsumer = () => {
242+
const { evaluateCohortEligibility } = useShieldSubscriptionContext();
243+
evaluateFn = evaluateCohortEligibility;
244+
return <div data-testid="consumer">Consumer</div>;
245+
};
246+
247+
const { rerender } = render(
248+
<ShieldSubscriptionProvider>
249+
<TestConsumer />
250+
</ShieldSubscriptionProvider>,
251+
);
252+
253+
// Call with basic functionality disabled
254+
await evaluateFn?.('wallet_home');
255+
256+
// Should not call getSubscriptionEligibility when basic functionality is disabled
257+
await waitFor(() => {
258+
expect(mockGetSubscriptionEligibility).not.toHaveBeenCalled();
259+
});
260+
261+
// Enable basic functionality
262+
isBasicFunctionalityEnabled = true;
263+
264+
// Force re-render with updated state
265+
rerender(
266+
<ShieldSubscriptionProvider>
267+
<TestConsumer />
268+
</ShieldSubscriptionProvider>,
269+
);
270+
271+
// Call again with basic functionality enabled
272+
await evaluateFn?.('wallet_home');
273+
274+
// Now it should call getSubscriptionEligibility
275+
await waitFor(
276+
() => {
277+
expect(mockGetSubscriptionEligibility).toHaveBeenCalled();
278+
},
279+
{ timeout: 3000 },
280+
);
281+
});
282+
});
283+
});

0 commit comments

Comments
 (0)