Skip to content

Commit

Permalink
fix(streak-count): fix streak count calculation logic.
Browse files Browse the repository at this point in the history
- Count and todayCount will update at every time fetching user profile.
- Updates with task completion toggled.
- Update Home.test.jsx to fully mock the UserContext to improve speed.
  • Loading branch information
ZL-Asica committed Nov 8, 2024
1 parent 84ad619 commit 8c80a93
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 79 deletions.
10 changes: 9 additions & 1 deletion src/contexts/UserContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,17 @@ import { createContext, useContext, useEffect, useState } from 'react'
* @typedef {Object<string, Goal>} GoalMap - A map of goal IDs to Goal objects.
*/

/**
* @typedef {Object} CompletedDays
* @property {string} date - Date in YYYY-MM-DD format.
* @property {number} count - Number of completions for the date.
*/

/**
* @typedef {Object} Streak
* @property {Object.<string, number>} completedDays - Map of dates (as strings) to their completion counts.
* @property {CompletedDays} completedDays - Map of dates (as strings) to their completion counts.
* @property {number} count - Current streak count.
* @property {number} todayCount - Today's completion count.
*/

/**
Expand Down
56 changes: 31 additions & 25 deletions src/pages/Home.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,40 @@ import Home from '@/pages/Home'
import { act, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, test, vi } from 'vitest'

// Mock `@contexts/UserContext` to control user profile and updates
vi.mock('@/contexts/UserContext', async () => {
const actual = await vi.importActual('@/contexts/UserContext')
let mockUserProfile = {
uid: '123',
profilePic: '',
name: 'Test User',
goals: [],
streak: [],
}
// Define mock data and functions directly
let mockUserProfile = {
uid: '123',
profilePic: '',
name: 'Test User',
goals: [],
streak: { completedDays: {}, count: 0 },
}

return {
...actual,
useUser: () => ({
user: mockUserProfile,
loading: false,
updateProfile: vi.fn((updates) => {
mockUserProfile = { ...mockUserProfile, ...updates }
}),
}),
}
const mockUpdateProfile = vi.fn((updates) => {
mockUserProfile = { ...mockUserProfile, ...updates }
})

// Mock `@contexts/UserContext` to control user profile and updates
vi.mock('@/contexts/UserContext', () => ({
UserProvider: ({ children }) => <>{children}</>,
useUser: () => ({
user: mockUserProfile,
loading: false,
updateProfile: mockUpdateProfile,
}),
}))

describe('Home Screen - No Goals', () => {
beforeEach(() => {
// Reset mockUserProfile for each test
vi.clearAllMocks()
// Reset mockUserProfile to its initial state before each test
mockUserProfile = {
uid: '123',
profilePic: '',
name: 'Test User',
goals: [],
streak: { completedDays: {}, count: 0 },
}
})

test('Displays only "New Goal" field when there are no goals', async () => {
Expand All @@ -42,11 +49,10 @@ describe('Home Screen - No Goals', () => {
})

// Check if "New Goal" text field is present
const newGoalInput = screen.getByLabelText('New Goal')
expect(newGoalInput).not.to.be.null
expect(screen.getByLabelText('New Goal')).toBeTruthy()

// Ensure "New Microgoal" and "New Task" fields are not present initially
expect(screen.queryByLabelText('New Microgoal')).to.be.null
expect(screen.queryByLabelText('New Task')).to.be.null
expect(screen.queryByLabelText('New Microgoal')).toBeNull()
expect(screen.queryByLabelText('New Task')).toBeNull()
})
})
8 changes: 6 additions & 2 deletions src/pages/Streak.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @ts-check

import { useUser } from '@/contexts/UserContext'
import '@/styles/StreakPage.css'
import { calculateStreakCount } from '@/utils/streakUtils'
import { getChicagoDate } from '@/utils/streakUtils'
import FireIcon from '@mui/icons-material/Whatshot'
import { Box, Typography } from '@mui/material'
import { useMemo } from 'react'
Expand All @@ -9,8 +11,10 @@ import 'react-calendar/dist/Calendar.css'

const Streak = () => {
const { user } = useUser()

const streakCount = user.streak?.count || 0
const completedDays = user.streak?.completedDays || {}
const { today, streakCount } = calculateStreakCount(completedDays)
const today = getChicagoDate()

// Cache the completed dates
const completedDatesSet = useMemo(
Expand Down
21 changes: 11 additions & 10 deletions src/utils/firebase/createUserProfile.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @ts-check

import { db } from '@/utils/firebaseConfig'
import { calculateStreakCount } from '@/utils/streakUtils'
import { doc, getDoc, setDoc, updateDoc } from 'firebase/firestore'

/**
Expand All @@ -18,7 +19,16 @@ export const fetchUserProfile = async (uid) => {
return null
}

return userSnapshot.data()
const profile = userSnapshot.data()

const { count, todayCount } = calculateStreakCount(
profile.streak.completedDays
)

return {
...profile,
streak: { ...profile.streak, count, todayCount },
}
} catch (error) {
console.error('Error fetching user profile:', error)
return null
Expand Down Expand Up @@ -59,15 +69,6 @@ export const createFirstUserProfile = async (user) => {
}
}

/**
* Retrieves the user profile from Firestore by UID.
* @param {string} uid - User's UID.
* @returns {Promise<object|null>} - The user profile data or null if not found.
*/
export const getUserProfile = async (uid) => {
return await fetchUserProfile(uid)
}

