From e8e83bffec3faa9bbb9f07497b04c210732da468 Mon Sep 17 00:00:00 2001 From: Muskan Yadav Date: Sat, 4 Jul 2026 15:20:57 +0530 Subject: [PATCH 1/3] test(Highlights-timezone-boundaries): verify Timezone Normalization & Calendar Data Boundary Alignment (Variation 8) --- .../Highlights.timezone-boundaries.test.tsx | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 components/dashboard/PRInsights/Highlights.timezone-boundaries.test.tsx diff --git a/components/dashboard/PRInsights/Highlights.timezone-boundaries.test.tsx b/components/dashboard/PRInsights/Highlights.timezone-boundaries.test.tsx new file mode 100644 index 000000000..4947505cf --- /dev/null +++ b/components/dashboard/PRInsights/Highlights.timezone-boundaries.test.tsx @@ -0,0 +1,150 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import Highlights from './Highlights'; +import type { PRInsightData } from '@/services/github/pr-insights'; + +// Mock framer-motion to avoid animation issues in tests +vi.mock('framer-motion', () => ({ + motion: { + a: ({ + children, + className, + href, + target, + rel, + }: React.AnchorHTMLAttributes & { children: React.ReactNode }) => ( + + {children} + + ), + }, +})); + +// Mock TranslationContext +vi.mock('@/context/TranslationContext', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + 'dashboard.prInsights.fastest_merged': 'Fastest Merged', + 'dashboard.prInsights.most_discussed': 'Most Discussed', + 'dashboard.prInsights.largest_pr': 'Largest PR', + 'dashboard.prInsights.hrs': 'hrs', + 'dashboard.prInsights.comments': 'comments', + 'dashboard.prInsights.no_highlights': 'No highlights available', + }; + return translations[key] || key; + }, + }), +})); + +const mockHighlights: PRInsightData['highlights'] = { + fastestMerged: { + title: 'Fix UTC offset bug', + time: 2.5, + url: 'https://github.com/test/pr/1', + }, + mostDiscussed: { + title: 'Add IST timezone support', + comments: 42, + url: 'https://github.com/test/pr/2', + }, + largest: { + title: 'Refactor calendar boundary logic', + additions: 300, + deletions: 150, + url: 'https://github.com/test/pr/3', + }, +}; + +describe('Highlights — timezone boundaries', () => { + beforeEach(() => { + // Simulate UTC timezone environment + vi.stubGlobal('Intl', { + ...Intl, + DateTimeFormat: vi.fn().mockImplementation(() => ({ + resolvedOptions: () => ({ timeZone: 'UTC' }), + format: (date: Date) => date.toISOString().split('T')[0], + })), + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('renders all three highlight cards with correct values in UTC timezone', () => { + render(); + + expect(screen.getByText('Fastest Merged')).toBeInTheDocument(); + expect(screen.getByText('Most Discussed')).toBeInTheDocument(); + expect(screen.getByText('Largest PR')).toBeInTheDocument(); + expect(screen.getByText('2.5 hrs')).toBeInTheDocument(); + expect(screen.getByText('42 comments')).toBeInTheDocument(); + expect(screen.getByText('+300 -150')).toBeInTheDocument(); + }); + + it('displays N/A and no-highlights message when all highlight data is null across timezones', () => { + const emptyHighlights: PRInsightData['highlights'] = { + fastestMerged: null, + mostDiscussed: null, + largest: null, + }; + + render(); + + const naValues = screen.getAllByText('N/A'); + expect(naValues).toHaveLength(3); + + const noHighlights = screen.getAllByText('No highlights available'); + expect(noHighlights).toHaveLength(3); + }); + + it('correctly formats decimal time values across locale boundaries', () => { + const highlights: PRInsightData['highlights'] = { + ...mockHighlights, + fastestMerged: { + title: 'DST transition PR', + time: 23.999, + url: 'https://github.com/test/pr/4', + }, + }; + + render(); + + // toFixed(1) should normalize 23.999 to 24.0 across all locales + expect(screen.getByText('24.0 hrs')).toBeInTheDocument(); + }); + + it('renders PR links with correct href across different timezone contexts', () => { + render(); + + const links = screen.getAllByRole('link'); + expect(links).toHaveLength(3); + + expect(links[0]).toHaveAttribute('href', 'https://github.com/test/pr/1'); + expect(links[1]).toHaveAttribute('href', 'https://github.com/test/pr/2'); + expect(links[2]).toHaveAttribute('href', 'https://github.com/test/pr/3'); + + // All links open in new tab — safe across all timezone/locale environments + links.forEach((link) => { + expect(link).toHaveAttribute('target', '_blank'); + expect(link).toHaveAttribute('rel', 'noopener noreferrer'); + }); + }); + + it('handles leap year boundary date in PR title without rendering errors', () => { + const leapYearHighlights: PRInsightData['highlights'] = { + ...mockHighlights, + fastestMerged: { + title: 'Fix Feb 29 2024 leap year calendar boundary', + time: 1.0, + url: 'https://github.com/test/pr/5', + }, + }; + + render(); + + expect(screen.getByText('Fix Feb 29 2024 leap year calendar boundary')).toBeInTheDocument(); + expect(screen.getByText('1.0 hrs')).toBeInTheDocument(); + }); +}); From 195d7d894800a9db6e53445aa79b9f301fab2b5e Mon Sep 17 00:00:00 2001 From: Muskan Yadav Date: Sat, 4 Jul 2026 15:30:50 +0530 Subject: [PATCH 2/3] test(RepoPerformanceTable-error-resilience): verify Hydration Stability, Exception Safety & Error Fallbacks (Variation 6) --- ...PerformanceTable.error-resilience.test.tsx | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 components/dashboard/PRInsights/RepoPerformanceTable.error-resilience.test.tsx diff --git a/components/dashboard/PRInsights/RepoPerformanceTable.error-resilience.test.tsx b/components/dashboard/PRInsights/RepoPerformanceTable.error-resilience.test.tsx new file mode 100644 index 000000000..e6bd05aca --- /dev/null +++ b/components/dashboard/PRInsights/RepoPerformanceTable.error-resilience.test.tsx @@ -0,0 +1,124 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import RepoPerformanceTable from './RepoPerformanceTable'; +import type { PRInsightData } from '@/services/github/pr-insights'; + +// Mock framer-motion +vi.mock('framer-motion', () => ({ + motion: { + div: ({ children, className }: { children: React.ReactNode; className?: string }) => ( +
{children}
+ ), + }, +})); + +// Mock TranslationContext +vi.mock('@/context/TranslationContext', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + 'dashboard.prInsights.no_repos': 'No repositories found', + 'dashboard.prInsights.repo_title': 'Repository Performance', + 'dashboard.prInsights.repo_subtitle': 'PR stats by repository', + 'dashboard.prInsights.repo_header': 'Repository', + 'dashboard.prInsights.prs_header': 'PRs', + 'dashboard.prInsights.merge_rate_header': 'Merge Rate', + 'dashboard.prInsights.reviews_header': 'Reviews', + }; + return translations[key] || key; + }, + }), +})); + +const mockData: PRInsightData = { + repoPerformance: [ + { + name: 'owner/repo-one', + totalPRs: 10, + mergeRate: 80, + reviewCount: 5, + }, + { + name: 'owner/repo-two', + totalPRs: 3, + mergeRate: 66.7, + reviewCount: 2, + }, + ], + highlights: { + fastestMerged: null, + mostDiscussed: null, + largest: null, + }, + summary: { + totalPRs: 13, + mergedPRs: 10, + openPRs: 2, + closedPRs: 1, + avgMergeTimeHours: 4.5, + mergeRate: 76.9, + }, + timeline: [], +}; + +describe('RepoPerformanceTable — error resilience', () => { + it('renders empty state fallback UI when repoPerformance is an empty array', () => { + const emptyData: PRInsightData = { ...mockData, repoPerformance: [] }; + render(); + + expect(screen.getByText('No repositories found')).toBeInTheDocument(); + }); + + it('renders empty state fallback UI when repoPerformance is null', () => { + const nullData: PRInsightData = { + ...mockData, + repoPerformance: null as unknown as PRInsightData['repoPerformance'], + }; + render(); + + expect(screen.getByText('No repositories found')).toBeInTheDocument(); + }); + + it('renders table correctly with valid repo data without crashing', () => { + render(); + + expect(screen.getByText('Repository Performance')).toBeInTheDocument(); + expect(screen.getByText('repo-one')).toBeInTheDocument(); + expect(screen.getByText('repo-two')).toBeInTheDocument(); + expect(screen.getByText('80%')).toBeInTheDocument(); + }); + + it('handles repo name without slash gracefully without throwing', () => { + const noSlashData: PRInsightData = { + ...mockData, + repoPerformance: [ + { + name: 'standalone-repo', + totalPRs: 5, + mergeRate: 100, + reviewCount: 3, + }, + ], + }; + render(); + + // name.split('/')[1] returns undefined, so fallback to full name + // Component renders name in both title and subtitle divs + const repoElements = screen.getAllByText('standalone-repo'); + expect(repoElements.length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('100%')).toBeInTheDocument(); + }); + + it('renders merge rate progress bar width correctly without overflow errors', () => { + const { container } = render(); + + const progressBars = container.querySelectorAll('.bg-cyan-500'); + expect(progressBars).toHaveLength(2); + + const firstBar = progressBars[0] as HTMLElement; + expect(firstBar.style.width).toBe('80%'); + + const secondBar = progressBars[1] as HTMLElement; + expect(secondBar.style.width).toBe('66.7%'); + }); +}); From 6a3d7d348262a3025da85f8f2c10a9262598c679 Mon Sep 17 00:00:00 2001 From: Muskan Yadav Date: Sat, 4 Jul 2026 16:37:29 +0530 Subject: [PATCH 3/3] fix: correct PRInsightData type mismatches in test mocks --- .../Highlights.timezone-boundaries.test.tsx | 10 +++--- ...PerformanceTable.error-resilience.test.tsx | 36 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/components/dashboard/PRInsights/Highlights.timezone-boundaries.test.tsx b/components/dashboard/PRInsights/Highlights.timezone-boundaries.test.tsx index 4947505cf..d44455b73 100644 --- a/components/dashboard/PRInsights/Highlights.timezone-boundaries.test.tsx +++ b/components/dashboard/PRInsights/Highlights.timezone-boundaries.test.tsx @@ -56,7 +56,7 @@ const mockHighlights: PRInsightData['highlights'] = { }, }; -describe('Highlights — timezone boundaries', () => { +describe('Highlights - timezone boundaries', () => { beforeEach(() => { // Simulate UTC timezone environment vi.stubGlobal('Intl', { @@ -85,9 +85,9 @@ describe('Highlights — timezone boundaries', () => { it('displays N/A and no-highlights message when all highlight data is null across timezones', () => { const emptyHighlights: PRInsightData['highlights'] = { - fastestMerged: null, - mostDiscussed: null, - largest: null, + fastestMerged: undefined, + mostDiscussed: undefined, + largest: undefined, }; render(); @@ -125,7 +125,7 @@ describe('Highlights — timezone boundaries', () => { expect(links[1]).toHaveAttribute('href', 'https://github.com/test/pr/2'); expect(links[2]).toHaveAttribute('href', 'https://github.com/test/pr/3'); - // All links open in new tab — safe across all timezone/locale environments + // All links open in new tab - safe across all timezone/locale environments links.forEach((link) => { expect(link).toHaveAttribute('target', '_blank'); expect(link).toHaveAttribute('rel', 'noopener noreferrer'); diff --git a/components/dashboard/PRInsights/RepoPerformanceTable.error-resilience.test.tsx b/components/dashboard/PRInsights/RepoPerformanceTable.error-resilience.test.tsx index e6bd05aca..e7dd59e04 100644 --- a/components/dashboard/PRInsights/RepoPerformanceTable.error-resilience.test.tsx +++ b/components/dashboard/PRInsights/RepoPerformanceTable.error-resilience.test.tsx @@ -31,37 +31,46 @@ vi.mock('@/context/TranslationContext', () => ({ })); const mockData: PRInsightData = { + totalPRs: 13, + openPRs: 2, + mergedPRs: 10, + closedPRs: 1, + mergeRate: 76.9, + avgReviewTime: 4.5, + avgTimeToFirstReview: 3.0, + avgCycleTime: 4.5, + weeklyActivity: [], + monthlyActivity: [], + reviewsGiven: 7, + reviewsReceived: 7, + avgReviewResponseTime: 4.5, + fastestReview: 1.0, + slowestReview: 6.0, repoPerformance: [ { name: 'owner/repo-one', totalPRs: 10, mergeRate: 80, reviewCount: 5, + avgReviewTime: 1.8, }, { name: 'owner/repo-two', totalPRs: 3, mergeRate: 66.7, reviewCount: 2, + avgReviewTime: 3.4, }, ], highlights: { - fastestMerged: null, - mostDiscussed: null, - largest: null, + fastestMerged: undefined, + mostDiscussed: undefined, + largest: undefined, }, - summary: { - totalPRs: 13, - mergedPRs: 10, - openPRs: 2, - closedPRs: 1, - avgMergeTimeHours: 4.5, - mergeRate: 76.9, - }, - timeline: [], + prs: [], }; -describe('RepoPerformanceTable — error resilience', () => { +describe('RepoPerformanceTable - error resilience', () => { it('renders empty state fallback UI when repoPerformance is an empty array', () => { const emptyData: PRInsightData = { ...mockData, repoPerformance: [] }; render(); @@ -97,6 +106,7 @@ describe('RepoPerformanceTable — error resilience', () => { totalPRs: 5, mergeRate: 100, reviewCount: 3, + avgReviewTime: 2.0, }, ], };