@@ -2,9 +2,9 @@ import { describe, it, expect } from 'vitest';
22import {
33 calculateStreak ,
44 calculateMonthlyStats ,
5- isStreakAlive ,
6- aggregateCalendars ,
75 calculateWrappedStats ,
6+ aggregateCalendars ,
7+ isStreakAlive ,
88} from './calculate' ;
99import 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