|
| 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