From bf508185371476f74fbacd7556ca28ebd3777761 Mon Sep 17 00:00:00 2001 From: Aditya Kadam Date: Fri, 12 Jun 2026 13:00:42 +0530 Subject: [PATCH 1/2] test(ApiPr-insightsRoute-theme-contrast): verify Dark and Light Prefers-Color-Scheme Visual Cohesion --- .../pr-insights/route.theme-contrast.test.ts | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 app/api/pr-insights/route.theme-contrast.test.ts diff --git a/app/api/pr-insights/route.theme-contrast.test.ts b/app/api/pr-insights/route.theme-contrast.test.ts new file mode 100644 index 000000000..57b1716d4 --- /dev/null +++ b/app/api/pr-insights/route.theme-contrast.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { GET } from './route'; + +vi.mock('@/services/github/pr-insights', () => ({ + fetchPRInsights: vi.fn(), +})); + +import { fetchPRInsights } from '@/services/github/pr-insights'; +import type { PRInsightData } from '@/services/github/pr-insights'; + +const mockInsights: PRInsightData = { + totalPRs: 20, + openPRs: 5, + mergedPRs: 12, + closedPRs: 3, + mergeRate: 60, + avgReviewTime: 5, + avgTimeToFirstReview: 2, + avgCycleTime: 24, + weeklyActivity: [{ name: '2024-W01', prs: 3 }], + monthlyActivity: [{ name: '2024-01', prs: 8 }], + reviewsGiven: 7, + reviewsReceived: 9, + avgReviewResponseTime: 5, + fastestReview: 1, + slowestReview: 48, + repoPerformance: [ + { name: 'org/repo', totalPRs: 10, mergeRate: 70, reviewCount: 4, avgReviewTime: 6 }, + ], + highlights: { + mostDiscussed: { title: 'Big PR', url: 'https://github.com/org/repo/pull/1', comments: 12 }, + fastestMerged: { title: 'Quick fix', url: 'https://github.com/org/repo/pull/2', time: 0.5 }, + largest: { + title: 'Refactor', + url: 'https://github.com/org/repo/pull/3', + additions: 500, + deletions: 100, + }, + }, +}; + +function makeRequest( + params: Record = {}, + headers: Record = {} +): Request { + const url = new URL('http://localhost/api/pr-insights'); + for (const [key, value] of Object.entries(params)) { + url.searchParams.set(key, value); + } + return new Request(url.toString(), { + headers: new Headers(headers), + }); +} + +describe('GET /api/pr-insights theme-contrast: Dark and Light Prefers-Color-Scheme Visual Cohesion', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(fetchPRInsights).mockResolvedValue(mockInsights); + }); + + it('returns identical data structure regardless of dark or light prefers-color-scheme header', async () => { + const darkResponse = await GET( + makeRequest({ username: 'testuser' }, { 'sec-ch-prefers-color-scheme': 'dark' }) + ); + const lightResponse = await GET( + makeRequest({ username: 'testuser' }, { 'sec-ch-prefers-color-scheme': 'light' }) + ); + + const darkBody = await darkResponse.json(); + const lightBody = await lightResponse.json(); + + // API is theme-agnostic — payload must be identical for both color schemes + expect(darkBody).toEqual(lightBody); + expect(darkResponse.status).toBe(200); + expect(lightResponse.status).toBe(200); + }); + + it('returns numeric stat fields suitable for color-coded badges in both themes', async () => { + const response = await GET(makeRequest({ username: 'testuser' })); + const body = await response.json(); + + // mergeRate, totalPRs etc are rendered as colored stat cards in both light/dark UI + expect(typeof body.mergeRate).toBe('number'); + expect(typeof body.totalPRs).toBe('number'); + expect(typeof body.openPRs).toBe('number'); + expect(typeof body.mergedPRs).toBe('number'); + expect(typeof body.closedPRs).toBe('number'); + }); + + it('returns highlight fields with title and url required for accessible link contrast styling', async () => { + const response = await GET(makeRequest({ username: 'testuser' })); + const body = await response.json(); + + // Highlight cards need title/url text for theme-aware link styling + expect(body.highlights.mostDiscussed).toHaveProperty('title'); + expect(body.highlights.mostDiscussed).toHaveProperty('url'); + expect(body.highlights.fastestMerged).toHaveProperty('title'); + expect(body.highlights.largest).toHaveProperty('title'); + }); + + it('returns consistent error response and Content-Type regardless of theme headers', async () => { + const darkResponse = await GET(makeRequest({}, { 'sec-ch-prefers-color-scheme': 'dark' })); + const lightResponse = await GET(makeRequest({}, { 'sec-ch-prefers-color-scheme': 'light' })); + + expect(darkResponse.status).toBe(400); + expect(lightResponse.status).toBe(400); + + expect(darkResponse.headers.get('content-type')).toMatch(/application\/json/); + expect(lightResponse.headers.get('content-type')).toMatch(/application\/json/); + + const darkBody = await darkResponse.json(); + const lightBody = await lightResponse.json(); + expect(darkBody.error).toBe(lightBody.error); + }); + + it('returns repoPerformance array with mergeRate values usable for progress bar contrast styling in both themes', async () => { + const response = await GET(makeRequest({ username: 'testuser' })); + const body = await response.json(); + + expect(Array.isArray(body.repoPerformance)).toBe(true); + body.repoPerformance.forEach((repo: { mergeRate: number; name: string }) => { + // mergeRate drives progress bar fill color in both light/dark themes + expect(typeof repo.mergeRate).toBe('number'); + expect(repo.mergeRate).toBeGreaterThanOrEqual(0); + expect(repo.mergeRate).toBeLessThanOrEqual(100); + expect(typeof repo.name).toBe('string'); + }); + }); +}); From f0b8a31169af945b942575b0ec6544a00cb63df3 Mon Sep 17 00:00:00 2001 From: Aditya Kadam Date: Fri, 12 Jun 2026 13:49:32 +0530 Subject: [PATCH 2/2] test(ApiPr-insightsRoute): rewrite tests to cover business logic per maintainer feedback --- .../pr-insights/route.theme-contrast.test.ts | 110 ++++++++---------- 1 file changed, 50 insertions(+), 60 deletions(-) diff --git a/app/api/pr-insights/route.theme-contrast.test.ts b/app/api/pr-insights/route.theme-contrast.test.ts index 57b1716d4..bb3277d6c 100644 --- a/app/api/pr-insights/route.theme-contrast.test.ts +++ b/app/api/pr-insights/route.theme-contrast.test.ts @@ -39,91 +39,81 @@ const mockInsights: PRInsightData = { }, }; -function makeRequest( - params: Record = {}, - headers: Record = {} -): Request { +function makeRequest(params: Record = {}): Request { const url = new URL('http://localhost/api/pr-insights'); for (const [key, value] of Object.entries(params)) { url.searchParams.set(key, value); } - return new Request(url.toString(), { - headers: new Headers(headers), - }); + return new Request(url.toString()); } -describe('GET /api/pr-insights theme-contrast: Dark and Light Prefers-Color-Scheme Visual Cohesion', () => { +describe('GET /api/pr-insights', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(fetchPRInsights).mockResolvedValue(mockInsights); }); - it('returns identical data structure regardless of dark or light prefers-color-scheme header', async () => { - const darkResponse = await GET( - makeRequest({ username: 'testuser' }, { 'sec-ch-prefers-color-scheme': 'dark' }) - ); - const lightResponse = await GET( - makeRequest({ username: 'testuser' }, { 'sec-ch-prefers-color-scheme': 'light' }) - ); - - const darkBody = await darkResponse.json(); - const lightBody = await lightResponse.json(); - - // API is theme-agnostic — payload must be identical for both color schemes - expect(darkBody).toEqual(lightBody); - expect(darkResponse.status).toBe(200); - expect(lightResponse.status).toBe(200); - }); + it('returns 400 when the username parameter is missing', async () => { + const response = await GET(makeRequest()); - it('returns numeric stat fields suitable for color-coded badges in both themes', async () => { - const response = await GET(makeRequest({ username: 'testuser' })); + expect(response.status).toBe(400); const body = await response.json(); - - // mergeRate, totalPRs etc are rendered as colored stat cards in both light/dark UI - expect(typeof body.mergeRate).toBe('number'); - expect(typeof body.totalPRs).toBe('number'); - expect(typeof body.openPRs).toBe('number'); - expect(typeof body.mergedPRs).toBe('number'); - expect(typeof body.closedPRs).toBe('number'); + expect(body.error).toBe('Username is required'); + expect(fetchPRInsights).not.toHaveBeenCalled(); }); - it('returns highlight fields with title and url required for accessible link contrast styling', async () => { - const response = await GET(makeRequest({ username: 'testuser' })); + it('returns 400 when the username exceeds the 39 character GitHub limit', async () => { + const longUsername = 'a'.repeat(40); + const response = await GET(makeRequest({ username: longUsername })); + + expect(response.status).toBe(400); const body = await response.json(); + expect(body.error).toBe('Invalid GitHub username'); + expect(fetchPRInsights).not.toHaveBeenCalled(); + }); + + it('returns 400 for a username containing invalid characters', async () => { + const response = await GET(makeRequest({ username: 'invalid_user!' })); - // Highlight cards need title/url text for theme-aware link styling - expect(body.highlights.mostDiscussed).toHaveProperty('title'); - expect(body.highlights.mostDiscussed).toHaveProperty('url'); - expect(body.highlights.fastestMerged).toHaveProperty('title'); - expect(body.highlights.largest).toHaveProperty('title'); + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.error).toBe('Invalid GitHub username'); + expect(fetchPRInsights).not.toHaveBeenCalled(); }); - it('returns consistent error response and Content-Type regardless of theme headers', async () => { - const darkResponse = await GET(makeRequest({}, { 'sec-ch-prefers-color-scheme': 'dark' })); - const lightResponse = await GET(makeRequest({}, { 'sec-ch-prefers-color-scheme': 'light' })); + it('trims whitespace from the username before validation and fetching', async () => { + const response = await GET(makeRequest({ username: ' octocat ' })); - expect(darkResponse.status).toBe(400); - expect(lightResponse.status).toBe(400); + expect(response.status).toBe(200); + expect(fetchPRInsights).toHaveBeenCalledWith('octocat'); + }); - expect(darkResponse.headers.get('content-type')).toMatch(/application\/json/); - expect(lightResponse.headers.get('content-type')).toMatch(/application\/json/); + it('returns the full PR insights payload on a successful fetch', async () => { + const response = await GET(makeRequest({ username: 'octocat' })); - const darkBody = await darkResponse.json(); - const lightBody = await lightResponse.json(); - expect(darkBody.error).toBe(lightBody.error); + expect(response.status).toBe(200); + const body = await response.json(); + expect(body).toEqual(mockInsights); + expect(fetchPRInsights).toHaveBeenCalledWith('octocat'); }); - it('returns repoPerformance array with mergeRate values usable for progress bar contrast styling in both themes', async () => { - const response = await GET(makeRequest({ username: 'testuser' })); + it('returns 500 with the error message when fetchPRInsights throws an Error', async () => { + vi.mocked(fetchPRInsights).mockRejectedValue(new Error('GitHub API error')); + + const response = await GET(makeRequest({ username: 'octocat' })); + + expect(response.status).toBe(500); const body = await response.json(); + expect(body.error).toBe('GitHub API error'); + }); - expect(Array.isArray(body.repoPerformance)).toBe(true); - body.repoPerformance.forEach((repo: { mergeRate: number; name: string }) => { - // mergeRate drives progress bar fill color in both light/dark themes - expect(typeof repo.mergeRate).toBe('number'); - expect(repo.mergeRate).toBeGreaterThanOrEqual(0); - expect(repo.mergeRate).toBeLessThanOrEqual(100); - expect(typeof repo.name).toBe('string'); - }); + it('returns 500 with a generic message when fetchPRInsights throws a non-Error value', async () => { + vi.mocked(fetchPRInsights).mockRejectedValue('unexpected failure'); + + const response = await GET(makeRequest({ username: 'octocat' })); + + expect(response.status).toBe(500); + const body = await response.json(); + expect(body.error).toBe('Failed to fetch PR insights'); }); });