diff --git a/src/lib/__tests__/yearInReviewUtils.test.ts b/src/lib/__tests__/yearInReviewUtils.test.ts new file mode 100644 index 0000000..bc525b3 --- /dev/null +++ b/src/lib/__tests__/yearInReviewUtils.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect } from "vitest"; +import { + buildHourlyHeatmapFromCommitDates, + getMostActiveHour, + getMostActiveDayFromCalendar +} from "@/lib/yearInReviewUtils"; + +describe("buildHourlyHeatmapFromCommitDates", () => { + it("returns a 7x24 heatmap initialized with zeros for an empty array", () => { + const heatmap = buildHourlyHeatmapFromCommitDates([]); + expect(heatmap).toHaveLength(7); + heatmap.forEach(day => { + expect(day).toHaveLength(24); + expect(day.every(count => count === 0)).toBe(true); + }); + }); + + it("correctly counts commit dates based on UTC day and hour", () => { + const commitDates = [ + "2023-01-01T10:00:00Z", // Sunday (0), Hour 10 + "2023-01-01T10:30:00Z", // Sunday (0), Hour 10 + "2023-01-02T15:45:00Z", // Monday (1), Hour 15 + "2023-01-07T23:59:59Z", // Saturday (6), Hour 23 + ]; + const heatmap = buildHourlyHeatmapFromCommitDates(commitDates); + + expect(heatmap[0][10]).toBe(2); + expect(heatmap[1][15]).toBe(1); + expect(heatmap[6][23]).toBe(1); + + // Verify other slots are 0 + expect(heatmap[0][11]).toBe(0); + expect(heatmap[2][15]).toBe(0); + }); + + it("ignores invalid date strings", () => { + // Additional invalid date assertions to address regression tests mentioned + expect(buildHourlyHeatmapFromCommitDates(["invalid-date"])).toEqual(Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0))); + const commitDates = [ + "2023-01-01T10:00:00Z", + "invalid-date", + "not-a-date" + ]; + const heatmap = buildHourlyHeatmapFromCommitDates(commitDates); + + expect(heatmap[0][10]).toBe(1); + // All other entries should be 0 + const totalCommits = heatmap.flat().reduce((sum, count) => sum + count, 0); + expect(totalCommits).toBe(1); + }); +}); + +describe("getMostActiveHour", () => { + it("returns 0 if heatmap is malformed (not 7x24 matrix)", () => { + expect(getMostActiveHour([])).toBe(0); + expect(getMostActiveHour([[1,2,3]])).toBe(0); + }); + + it("returns 0 for malformed heatmaps with NaN or Infinity", () => { + const heatmapWithNaN = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => NaN)); + expect(getMostActiveHour(heatmapWithNaN)).toBe(0); + const heatmapWithInfinity = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => Infinity)); + expect(getMostActiveHour(heatmapWithInfinity)).toBe(0); + }); + + it("returns 0 for an empty heatmap (all zeros)", () => { + const heatmap = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0)); + expect(getMostActiveHour(heatmap)).toBe(0); + }); + + it("returns the hour with the most commits across all days", () => { + const heatmap = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0)); + heatmap[0][10] = 5; // Sunday hour 10: 5 commits + heatmap[1][10] = 3; // Monday hour 10: 3 commits -> Total 8 + + heatmap[2][15] = 4; // Tuesday hour 15: 4 commits + heatmap[3][15] = 5; // Wednesday hour 15: 5 commits -> Total 9 + + expect(getMostActiveHour(heatmap)).toBe(15); + }); + + it("returns the first encountered hour in case of a tie", () => { + const heatmap = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0)); + heatmap[0][5] = 10; // Total 10 for hour 5 + heatmap[0][12] = 10; // Total 10 for hour 12 + heatmap[0][20] = 10; // Total 10 for hour 20 + + // Hour 5 is encountered first in the 0..23 loop + expect(getMostActiveHour(heatmap)).toBe(5); + }); +}); + +describe("getMostActiveDayFromCalendar", () => { + it("returns 'Sunday' when the calendar is empty", () => { + expect(getMostActiveDayFromCalendar([])).toBe("Sunday"); + }); + + it("correctly identifies the most active day of the week", () => { + const calendar = [ + { date: "2023-01-01", count: 5 }, // Sunday + { date: "2023-01-02", count: 10 }, // Monday + { date: "2023-01-08", count: 3 }, // Sunday -> Sunday total: 8, Monday total: 10 + { date: "2023-01-04", count: 2 }, // Wednesday -> Wednesday total: 2 + ]; + expect(getMostActiveDayFromCalendar(calendar)).toBe("Monday"); + }); + + it("ignores days with zero or negative counts", () => { + const calendar = [ + { date: "2023-01-01", count: 0 }, // Sunday + { date: "2023-01-02", count: -5 }, // Monday + { date: "2023-01-03", count: 2 }, // Tuesday + ]; + expect(getMostActiveDayFromCalendar(calendar)).toBe("Tuesday"); + }); + + it("returns the first encountered day in case of a tie", () => { + const calendar = [ + { date: "2023-01-02", count: 10 }, // Monday (index 1) + { date: "2023-01-04", count: 10 }, // Wednesday (index 3) + ]; + // "Monday" should be returned since it appears earlier in the [Sunday, Monday, ...] array + expect(getMostActiveDayFromCalendar(calendar)).toBe("Monday"); + }); +}); diff --git a/src/lib/yearInReviewUtils.ts b/src/lib/yearInReviewUtils.ts index 9574e09..6862c0d 100644 --- a/src/lib/yearInReviewUtils.ts +++ b/src/lib/yearInReviewUtils.ts @@ -13,6 +13,14 @@ export function buildHourlyHeatmapFromCommitDates(commitDates: string[]): number } export function getMostActiveHour(heatmap: number[][]): number { + const isValidHeatmap = + Array.isArray(heatmap) && + heatmap.length === 7 && + heatmap.every((row) => Array.isArray(row) && row.length === 24 && row.every((count) => Number.isFinite(count))); + + if (!isValidHeatmap) { + return 0; + } let maxCount = -1; let mostActiveHour = 0; for (let hour = 0; hour < 24; hour += 1) { @@ -36,7 +44,11 @@ export function getMostActiveDayFromCalendar(calendar: { date: string; count: nu if (day.count <= 0) { continue; } - const weekday = new Date(`${day.date}T00:00:00Z`).getUTCDay(); + const parsedDate = new Date(`${day.date}T00:00:00Z`); + if (Number.isNaN(parsedDate.getTime())) { + continue; + } + const weekday = parsedDate.getUTCDay(); totals[weekday] += day.count; }