Skip to content

Commit 50d4e09

Browse files
authored
test(dashboard): add timezone normalization and boundary tests for DashboardClient (JhaSourav07#3613)
## Description Fixes JhaSourav07#2530 ### What Changed * **Timezone Boundary Tests (`components/dashboard/DashboardClient.timezone-boundaries.test.tsx`):** Added a new Vitest suite to verify system clock normalization, leap year parsing, and Daylight Savings Time (DST) offsets. * **Time Travel Mocks:** Implemented `vi.useFakeTimers()` to strictly control the virtual environment's clock. This allows us to safely test midnight crossovers and edge cases without flakey test runs. * **Component Isolation:** Mocked heavy visual children (like `ActivityLandscape` and `Heatmap`) to prevent JSDOM canvas crashes, ensuring lightning-fast integration testing of the core date-parsing logic. * **Named Export Fixes:** Correctly mocked `PopularRepos` as a named export and utilized `Parameters<typeof DashboardClient>` to resolve React rendering crashes and strict TypeScript `any` linting errors in the testing environment. ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [x] 🕐 Pillar 3 — Timezone Logic Optimization - [ ] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview **N/A for test changes. ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents 3267843 + 44fc006 commit 50d4e09

1 file changed

Lines changed: 141 additions & 0 deletions

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import DashboardClient from './DashboardClient';
4+
import type { DashboardPeriod } from '@/utils/dashboardPeriod';
5+
6+
// 1. Mock Next.js router to prevent crashes
7+
vi.mock('next/navigation', () => ({
8+
useRouter: () => ({ replace: vi.fn() }),
9+
}));
10+
11+
// 2. Isolate Date logic by mocking the heavy visual children
12+
vi.mock('./StatsCard', () => ({
13+
default: ({ title, utcDate }: { title: string; utcDate: string }) => (
14+
<div data-testid={`stats-card-${title.replace(/\s+/g, '-')}`} data-utc-date={utcDate}>
15+
{title}
16+
</div>
17+
),
18+
}));
19+
20+
// Mocking the rest of the visual components as empty divs to prevent JSDOM Canvas errors
21+
vi.mock('./ProfileCard', () => ({ default: () => <div /> }));
22+
vi.mock('./Achievements', () => ({ default: () => <div /> }));
23+
vi.mock('./ActivityLandscape', () => ({ default: () => <div /> }));
24+
vi.mock('./LanguageChart', () => ({ default: () => <div /> }));
25+
vi.mock('./CommitClock', () => ({ default: () => <div /> }));
26+
vi.mock('./HistoricalTrendView', () => ({ default: () => <div /> }));
27+
vi.mock('./AIInsights', () => ({ default: () => <div /> }));
28+
vi.mock('./PopularRepos', () => ({ PopularRepos: () => <div /> }));
29+
vi.mock('./RepositoryGraph', () => ({ default: () => <div /> }));
30+
vi.mock('./Heatmap', () => ({ default: () => <div /> }));
31+
vi.mock('./RefreshButton', () => ({ default: () => <div /> }));
32+
33+
// 3. Minimum mock data required to render the DashboardClient
34+
const mockPeriod = { label: 'Last Year' } as unknown as DashboardPeriod;
35+
const mockInitialData = {
36+
profile: { name: 'TestUser', stats: { repositories: 10 } },
37+
stats: { currentStreak: 5, peakStreak: 15, totalContributions: 100 },
38+
languages: [],
39+
activity: [{ date: '2026-06-03', count: 5, intensity: 3 }],
40+
insights: [],
41+
achievements: [],
42+
commitClock: [{ day: 'Mon', commits: 10 }],
43+
graphData: { nodes: [], links: [] },
44+
} as unknown as Parameters<typeof DashboardClient>[0]['initialData'];
45+
46+
describe('DashboardClient - Timezone Normalization & Boundary Alignment', () => {
47+
beforeEach(() => {
48+
// Enable simulated timers so we can freely manipulate the system clock
49+
vi.useFakeTimers();
50+
});
51+
52+
afterEach(() => {
53+
// Restore the real system clock after every test
54+
vi.useRealTimers();
55+
vi.clearAllMocks();
56+
});
57+
58+
// Test 1: Standard Timezone Boundaries (JST/EST to UTC)
59+
it('mocks standard timezone settings (JST/EST) and calculates commits onto correct UTC dates', () => {
60+
// Simulate Tokyo Time (JST) crossing midnight into June 4th (UTC is still June 3rd, 3:00 PM)
61+
vi.setSystemTime(new Date('2026-06-03T15:00:00Z'));
62+
const { rerender } = render(
63+
<DashboardClient initialData={mockInitialData} username="test" period={mockPeriod} />
64+
);
65+
66+
let streakCard = screen.getByTestId('stats-card-Current-Streak');
67+
expect(streakCard.getAttribute('data-utc-date')).toBe('2026-06-03');
68+
69+
// Simulate New York Time (EST) early morning June 4th (UTC is June 4th, 12:00 PM)
70+
vi.setSystemTime(new Date('2026-06-04T12:00:00Z'));
71+
rerender(<DashboardClient initialData={mockInitialData} username="test" period={mockPeriod} />);
72+
73+
streakCard = screen.getByTestId('stats-card-Current-Streak');
74+
expect(streakCard.getAttribute('data-utc-date')).toBe('2026-06-04');
75+
});
76+
77+
// Test 2: Leap Year Alignment
78+
it('verifies leap year boundaries parse without leaving gaps in calendar alignment', () => {
79+
// Set time strictly to a Leap Year boundary
80+
vi.setSystemTime(new Date('2024-02-29T12:00:00Z'));
81+
render(<DashboardClient initialData={mockInitialData} username="test" period={mockPeriod} />);
82+
83+
const streakCard = screen.getByTestId('stats-card-Current-Streak');
84+
// Ensure the dashboard doesn't accidentally roll over to March 1st
85+
expect(streakCard.getAttribute('data-utc-date')).toBe('2024-02-29');
86+
});
87+
88+
// Test 3: Daylight Savings Offsets
89+
it('tests offsets around transition dates like Daylight Savings Time (DST)', () => {
90+
// US Spring Forward happens at 2:00 AM in March.
91+
// Testing 1:59 AM (Before shift)
92+
vi.setSystemTime(new Date('2026-03-08T06:59:59Z')); // UTC equivalent
93+
const { rerender } = render(
94+
<DashboardClient initialData={mockInitialData} username="test" period={mockPeriod} />
95+
);
96+
97+
let streakCard = screen.getByTestId('stats-card-Current-Streak');
98+
expect(streakCard.getAttribute('data-utc-date')).toBe('2026-03-08');
99+
100+
// Testing 3:00 AM (After shift - skips 2:00 AM)
101+
vi.setSystemTime(new Date('2026-03-08T08:00:00Z')); // UTC equivalent
102+
rerender(<DashboardClient initialData={mockInitialData} username="test" period={mockPeriod} />);
103+
104+
streakCard = screen.getByTestId('stats-card-Current-Streak');
105+
// The UTC date should remain rock-solid regardless of the local DST jump
106+
expect(streakCard.getAttribute('data-utc-date')).toBe('2026-03-08');
107+
});
108+
109+
// Test 4: Calendar Date Formatting
110+
it('asserts calendar date format utility outputs match strictly YYYY-MM-DD expectations', () => {
111+
// Pick a single digit month and day to ensure no missing zero-padding
112+
vi.setSystemTime(new Date('2026-01-05T08:00:00Z'));
113+
render(<DashboardClient initialData={mockInitialData} username="test" period={mockPeriod} />);
114+
115+
const streakCard = screen.getByTestId('stats-card-Current-Streak');
116+
const dateStr = streakCard.getAttribute('data-utc-date') || '';
117+
118+
// Regex verifies it exactly matches YYYY-MM-DD format (e.g., 2026-01-05, not 2026-1-5)
119+
expect(dateStr).toMatch(/^\d{4}-\d{2}-\d{2}$/);
120+
expect(dateStr).toBe('2026-01-05');
121+
});
122+
123+
// Test 5: End of Year Roll-over
124+
it('verifies boundary alignment during the New Year crossover', () => {
125+
// One second before New Year 2027 UTC
126+
vi.setSystemTime(new Date('2026-12-31T23:59:59Z'));
127+
const { rerender } = render(
128+
<DashboardClient initialData={mockInitialData} username="test" period={mockPeriod} />
129+
);
130+
131+
let streakCard = screen.getByTestId('stats-card-Current-Streak');
132+
expect(streakCard.getAttribute('data-utc-date')).toBe('2026-12-31');
133+
134+
// Two seconds later... Happy New Year!
135+
vi.setSystemTime(new Date('2027-01-01T00:00:01Z'));
136+
rerender(<DashboardClient initialData={mockInitialData} username="test" period={mockPeriod} />);
137+
138+
streakCard = screen.getByTestId('stats-card-Current-Streak');
139+
expect(streakCard.getAttribute('data-utc-date')).toBe('2027-01-01');
140+
});
141+
});

0 commit comments

Comments
 (0)