diff --git a/src/lib/__tests__/githubYearInReview.test.ts b/src/lib/__tests__/githubYearInReview.test.ts index f89b1ac..55216c5 100644 --- a/src/lib/__tests__/githubYearInReview.test.ts +++ b/src/lib/__tests__/githubYearInReview.test.ts @@ -408,6 +408,87 @@ describe("fetchYearInReviewData additional coverage", () => { } }); + + + + it("handles missing repository data in mergeTopRepository", async () => { + mockFetch.mockImplementation(() => { + return Promise.resolve(jsonResponse({ + data: { + user: { + id: "123", + contributionsCollection: { + totalCommitContributions: 100, + totalPullRequestContributions: 50, + totalIssueContributions: 10, + totalPullRequestReviewContributions: 0, + contributionCalendar: { totalContributions: 160, weeks: [] }, + } + } + } + })); + }); + + const data = await fetchYearInReviewData("testuser", 2023, "token"); + expect(data.topRepository).toBeNull(); + }); + + it("handles undefined repository data in mergeTopRepository", async () => { + mockFetch.mockImplementation(() => { + return Promise.resolve(jsonResponse({ + data: { + user: { + id: "123", + contributionsCollection: { + totalCommitContributions: 100, + totalPullRequestContributions: 50, + totalIssueContributions: 10, + totalPullRequestReviewContributions: 0, + contributionCalendar: { totalContributions: 160, weeks: [] }, + commitContributionsByRepository: undefined, + pullRequestContributionsByRepository: undefined, + issueContributionsByRepository: undefined, + } + } + } + })); + }); + + const data = await fetchYearInReviewData("testuser", 2023, "token"); + expect(data.topRepository).toBeNull(); + }); + + it("handles full repository data in mergeTopRepository", async () => { + mockFetch.mockImplementation(() => { + return Promise.resolve(jsonResponse({ + data: { + user: { + id: "123", + contributionsCollection: { + totalCommitContributions: 100, + totalPullRequestContributions: 50, + totalIssueContributions: 10, + totalPullRequestReviewContributions: 0, + contributionCalendar: { totalContributions: 160, weeks: [] }, + commitContributionsByRepository: [ + { repository: { owner: { login: "user1" }, name: "repo1" }, contributions: { totalCount: 50 } } + ], + pullRequestContributionsByRepository: [ + { repository: { owner: { login: "user1" }, name: "repo1" }, contributions: { totalCount: 20 } } + ], + issueContributionsByRepository: [ + { repository: { owner: { login: "user1" }, name: "repo1" }, contributions: { totalCount: 10 } } + ], + } + } + } + })); + }); + + const data = await fetchYearInReviewData("testuser", 2023, "token"); + expect(data.topRepository).toEqual({ name: "user1/repo1", contributions: 80 }); + }); + it("handles partial repository data in mergeTopRepository", async () => { mockFetch.mockImplementation(() => { return Promise.resolve(jsonResponse({ diff --git a/src/lib/github.ts b/src/lib/github.ts index d35c21b..ed7dd37 100644 --- a/src/lib/github.ts +++ b/src/lib/github.ts @@ -743,13 +743,24 @@ export const fetchActivity = cache(async function fetchActivity( /** * 効率的に Map から上位 K 件を抽出するヘルパー関数 - * 上位 K 件だけを返します。 + * 配列の作成とソートを最小限に抑えることでパフォーマンスを向上させます */ function getTopK(map: Map, k: number = 10): { name: string; count: number }[] { - const limit = Math.max(0, k); - return Array.from(map, ([name, count]) => ({ name, count })) - .sort((a, b) => b.count - a.count) - .slice(0, limit); + const top: { name: string; count: number }[] = []; + for (const [name, count] of map.entries()) { + if (top.length < k) { + top.push({ name, count }); + top.sort((a, b) => b.count - a.count); + } else if (count > top[k - 1].count) { + let i = k - 2; + while (i >= 0 && top[i].count < count) { + top[i + 1] = top[i]; + i--; + } + top[i + 1] = { name, count }; + } + } + return top; } /** diff --git a/src/lib/githubYearInReview.ts b/src/lib/githubYearInReview.ts index 23c4538..36fc68f 100644 --- a/src/lib/githubYearInReview.ts +++ b/src/lib/githubYearInReview.ts @@ -118,15 +118,19 @@ async function graphql(query: string, token: string, variables: Record["contributionsCollection"]): { name: string; contributions: number } | null { const counter = new Map(); + const buckets = [ - ...(data.commitContributionsByRepository || []), - ...(data.pullRequestContributionsByRepository || []), - ...(data.issueContributionsByRepository || []), + data.commitContributionsByRepository, + data.pullRequestContributionsByRepository, + data.issueContributionsByRepository, ]; - for (const item of buckets) { - const name = `${item.repository.owner.login}/${item.repository.name}`; - counter.set(name, (counter.get(name) ?? 0) + item.contributions.totalCount); + for (const bucket of buckets) { + if (!bucket) continue; + for (const item of bucket) { + const name = `${item.repository.owner.login}/${item.repository.name}`; + counter.set(name, (counter.get(name) ?? 0) + item.contributions.totalCount); + } } let top: { name: string; contributions: number } | null = null; diff --git a/vitest.config.ts b/vitest.config.ts index 9404856..eba2a2b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,6 +13,7 @@ export default defineConfig({ provider: "v8", reporter: ["text", "lcov"], include: [ + "src/lib/githubYearInReview.ts", "src/lib/**/*.ts", "src/hooks/**/*.ts", "src/components/ThemeController.tsx",