Skip to content

Commit e9edf4d

Browse files
authored
Add unit tests for EarningsPage component
1 parent 7673714 commit e9edf4d

1 file changed

Lines changed: 374 additions & 0 deletions

File tree

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2+
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
3+
import EarningsPage from '@/app/earnings/page';
4+
5+
// Mock Next.js navigation
6+
jest.mock('next/navigation', () => ({
7+
useRouter: jest.fn(),
8+
useSearchParams: jest.fn(),
9+
usePathname: jest.fn(),
10+
}));
11+
12+
// Mock AuthProvider
13+
jest.mock('@/components/AuthProvider', () => ({
14+
useAuth: () => ({ user: { id: 'test-user' } }),
15+
}));
16+
17+
// Mock API
18+
jest.mock('@/app/lib/mockApi', () => ({
19+
MockApi: {
20+
getEarningsReport: jest.fn().mockResolvedValue({
21+
summary: {
22+
total: '1234.56',
23+
completed: '1000.00',
24+
pending: '234.56',
25+
},
26+
transactions: [
27+
{
28+
id: 'TX001',
29+
date: '2024-01-15',
30+
description: 'YouTube Revenue',
31+
amount: 150.0,
32+
platform: 'YouTube',
33+
status: 'completed',
34+
type: 'revenue',
35+
taxId: 'TAX-001',
36+
},
37+
{
38+
id: 'TX002',
39+
date: '2024-02-20',
40+
description: 'TikTok Revenue',
41+
amount: 200.0,
42+
platform: 'TikTok',
43+
status: 'completed',
44+
type: 'revenue',
45+
taxId: 'TAX-002',
46+
},
47+
{
48+
id: 'TX003',
49+
date: '2024-03-10',
50+
description: 'Instagram Revenue',
51+
amount: 100.0,
52+
platform: 'Instagram',
53+
status: 'pending',
54+
type: 'revenue',
55+
taxId: 'TAX-003',
56+
},
57+
],
58+
}),
59+
},
60+
}));
61+
62+
describe('Earnings Page - Filter Persistence', () => {
63+
let mockPush: jest.Mock;
64+
let mockSearchParams: URLSearchParams;
65+
66+
beforeEach(() => {
67+
mockPush = jest.fn();
68+
mockSearchParams = new URLSearchParams();
69+
70+
(useRouter as jest.Mock).mockReturnValue({ push: mockPush });
71+
(useSearchParams as jest.Mock).mockReturnValue(mockSearchParams);
72+
(usePathname as jest.Mock).mockReturnValue('/earnings');
73+
});
74+
75+
afterEach(() => {
76+
jest.clearAllMocks();
77+
});
78+
79+
describe('Filter state initialization from URL', () => {
80+
it('should load default filters when no URL params exist', async () => {
81+
render(<EarningsPage />);
82+
83+
await waitFor(() => {
84+
expect(screen.getByText(/YouTube Revenue/i)).toBeInTheDocument();
85+
expect(screen.getByText(/TikTok Revenue/i)).toBeInTheDocument();
86+
expect(screen.getByText(/Instagram Revenue/i)).toBeInTheDocument();
87+
});
88+
});
89+
90+
it('should initialize search filter from URL', async () => {
91+
mockSearchParams.set('search', 'YouTube');
92+
93+
render(<EarningsPage />);
94+
95+
await waitFor(() => {
96+
const searchInput = screen.getByPlaceholderText(/Search by ID/i) as HTMLInputElement;
97+
expect(searchInput.value).toBe('YouTube');
98+
});
99+
});
100+
101+
it('should initialize date range from URL', async () => {
102+
mockSearchParams.set('startDate', '2024-02-01');
103+
mockSearchParams.set('endDate', '2024-02-28');
104+
105+
render(<EarningsPage />);
106+
107+
await waitFor(() => {
108+
const startDateInput = screen.getByLabelText(/From/i) as HTMLInputElement;
109+
const endDateInput = screen.getByLabelText(/To/i) as HTMLInputElement;
110+
111+
expect(startDateInput.value).toBe('2024-02-01');
112+
expect(endDateInput.value).toBe('2024-02-28');
113+
});
114+
});
115+
});
116+
117+
describe('Search filter updates persist to URL', () => {
118+
it('should update URL when search term changes', async () => {
119+
render(<EarningsPage />);
120+
121+
await waitFor(() => {
122+
expect(screen.getByPlaceholderText(/Search by ID/i)).toBeInTheDocument();
123+
});
124+
125+
const searchInput = screen.getByPlaceholderText(/Search by ID/i);
126+
fireEvent.change(searchInput, { target: { value: 'TikTok' } });
127+
128+
await waitFor(() => {
129+
expect(mockPush).toHaveBeenCalledWith(
130+
expect.stringContaining('search=TikTok'),
131+
{ scroll: false }
132+
);
133+
});
134+
});
135+
136+
it('should clear search param when search is cleared', async () => {
137+
mockSearchParams.set('search', 'YouTube');
138+
139+
render(<EarningsPage />);
140+
141+
await waitFor(() => {
142+
expect(screen.getByPlaceholderText(/Search by ID/i)).toBeInTheDocument();
143+
});
144+
145+
const searchInput = screen.getByPlaceholderText(/Search by ID/i);
146+
fireEvent.change(searchInput, { target: { value: '' } });
147+
148+
await waitFor(() => {
149+
expect(mockPush).toHaveBeenCalledWith('/earnings', { scroll: false });
150+
});
151+
});
152+
153+
it('should show clear button when search has value', async () => {
154+
mockSearchParams.set('search', 'YouTube');
155+
156+
render(<EarningsPage />);
157+
158+
await waitFor(() => {
159+
const clearButton = screen.getByRole('button', { name: '' });
160+
expect(clearButton).toBeInTheDocument();
161+
});
162+
});
163+
});
164+
165+
describe('Date range filter updates persist to URL', () => {
166+
it('should update URL when start date changes', async () => {
167+
render(<EarningsPage />);
168+
169+
await waitFor(() => {
170+
expect(screen.getByLabelText(/From/i)).toBeInTheDocument();
171+
});
172+
173+
const startDateInput = screen.getByLabelText(/From/i);
174+
fireEvent.change(startDateInput, { target: { value: '2024-01-01' } });
175+
176+
await waitFor(() => {
177+
expect(mockPush).toHaveBeenCalledWith(
178+
expect.stringContaining('startDate=2024-01-01'),
179+
{ scroll: false }
180+
);
181+
});
182+
});
183+
184+
it('should update URL when end date changes', async () => {
185+
render(<EarningsPage />);
186+
187+
await waitFor(() => {
188+
expect(screen.getByLabelText(/To/i)).toBeInTheDocument();
189+
});
190+
191+
const endDateInput = screen.getByLabelText(/To/i);
192+
fireEvent.change(endDateInput, { target: { value: '2024-12-31' } });
193+
194+
await waitFor(() => {
195+
expect(mockPush).toHaveBeenCalledWith(
196+
expect.stringContaining('endDate=2024-12-31'),
197+
{ scroll: false }
198+
);
199+
});
200+
});
201+
202+
it('should update URL with both date filters', async () => {
203+
render(<EarningsPage />);
204+
205+
await waitFor(() => {
206+
expect(screen.getByLabelText(/From/i)).toBeInTheDocument();
207+
});
208+
209+
const startDateInput = screen.getByLabelText(/From/i);
210+
const endDateInput = screen.getByLabelText(/To/i);
211+
212+
fireEvent.change(startDateInput, { target: { value: '2024-01-01' } });
213+
fireEvent.change(endDateInput, { target: { value: '2024-12-31' } });
214+
215+
await waitFor(() => {
216+
const lastCall = mockPush.mock.calls[mockPush.mock.calls.length - 1][0];
217+
expect(lastCall).toContain('startDate=2024-01-01');
218+
expect(lastCall).toContain('endDate=2024-12-31');
219+
});
220+
});
221+
222+
it('should show clear dates button when dates are set', async () => {
223+
mockSearchParams.set('startDate', '2024-01-01');
224+
mockSearchParams.set('endDate', '2024-12-31');
225+
226+
render(<EarningsPage />);
227+
228+
await waitFor(() => {
229+
expect(screen.getByText(/Clear dates/i)).toBeInTheDocument();
230+
});
231+
});
232+
233+
it('should clear date params when clear button is clicked', async () => {
234+
mockSearchParams.set('startDate', '2024-01-01');
235+
mockSearchParams.set('endDate', '2024-12-31');
236+
237+
render(<EarningsPage />);
238+
239+
await waitFor(() => {
240+
expect(screen.getByText(/Clear dates/i)).toBeInTheDocument();
241+
});
242+
243+
const clearButton = screen.getByText(/Clear dates/i);
244+
fireEvent.click(clearButton);
245+
246+
await waitFor(() => {
247+
expect(mockPush).toHaveBeenCalledWith('/earnings', { scroll: false });
248+
});
249+
});
250+
});
251+
252+
describe('Combined filters', () => {
253+
it('should handle search and date filters together', async () => {
254+
render(<EarningsPage />);
255+
256+
await waitFor(() => {
257+
expect(screen.getByPlaceholderText(/Search by ID/i)).toBeInTheDocument();
258+
});
259+
260+
const searchInput = screen.getByPlaceholderText(/Search by ID/i);
261+
const startDateInput = screen.getByLabelText(/From/i);
262+
263+
fireEvent.change(searchInput, { target: { value: 'YouTube' } });
264+
fireEvent.change(startDateInput, { target: { value: '2024-01-01' } });
265+
266+
await waitFor(() => {
267+
const lastCall = mockPush.mock.calls[mockPush.mock.calls.length - 1][0];
268+
expect(lastCall).toContain('search=YouTube');
269+
expect(lastCall).toContain('startDate=2024-01-01');
270+
});
271+
});
272+
});
273+
274+
describe('Shareable URLs', () => {
275+
it('should load page with all filters from shared URL', async () => {
276+
mockSearchParams.set('search', 'TikTok');
277+
mockSearchParams.set('startDate', '2024-02-01');
278+
mockSearchParams.set('endDate', '2024-02-28');
279+
280+
render(<EarningsPage />);
281+
282+
await waitFor(() => {
283+
const searchInput = screen.getByPlaceholderText(/Search by ID/i) as HTMLInputElement;
284+
const startDateInput = screen.getByLabelText(/From/i) as HTMLInputElement;
285+
const endDateInput = screen.getByLabelText(/To/i) as HTMLInputElement;
286+
287+
expect(searchInput.value).toBe('TikTok');
288+
expect(startDateInput.value).toBe('2024-02-01');
289+
expect(endDateInput.value).toBe('2024-02-28');
290+
});
291+
});
292+
});
293+
294+
describe('Browser navigation compatibility', () => {
295+
it('should use router.push for filter changes', async () => {
296+
render(<EarningsPage />);
297+
298+
await waitFor(() => {
299+
expect(screen.getByPlaceholderText(/Search by ID/i)).toBeInTheDocument();
300+
});
301+
302+
const searchInput = screen.getByPlaceholderText(/Search by ID/i);
303+
fireEvent.change(searchInput, { target: { value: 'test' } });
304+
305+
await waitFor(() => {
306+
expect(mockPush).toHaveBeenCalledWith(
307+
expect.any(String),
308+
{ scroll: false }
309+
);
310+
});
311+
});
312+
});
313+
314+
describe('Transaction filtering', () => {
315+
it('should filter transactions by search term', async () => {
316+
mockSearchParams.set('search', 'YouTube');
317+
318+
render(<EarningsPage />);
319+
320+
await waitFor(() => {
321+
expect(screen.getByText(/YouTube Revenue/i)).toBeInTheDocument();
322+
expect(screen.queryByText(/TikTok Revenue/i)).not.toBeInTheDocument();
323+
expect(screen.queryByText(/Instagram Revenue/i)).not.toBeInTheDocument();
324+
});
325+
});
326+
327+
it('should filter transactions by date range', async () => {
328+
mockSearchParams.set('startDate', '2024-02-01');
329+
mockSearchParams.set('endDate', '2024-02-28');
330+
331+
render(<EarningsPage />);
332+
333+
await waitFor(() => {
334+
expect(screen.queryByText(/YouTube Revenue/i)).not.toBeInTheDocument(); // Jan
335+
expect(screen.getByText(/TikTok Revenue/i)).toBeInTheDocument(); // Feb
336+
expect(screen.queryByText(/Instagram Revenue/i)).not.toBeInTheDocument(); // Mar
337+
});
338+
});
339+
340+
it('should show transaction count', async () => {
341+
render(<EarningsPage />);
342+
343+
await waitFor(() => {
344+
expect(screen.getByText(/3 of 3 transactions/i)).toBeInTheDocument();
345+
});
346+
});
347+
348+
it('should update transaction count when filtered', async () => {
349+
mockSearchParams.set('search', 'YouTube');
350+
351+
render(<EarningsPage />);
352+
353+
await waitFor(() => {
354+
expect(screen.getByText(/1 of 3 transactions/i)).toBeInTheDocument();
355+
});
356+
});
357+
});
358+
359+
describe('Filter state persistence across navigation', () => {
360+
it('should maintain filters when returning from export', async () => {
361+
mockSearchParams.set('search', 'YouTube');
362+
mockSearchParams.set('startDate', '2024-01-01');
363+
364+
render(<EarningsPage />);
365+
366+
await waitFor(() => {
367+
const searchInput = screen.getByPlaceholderText(/Search by ID/i) as HTMLInputElement;
368+
expect(searchInput.value).toBe('YouTube');
369+
});
370+
371+
// Filters remain in URL, so they persist across navigation
372+
});
373+
});
374+
});

0 commit comments

Comments
 (0)