/**
* Updates the user profile data in Firestore by UID.
* @param {string} uid - User's UID.
Expand Down
73 changes: 32 additions & 41 deletions src/utils/streakUtils.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
// @ts-check

import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'

dayjs.extend(timezone)
dayjs.extend(utc)

/**
* Gets the current date in Chicago timezone (formatted as YYYY-MM-DD).
* @returns {string} The current Chicago date as a string in YYYY-MM-DD format
*/
export const getChicagoDate = () => {
const chicagoTimeOffset = -6 * 60 // CST is UTC-6
const chicagoDate = new Date(
new Date().getTime() + chicagoTimeOffset * 60 * 1000
)
return chicagoDate.toISOString().split('T')[0]
return dayjs().tz('America/Chicago').format('YYYY-MM-DD')
}

/**
* Completed days map with date-count pairs.
* @typedef {Object} completedDays
* @property {string} date - Date in YYYY-MM-DD format.
* @property {number} count - Number of completions for the date.
*/

/**
* Updates the completed days for a user based on the current date.
* @param {import('@/contexts/UserContext').Streak} streak - The user's streak object.
Expand All @@ -28,45 +23,41 @@ export const getChicagoDate = () => {
*/
export const updateStreakDays = (streak, countChange) => {
const currentDate = getChicagoDate()
const completedDays = streak.completedDays || {}

// Get the current count for the current date or initialize it to 0
const currentCount = completedDays[currentDate] || 0
const completedDays = { ...streak.completedDays }

// Update the count for the current date
streak.completedDays = {
...completedDays,
[currentDate]: Math.max(0, currentCount + countChange),
}
completedDays[currentDate] = Math.max(
0,
(completedDays[currentDate] || 0) + countChange
)

const { count, todayCount } = calculateStreakCount(completedDays)

return streak
return {
...streak,
completedDays,
count,
todayCount,
}
}

/**
* Calculates the current streak count based on completedDays.
* @param {completedDays} completedDays - The map of date strings to completion counts.
* @returns {{ today: string, streakCount: number }} - The current date and streak count.
* @param {import ('@/contexts/UserContext').CompletedDays} completedDays - The user's completed days.
* @returns {{ count: number, todayCount: number }} - The current date and streak count.
*/
export const calculateStreakCount = (completedDays) => {
const today = getChicagoDate()
const yesterday = dayjs(today).subtract(1, 'day').format('YYYY-MM-DD')

// Sort dates in descending order, so we can check from yesterday backward
const dates = Object.keys(completedDays).sort(
(a, b) => new Date(b).getTime() - new Date(a).getTime()
)

let streakCount = 0

for (const date of dates) {
if (date === today) continue // Skip today, only count until yesterday

if (completedDays[date] > 0 && date <= yesterday) {
streakCount++
} else {
break // Stop counting if a non-completed day is found
}
const todayCount = completedDays[today] || 0
// If the user completed a task today, the streak is at least 1
let count = todayCount > 0 ? 1 : 0

// Start from yesterday to avoid counting today twice
let currentDate = dayjs(today).subtract(1, 'day').format('YYYY-MM-DD')
while (completedDays[currentDate] > 0) {
count++
currentDate = dayjs(currentDate).subtract(1, 'day').format('YYYY-MM-DD')
}

return { today, streakCount }
return { count, todayCount }
}

0 comments on commit 8c80a93

Please sign in to comment.