From fa9f5a7ca660013cccdec2b741e3485899d2f4f4 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Thu, 13 Mar 2025 04:31:46 +0100 Subject: [PATCH] fix(client): calculate streaks correctly (#59252) --- .../profile/components/stats.test.tsx | 29 +++++++++++++++---- .../components/profile/components/stats.tsx | 7 +++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/client/src/components/profile/components/stats.test.tsx b/client/src/components/profile/components/stats.test.tsx index 9e37d662c87..3f3065ff2c1 100644 --- a/client/src/components/profile/components/stats.test.tsx +++ b/client/src/components/profile/components/stats.test.tsx @@ -46,11 +46,23 @@ const twoStreakCalendar = { '1736946000': 1 // 2025-01-15 13:00:00 UTC }; +const multipleEntriesInOneDay = { + // Two on Jan 13, 2025 + '1736755200': 1, // 2025-01-13 08:00:00 UTC + '1736755500': 1, // 2025-01-13 08:05:00 UTC + // Two on Jan 14, 2025 + '1736845200': 1, // 2025-01-14 09:00:00 UTC + '1736845500': 1, // 2025-01-14 09:05:00 UTC + // Two on Jan 15, 2025 + '1736946000': 1, // 2025-01-15 13:00:00 UTC + '1736946300': 1 // 2025-01-15 13:05:00 UTC +}; + jest.useFakeTimers(); describe('calculateStreaks', () => { - test('Should return a longest streak of 5 days when the user has not completed a challenge in a while', () => { - jest.setSystemTime(new Date(2025, 0, 15)); + beforeEach(() => jest.setSystemTime(new Date(2025, 0, 15))); + test('Should return 0 for the current streak if the user has not made progress today', () => { const { longestStreak, currentStreak } = calculateStreaks(oldStreakCalendar); @@ -77,10 +89,8 @@ describe('calculateStreaks', () => { }); test('Should return a longest and current streaks of 1 day when the user has recently completed their first challenge', () => { - const now = new Date(2025, 0, 15); - jest.setSystemTime(now); const calendar = { - [now.valueOf() / 1000]: 1 + [Date.now() / 1000]: 1 }; const { longestStreak, currentStreak } = calculateStreaks(calendar); @@ -103,4 +113,13 @@ describe('calculateStreaks', () => { expect(longestStreak).toBe(0); expect(currentStreak).toBe(0); }); + + test('Should handle multiple entries in one day', () => { + const { longestStreak, currentStreak } = calculateStreaks( + multipleEntriesInOneDay + ); + + expect(longestStreak).toBe(3); + expect(currentStreak).toBe(3); + }); }); diff --git a/client/src/components/profile/components/stats.tsx b/client/src/components/profile/components/stats.tsx index 2a46f86f175..817783d6ea2 100644 --- a/client/src/components/profile/components/stats.tsx +++ b/client/src/components/profile/components/stats.tsx @@ -3,7 +3,7 @@ import { startOfDay, addDays, isEqual } from 'date-fns'; import { useTranslation } from 'react-i18next'; import { Spacer } from '@freecodecamp/ui'; import { last } from 'lodash-es'; -import { uniq } from 'lodash'; +import { uniqBy } from 'lodash'; import { FullWidthRow } from '../../helpers'; @@ -19,7 +19,10 @@ export const calculateStreaks = (calendar: Record) => { const timestamps = Object.keys(calendar).map( stamp => Number.parseInt(stamp, 10) * 1000 ); - const days = uniq(timestamps.map(stamp => startOfDay(stamp))); + const days = uniqBy( + timestamps.map(stamp => startOfDay(stamp)), + day => day.getTime() + ); const { longestStreak, currentStreak } = days.reduce( (acc, day) => {