Skip to content

Commit a14d064

Browse files
authored
test(calculate): verify streak formulas for massive single-day commit spike timeline (JhaSourav07#1853)
## Description Fixes JhaSourav07#1507 Added a new test case in `lib/calculate.test.ts` to verify streak calculations for a timeline containing a massive single-day commit spike alongside low-frequency contribution periods. This ensures streak values are calculated based on active contribution days, not the raw number of commits on a single day. ## Pillar * [ ] 🎨 Pillar 1 — New Theme Design * [ ] 📐 Pillar 2 — Geometric SVG Improvement * [ ] 🕐 Pillar 3 — Timezone Logic Optimization * [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview Not applicable — this PR only adds test coverage. ## Checklist before requesting a review: * [x] I have read the `CONTRIBUTING.md` file. * [x] I have tested these changes locally. * [x] I have run `npm run format` and `npm run lint` locally and resolved all errors. * [x] My commits follow the Conventional Commits format. * [ ] I have updated `README.md` if I added a new theme or URL parameter. * [x] I have started the repo. * [x] I have made sure that i have only one commit to merge in this PR. * [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard. * [ ] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents ed4fc4a + 572bfcc commit a14d064

1 file changed

Lines changed: 41 additions & 84 deletions

File tree

lib/calculate.test.ts

Lines changed: 41 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { describe, it, expect, vi } from 'vitest';
1+
import { describe, it, expect } from 'vitest';
22
import {
33
calculateStreak,
44
calculateMonthlyStats,
5+
isStreakAlive,
56
aggregateCalendars,
67
calculateWrappedStats,
7-
findTodayIndex,
88
} from './calculate';
99
import type { ContributionCalendar } from '../types';
1010

@@ -58,18 +58,32 @@ describe('calculateStreak', () => {
5858
expect(s.longestStreak).toBe(2);
5959
});
6060

61+
it('handles a massive single-day commit spike without affecting streak calculations', () => {
62+
const calendar = buildCalendar([
63+
1, 0, 1, 0, 1, 0, 1,
64+
65+
0, 0, 125, 0, 0, 0, 0,
66+
67+
1, 1, 1, 1, 1, 0, 0,
68+
69+
1, 1, 1, 1, 1, 1, 1,
70+
]);
71+
72+
const result = calculateStreak(calendar);
73+
74+
expect(result.currentStreak).toBe(7);
75+
expect(result.longestStreak).toBe(7);
76+
expect(result.totalContributions).toBe(141);
77+
});
78+
6179
it('handles multiple weeks of zero contributions separating active streaks', () => {
6280
const calendar = buildCalendar([
63-
// Week 1 - active streak
6481
1, 1, 1, 1, 1, 1, 1,
6582

66-
// Week 2 - gap
6783
0, 0, 0, 0, 0, 0, 0,
6884

69-
// Week 3 - gap
7085
0, 0, 0, 0, 0, 0, 0,
7186

72-
// Week 4 - new streak
7387
1, 1, 1, 1, 1, 1, 1,
7488
]);
7589

@@ -696,17 +710,6 @@ describe('calculateStreak', () => {
696710
});
697711
});
698712

699-
it('handles massive single-day commit spike timeline', () => {
700-
const calendar = buildCalendar([
701-
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 120, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1,
702-
]);
703-
704-
const result = calculateStreak(calendar);
705-
706-
expect(result.currentStreak).toBe(7);
707-
expect(result.longestStreak).toBe(7);
708-
});
709-
710713
describe('calculateStreak — timezone awareness', () => {
711714
const tzCalendar = {
712715
totalContributions: 3,
@@ -786,41 +789,27 @@ describe('calculateStreak — timezone awareness', () => {
786789
const result = calculateStreak(tzCalendar, 'UTC', nowUTC);
787790
expect(result.todayDate).toBe('2024-06-16');
788791
});
792+
});
789793

790-
it('calculates streak correctly during a spring-forward DST transition edge case', () => {
791-
// 1. We must mock the system clock so 'new Date()' behaves predictably
792-
vi.useFakeTimers();
793-
794-
// 2. Use America/New_York (spring-forward: 2024-03-10)
795-
process.env.TZ = 'America/New_York';
796-
797-
// 3. Set `now` to early UTC on March 10.
798-
// 03:00:00 UTC on March 10 is 22:00:00 (10:00 PM) on March 9 in New York (EST).
799-
const mockNow = new Date('2024-03-10T03:00:00.000Z');
800-
vi.setSystemTime(mockNow);
794+
describe('isStreakAlive', () => {
795+
it('returns true when both today and yesterday have contributions', () => {
796+
expect(isStreakAlive({ contributionCount: 1 }, { contributionCount: 1 })).toBe(true);
797+
});
801798

802-
// 4. Build a calendar with contributions on March 9 and March 10
803-
const dstCalendar = {
804-
totalContributions: 2,
805-
weeks: [
806-
{
807-
contributionDays: [
808-
{ contributionCount: 1, date: '2024-03-09' },
809-
{ contributionCount: 1, date: '2024-03-10' },
810-
],
811-
},
812-
],
813-
} as Parameters<typeof calculateStreak>[0];
799+
it('returns true when only today has contributions', () => {
800+
expect(isStreakAlive({ contributionCount: 1 }, { contributionCount: 0 })).toBe(true);
801+
});
814802

815-
// 5. Assert currentStreak is calculated correctly
816-
const result = calculateStreak(dstCalendar, 'America/New_York');
803+
it('returns true when only yesterday has contributions', () => {
804+
expect(isStreakAlive({ contributionCount: 0 }, { contributionCount: 1 })).toBe(true);
805+
});
817806

818-
// Because it is currently March 9th in New York, the current streak should securely be 1
819-
expect(result.currentStreak).toBe(1);
807+
it('returns false when both today and yesterday have zero contributions', () => {
808+
expect(isStreakAlive({ contributionCount: 0 }, { contributionCount: 0 })).toBe(false);
809+
});
820810

821-
// 6. Cleanup to prevent breaking other tests
822-
vi.useRealTimers();
823-
process.env.TZ = '';
811+
it('returns false when yesterday is null and today has no contributions', () => {
812+
expect(isStreakAlive({ contributionCount: 0 }, null)).toBe(false);
824813
});
825814
});
826815

@@ -942,9 +931,9 @@ describe('calculateMonthlyStats', () => {
942931
expect(result.previousMonthTotal).toBe(10);
943932
expect(result.currentMonthName).toBe('January');
944933
});
945-
// =========================================================================
934+
// ==================================================================
946935
// ISSUE OBJECTIVE: Empty calendar passed to calculateMonthlyStats
947-
// =========================================================================
936+
// ==================================================================
948937
it('returns zeros and does not crash when given an empty calendar', () => {
949938
const emptyCalendar = {
950939
totalContributions: 0,
@@ -995,9 +984,9 @@ describe('calculateStreak — empty and sparse year edge cases', () => {
995984
expect(result.totalContributions).toBe(2);
996985
});
997986

998-
// =========================================================================
987+
// ==================================================================
999988
// ISSUE #1503 — Variation 4: Full year (52 weeks × 7 days) of 0 contributions
1000-
// =========================================================================
989+
// ==================================================================
1001990
// Background: streak computation is susceptible to off-by-one errors when
1002991
// managing calendar offsets and date boundaries. A full year of zero commits
1003992
// is the most exhaustive boundary stress-test: the loop must traverse all 364
@@ -1201,7 +1190,7 @@ describe('calculateWrappedStats', () => {
12011190
});
12021191

12031192
// ISSUE OBJECTIVE: Verify weekendRatio is 100 when all commits are on weekends
1204-
// =========================================================================
1193+
// ==================================================================
12051194
it('returns weekendRatio === 100 when all contributions are on weekends', () => {
12061195
// Note: 2026-05-02 is a Saturday, 2026-05-03 is a Sunday, 2026-05-04 is a Monday
12071196
const weekendCalendar = {
@@ -1259,35 +1248,3 @@ describe('calculateWrappedStats', () => {
12591248
expect(resultUTCPlus5.todayDate).toBe('2024-01-15');
12601249
});
12611250
});
1262-
1263-
describe('findTodayIndex', () => {
1264-
it('returns index when date is found', () => {
1265-
const days = [
1266-
{ date: '2024-01-01', contributionCount: 1 },
1267-
{ date: '2024-01-02', contributionCount: 2 },
1268-
{ date: '2024-01-03', contributionCount: 3 },
1269-
];
1270-
1271-
const result = findTodayIndex(days, 'UTC', new Date('2024-01-02T12:00:00Z'));
1272-
1273-
expect(result).toBe(1);
1274-
});
1275-
1276-
it('falls back to last index when date is not found', () => {
1277-
const days = [
1278-
{ date: '2024-01-01', contributionCount: 1 },
1279-
{ date: '2024-01-02', contributionCount: 2 },
1280-
{ date: '2024-01-03', contributionCount: 3 },
1281-
];
1282-
1283-
const result = findTodayIndex(days, 'UTC', new Date('2024-01-10T12:00:00Z'));
1284-
1285-
expect(result).toBe(2);
1286-
});
1287-
1288-
it('returns -1 for empty days array', () => {
1289-
const result = findTodayIndex([], 'UTC', new Date('2024-01-10T12:00:00Z'));
1290-
1291-
expect(result).toBe(-1);
1292-
});
1293-
});

0 commit comments

Comments
 (0)