Skip to content

Commit b081d07

Browse files
authored
test(calculate): verify streak formulas for different starting days (JhaSourav07#1746)
## Description Adds test cases to verify streak calculations for different starting days of the week. Fixes JhaSourav07#1476 ## Pillar - [ ] 🎨 Pillar 1 – New Theme Design - [ ] 🔷 Pillar 2 – Geometric SVG Improvement - [x] 🕒 Pillar 3 – Timezone Logic Optimization - [ ] 🛠 Other (Bug fix, refactoring, docs) ## Type of change - [x] test ## Checklist - [x] I have run npm run format - [x] I have run npm run lint - [x] I have run npm run test - [x] Tests are passing locally
2 parents d70a4b8 + cdaf3be commit b081d07

1 file changed

Lines changed: 196 additions & 2 deletions

File tree

lib/calculate.test.ts

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { describe, it, expect } from 'vitest';
22
import {
33
calculateStreak,
44
calculateMonthlyStats,
5-
isStreakAlive,
6-
aggregateCalendars,
75
calculateWrappedStats,
6+
aggregateCalendars,
7+
isStreakAlive,
88
} from './calculate';
99
import type { ContributionCalendar } from '../types';
1010

@@ -1632,3 +1632,197 @@ describe('calculateWrappedStats', () => {
16321632
expect(resultUTCPlus5.todayDate).toBe('2024-01-15');
16331633
});
16341634
});
1635+
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
1636+
1637+
it('todayDate matches YYYY-MM-DD for a normal calendar', () => {
1638+
// A typical calendar with contributions — todayDate must always be a valid date string
1639+
// regardless of the contribution data, so the SVG pulse animation targets the right tower.
1640+
const calendar = buildCalendar([1, 0, 1, 1, 0, 1, 1]);
1641+
const fixedNow = new Date('2024-01-07T12:00:00Z');
1642+
const result = calculateStreak(calendar, 'UTC', fixedNow);
1643+
expect(result.todayDate).toMatch(DATE_REGEX);
1644+
});
1645+
1646+
it('todayDate matches YYYY-MM-DD for an empty calendar', () => {
1647+
// An empty calendar has no days to fall back on, so todayDate is derived
1648+
// purely from the current date — it must still be a valid YYYY-MM-DD string.
1649+
const emptyCalendar = buildCalendar([]);
1650+
const fixedNow = new Date('2024-03-15T00:00:00Z');
1651+
const result = calculateStreak(emptyCalendar, 'UTC', fixedNow);
1652+
expect(result.todayDate).toMatch(DATE_REGEX);
1653+
});
1654+
1655+
it('todayDate matches YYYY-MM-DD when a non-UTC timezone shifts the local date', () => {
1656+
// When the caller passes a timezone like Asia/Kolkata, the local date can differ
1657+
// from UTC (e.g. UTC is still Jan 14 but IST is already Jan 15).
1658+
// The format must remain YYYY-MM-DD regardless of which day the timezone lands on.
1659+
const calendar = buildCalendar([1, 1, 1, 1, 1, 1, 1]);
1660+
const fixedNow = new Date('2024-01-07T20:00:00Z'); // 01:30 Jan 8 in IST (UTC+5:30)
1661+
const result = calculateStreak(calendar, 'Asia/Kolkata', fixedNow);
1662+
expect(result.todayDate).toMatch(DATE_REGEX);
1663+
});
1664+
1665+
describe('calculateStreak — year boundary transition (Dec 31 → Jan 1)', () => {
1666+
// Streak math relies on the flattened day array being chronologically ordered,
1667+
// so a run that crosses from December into January must be counted as a single
1668+
// continuous streak. This guards against off-by-one bugs where the calendar
1669+
// year rollover (e.g. 2024-12-31 → 2025-01-01) is mistakenly treated as a gap.
1670+
it('counts a streak that spans the Dec 31 → Jan 1 boundary as one continuous run', () => {
1671+
const calendar: ContributionCalendar = {
1672+
totalContributions: 7,
1673+
weeks: [
1674+
{
1675+
contributionDays: [
1676+
{ contributionCount: 0, date: '2024-12-26' }, // gap before the streak begins
1677+
{ contributionCount: 1, date: '2024-12-27' },
1678+
{ contributionCount: 1, date: '2024-12-28' },
1679+
{ contributionCount: 1, date: '2024-12-29' },
1680+
{ contributionCount: 1, date: '2024-12-30' },
1681+
{ contributionCount: 1, date: '2024-12-31' }, // last day of the year
1682+
{ contributionCount: 1, date: '2025-01-01' }, // first day of the new year
1683+
],
1684+
},
1685+
{
1686+
contributionDays: [
1687+
{ contributionCount: 1, date: '2025-01-02' }, // "today"
1688+
],
1689+
},
1690+
],
1691+
};
1692+
1693+
// Pin "now" to Jan 2 so the final day is treated as today and the streak is live.
1694+
const now = new Date('2025-01-02T12:00:00Z');
1695+
const result = calculateStreak(calendar, 'UTC', now);
1696+
1697+
// The 7-day run (Dec 27 → Jan 2) must not be split by the year rollover.
1698+
expect(result.currentStreak).toBe(7);
1699+
expect(result.longestStreak).toBe(7);
1700+
expect(result.totalContributions).toBe(7);
1701+
expect(result.todayDate).toBe('2025-01-02');
1702+
});
1703+
});
1704+
1705+
// ---------- EPIC ENHANCEMENT TESTS ----------
1706+
1707+
describe('aggregateCalendars', () => {
1708+
it('handles calendars with different numbers of weeks', () => {
1709+
const cal1 = {
1710+
totalContributions: 15,
1711+
weeks: [
1712+
{
1713+
contributionDays: [{ date: '2024-01-01', contributionCount: 5 }],
1714+
},
1715+
{
1716+
contributionDays: [{ date: '2024-01-08', contributionCount: 10 }],
1717+
},
1718+
],
1719+
};
1720+
1721+
const cal2 = {
1722+
totalContributions: 3,
1723+
weeks: [
1724+
{
1725+
contributionDays: [{ date: '2024-01-01', contributionCount: 3 }],
1726+
},
1727+
],
1728+
};
1729+
1730+
const result = aggregateCalendars([cal1, cal2]);
1731+
1732+
expect(result.weeks).toHaveLength(2);
1733+
expect(result.weeks[0].contributionDays[0].contributionCount).toBe(8);
1734+
expect(result.weeks[1].contributionDays[0].contributionCount).toBe(10);
1735+
});
1736+
1737+
it('returns an empty calendar if no calendars are provided', () => {
1738+
const result = aggregateCalendars([]);
1739+
expect(result.totalContributions).toBe(0);
1740+
expect(result.weeks).toEqual([]);
1741+
});
1742+
1743+
it('aggregates multiple calendars correctly for orgs', () => {
1744+
const cal1 = buildCalendar([1, 0, 2]); // total: 3
1745+
const cal2 = buildCalendar([0, 3, 1]); // total: 4
1746+
1747+
const result = aggregateCalendars([cal1, cal2]);
1748+
1749+
expect(result.totalContributions).toBe(7);
1750+
expect(result.weeks[0].contributionDays[0].contributionCount).toBe(1); // 1 + 0
1751+
expect(result.weeks[0].contributionDays[1].contributionCount).toBe(3); // 0 + 3
1752+
expect(result.weeks[0].contributionDays[2].contributionCount).toBe(3); // 2 + 1
1753+
});
1754+
});
1755+
1756+
describe('calculateWrappedStats', () => {
1757+
it('returns weekendRatio as 0 when all contributions occur on weekdays', () => {
1758+
const calendar = {
1759+
totalContributions: 25,
1760+
weeks: [
1761+
{
1762+
contributionDays: [
1763+
{ date: '2024-01-01', contributionCount: 5 }, // Mon
1764+
{ date: '2024-01-02', contributionCount: 5 }, // Tue
1765+
{ date: '2024-01-03', contributionCount: 5 }, // Wed
1766+
{ date: '2024-01-04', contributionCount: 5 }, // Thu
1767+
{ date: '2024-01-05', contributionCount: 5 }, // Fri
1768+
],
1769+
},
1770+
],
1771+
};
1772+
1773+
const result = calculateWrappedStats(calendar);
1774+
1775+
expect(result.weekendRatio).toBe(0);
1776+
});
1777+
1778+
it('calculates GitHub Wrapped stats accurately', () => {
1779+
// 2024-01-01 was a Monday. Indices 5 (Sat) and 6 (Sun) are the weekend.
1780+
const cal = buildCalendar([0, 0, 0, 0, 0, 5, 15]);
1781+
1782+
const result = calculateWrappedStats(cal);
1783+
1784+
expect(result.totalContributions).toBe(20);
1785+
expect(result.highestDailyCount).toBe(15);
1786+
expect(result.mostActiveDate).toBe('2024-01-07');
1787+
expect(result.busiestMonth).toBe('2024-01');
1788+
expect(result.weekendRatio).toBe(100);
1789+
});
1790+
1791+
// ISSUE OBJECTIVE #1056: Verify empty calendar returns safe zero values
1792+
it('verify empty calendar returns safe zero values', () => {
1793+
// 1. Call calculateWrappedStats with empty data
1794+
expect(() => calculateWrappedStats({ totalContributions: 0, weeks: [] })).not.toThrow();
1795+
1796+
// 2. Actually get the result to test its properties
1797+
const result = calculateWrappedStats({ totalContributions: 0, weeks: [] });
1798+
1799+
// 3. Assert weekendRatio === 0 (and specifically not NaN)
1800+
expect(result.weekendRatio).toBe(0);
1801+
1802+
// 4. Assert highestDailyCount === 0
1803+
expect(result.highestDailyCount).toBe(0);
1804+
});
1805+
1806+
// ISSUE OBJECTIVE: Verify weekendRatio is 100 when all commits are on weekends
1807+
// =========================================================================
1808+
it('returns weekendRatio === 100 when all contributions are on weekends', () => {
1809+
// Note: 2026-05-02 is a Saturday, 2026-05-03 is a Sunday, 2026-05-04 is a Monday
1810+
const weekendCalendar = {
1811+
totalContributions: 10,
1812+
weeks: [
1813+
{
1814+
contributionDays: [
1815+
{ date: '2026-05-02', contributionCount: 5 }, // Saturday (Weekend)
1816+
{ date: '2026-05-03', contributionCount: 5 }, // Sunday (Weekend)
1817+
{ date: '2026-05-04', contributionCount: 0 }, // Monday (Weekday - 0 commits)
1818+
],
1819+
},
1820+
],
1821+
} as Parameters<typeof calculateWrappedStats>[0]; // Safely infers the exact type the function expects!
1822+
1823+
const result = calculateWrappedStats(weekendCalendar);
1824+
1825+
// Assert the ratio is exactly 100%
1826+
expect(result.weekendRatio).toBe(100);
1827+
});
1828+
});

0 commit comments

Comments
 (0)