Skip to content

Commit 9e54f7f

Browse files
committed
Add test coverage
1 parent 9e8acf1 commit 9e54f7f

File tree

2 files changed

+269
-5
lines changed

2 files changed

+269
-5
lines changed
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import { useSelector } from 'react-redux';
5+
import { RewardsPointsBalance } from './RewardsPointsBalance';
6+
import { useI18nContext } from '../../../hooks/useI18nContext';
7+
import { useRewardsContext } from '../../../contexts/rewards';
8+
import type { RewardsContextValue } from '../../../contexts/rewards';
9+
import type { SeasonStatusState } from '../../../../app/scripts/controllers/rewards/rewards-controller.types';
10+
11+
// Mock dependencies
12+
jest.mock('react-redux', () => ({
13+
useSelector: jest.fn(),
14+
}));
15+
16+
jest.mock('../../../hooks/useI18nContext', () => ({
17+
useI18nContext: jest.fn(),
18+
}));
19+
20+
jest.mock('../../../contexts/rewards', () => ({
21+
useRewardsContext: jest.fn(),
22+
}));
23+
24+
jest.mock('../../component-library/skeleton', () => ({
25+
Skeleton: ({ width }: { width: string }) => (
26+
<div data-testid="skeleton" style={{ width }}>
27+
Loading...
28+
</div>
29+
),
30+
}));
31+
32+
const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
33+
const mockUseI18nContext = useI18nContext as jest.MockedFunction<
34+
typeof useI18nContext
35+
>;
36+
const mockUseRewardsContext = useRewardsContext as jest.MockedFunction<
37+
typeof useRewardsContext
38+
>;
39+
40+
describe('RewardsPointsBalance', () => {
41+
const mockT = jest.fn((key: string, values?: string[]) => {
42+
if (key === 'rewardsOptIn') return 'Opt In';
43+
if (key === 'rewardsPointsBalance' && values) return `${values[0]} points`;
44+
if (key === 'rewardsPointsIcon') return 'Rewards Points Icon';
45+
return key;
46+
});
47+
48+
// Mock season status with complete structure
49+
const mockSeasonStatus: SeasonStatusState = {
50+
season: {
51+
id: 'test-season',
52+
name: 'Test Season',
53+
startDate: Date.now() - 86400000, // 1 day ago
54+
endDate: Date.now() + 86400000, // 1 day from now
55+
tiers: [],
56+
},
57+
balance: {
58+
total: 1000,
59+
},
60+
tier: {
61+
currentTier: {
62+
id: 'tier-1',
63+
name: 'Bronze',
64+
pointsNeeded: 0,
65+
image: {
66+
lightModeUrl: 'light.png',
67+
darkModeUrl: 'dark.png',
68+
},
69+
levelNumber: '1',
70+
rewards: [],
71+
},
72+
nextTier: null,
73+
nextTierPointsNeeded: null,
74+
},
75+
};
76+
77+
// Mock rewards context value with complete structure
78+
const mockRewardsContextValue: RewardsContextValue = {
79+
rewardsEnabled: true,
80+
candidateSubscriptionId: 'test-subscription-id',
81+
candidateSubscriptionIdError: false,
82+
seasonStatus: mockSeasonStatus,
83+
seasonStatusError: null,
84+
seasonStatusLoading: false,
85+
refetchSeasonStatus: jest.fn(),
86+
};
87+
88+
beforeEach(() => {
89+
jest.clearAllMocks();
90+
mockUseI18nContext.mockReturnValue(mockT);
91+
mockUseSelector.mockReturnValue('en-US'); // Default locale
92+
});
93+
94+
it('should render null when rewards are not enabled', () => {
95+
mockUseRewardsContext.mockReturnValue({
96+
...mockRewardsContextValue,
97+
rewardsEnabled: false,
98+
seasonStatus: null,
99+
candidateSubscriptionId: null,
100+
});
101+
102+
const { container } = render(<RewardsPointsBalance />);
103+
expect(container.firstChild).toBeNull();
104+
});
105+
106+
it('should render opt-in badge when candidateSubscriptionId is null', () => {
107+
mockUseRewardsContext.mockReturnValue({
108+
...mockRewardsContextValue,
109+
seasonStatus: null,
110+
candidateSubscriptionId: null,
111+
});
112+
113+
render(<RewardsPointsBalance />);
114+
115+
expect(screen.getByTestId('rewards-points-balance')).toBeInTheDocument();
116+
expect(
117+
screen.getByTestId('rewards-points-balance-value'),
118+
).toHaveTextContent('Opt In');
119+
expect(screen.getByAltText('Rewards Points Icon')).toBeInTheDocument();
120+
});
121+
122+
it('should render skeleton when loading and no balance exists', () => {
123+
mockUseRewardsContext.mockReturnValue({
124+
...mockRewardsContextValue,
125+
seasonStatus: null,
126+
seasonStatusLoading: true,
127+
});
128+
129+
render(<RewardsPointsBalance />);
130+
131+
expect(screen.getByTestId('skeleton')).toBeInTheDocument();
132+
expect(screen.getByText('Loading...')).toBeInTheDocument();
133+
});
134+
135+
it('should not render skeleton when loading but balance exists', () => {
136+
mockUseRewardsContext.mockReturnValue({
137+
...mockRewardsContextValue,
138+
seasonStatusLoading: true,
139+
});
140+
141+
render(<RewardsPointsBalance />);
142+
143+
expect(screen.queryByTestId('skeleton')).not.toBeInTheDocument();
144+
expect(screen.getByTestId('rewards-points-balance')).toBeInTheDocument();
145+
expect(
146+
screen.getByTestId('rewards-points-balance-value'),
147+
).toHaveTextContent('1,000 points');
148+
});
149+
150+
it('should render formatted points balance with default locale', () => {
151+
mockUseRewardsContext.mockReturnValue({
152+
...mockRewardsContextValue,
153+
seasonStatus: {
154+
...mockSeasonStatus,
155+
balance: {
156+
total: 12345,
157+
},
158+
},
159+
});
160+
161+
render(<RewardsPointsBalance />);
162+
163+
expect(screen.getByTestId('rewards-points-balance')).toBeInTheDocument();
164+
expect(
165+
screen.getByTestId('rewards-points-balance-value'),
166+
).toHaveTextContent('12,345 points');
167+
expect(screen.getByAltText('Rewards Points Icon')).toBeInTheDocument();
168+
});
169+
170+
it('should render formatted points balance with German locale', () => {
171+
mockUseSelector.mockReturnValue('de-DE');
172+
mockUseRewardsContext.mockReturnValue({
173+
...mockRewardsContextValue,
174+
seasonStatus: {
175+
...mockSeasonStatus,
176+
balance: {
177+
total: 12345,
178+
},
179+
},
180+
});
181+
182+
render(<RewardsPointsBalance />);
183+
184+
expect(screen.getByTestId('rewards-points-balance')).toBeInTheDocument();
185+
expect(
186+
screen.getByTestId('rewards-points-balance-value'),
187+
).toHaveTextContent('12.345 points');
188+
});
189+
190+
it('should render zero points correctly', () => {
191+
mockUseRewardsContext.mockReturnValue({
192+
...mockRewardsContextValue,
193+
seasonStatus: {
194+
...mockSeasonStatus,
195+
balance: {
196+
total: 0,
197+
},
198+
},
199+
});
200+
201+
render(<RewardsPointsBalance />);
202+
203+
expect(screen.getByTestId('rewards-points-balance')).toBeInTheDocument();
204+
expect(
205+
screen.getByTestId('rewards-points-balance-value'),
206+
).toHaveTextContent('0 points');
207+
});
208+
209+
it('should handle undefined seasonStatus gracefully', () => {
210+
mockUseRewardsContext.mockReturnValue({
211+
...mockRewardsContextValue,
212+
seasonStatus: null,
213+
});
214+
215+
render(<RewardsPointsBalance />);
216+
217+
expect(screen.getByTestId('rewards-points-balance')).toBeInTheDocument();
218+
expect(
219+
screen.getByTestId('rewards-points-balance-value'),
220+
).toHaveTextContent('0 points');
221+
});
222+
223+
it('should render with correct CSS classes and structure', () => {
224+
mockUseRewardsContext.mockReturnValue(mockRewardsContextValue);
225+
226+
render(<RewardsPointsBalance />);
227+
228+
const container = screen.getByTestId('rewards-points-balance');
229+
expect(container).toHaveClass(
230+
'flex',
231+
'items-center',
232+
'gap-2',
233+
'px-2',
234+
'bg-background-muted',
235+
'rounded',
236+
);
237+
238+
const image = screen.getByAltText('Rewards Points Icon');
239+
expect(image).toHaveAttribute(
240+
'src',
241+
'./images/metamask-rewards-points.svg',
242+
);
243+
expect(image).toHaveStyle({ width: '16px', height: '16px' });
244+
});
245+
246+
it('should call useSelector with getIntlLocale selector', () => {
247+
mockUseRewardsContext.mockReturnValue(mockRewardsContextValue);
248+
249+
render(<RewardsPointsBalance />);
250+
251+
expect(mockUseSelector).toHaveBeenCalled();
252+
});
253+
254+
it('should call useI18nContext hook', () => {
255+
mockUseRewardsContext.mockReturnValue(mockRewardsContextValue);
256+
257+
render(<RewardsPointsBalance />);
258+
259+
expect(mockUseI18nContext).toHaveBeenCalled();
260+
});
261+
262+
it('should call useRewardsContext hook', () => {
263+
mockUseRewardsContext.mockReturnValue(mockRewardsContextValue);
264+
265+
render(<RewardsPointsBalance />);
266+
267+
expect(mockUseRewardsContext).toHaveBeenCalled();
268+
});
269+
});

ui/components/app/rewards/RewardsPointsBalance.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ export const RewardsPointsBalance = () => {
3434
return <Skeleton width="100px" />;
3535
}
3636

37-
// Don't render if there's no points balance to show
38-
if (seasonStatus?.balance?.total === null) {
39-
return null;
40-
}
41-
4237
// Format the points balance with proper locale-aware number formatting
4338
const formattedPoints = new Intl.NumberFormat(locale).format(
4439
seasonStatus?.balance?.total ?? 0,

0 commit comments

Comments
 (0)