Add TimeSelector and ScheduleModal components with tests
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m45s

- Implemented TimeSelector component for selecting time with AM/PM toggle and minute/hour increment/decrement functionality.
- Created ScheduleModal component for scheduling chores with options for specific days or intervals.
- Added utility functions for scheduling logic in scheduleUtils.ts.
- Developed comprehensive tests for TimeSelector and scheduleUtils functions to ensure correct behavior.
This commit is contained in:
2026-02-23 15:44:55 -05:00
parent d8822b44be
commit 234adbe05f
26 changed files with 2880 additions and 60 deletions

View File

@@ -0,0 +1,254 @@
import { describe, it, expect } from 'vitest'
import {
intervalHitsToday,
isScheduledToday,
getDueTimeToday,
isPastTime,
isExtendedToday,
formatDueTimeLabel,
msUntilExpiry,
toLocalISODate,
} from '../common/scheduleUtils'
import type { ChoreSchedule } from '../common/models'
// ---------------------------------------------------------------------------
// toLocalISODate
// ---------------------------------------------------------------------------
describe('toLocalISODate', () => {
it('formats a date as YYYY-MM-DD', () => {
const d = new Date(2025, 0, 5) // Jan 5, 2025 local
expect(toLocalISODate(d)).toBe('2025-01-05')
})
it('pads month and day', () => {
const d = new Date(2025, 2, 9) // Mar 9
expect(toLocalISODate(d)).toBe('2025-03-09')
})
})
// ---------------------------------------------------------------------------
// intervalHitsToday
// ---------------------------------------------------------------------------
describe('intervalHitsToday', () => {
it('anchor day hits on itself', () => {
// Wednesday = weekday 3
const wednesday = new Date(2025, 0, 8) // Jan 8, 2025 is a Wednesday
expect(intervalHitsToday(3, 2, wednesday)).toBe(true)
})
it('anchor=Wednesday, interval=2, Thursday does NOT hit', () => {
const thursday = new Date(2025, 0, 9)
expect(intervalHitsToday(3, 2, thursday)).toBe(false)
})
it('anchor=Wednesday, interval=2, Friday hits (2 days after anchor)', () => {
const friday = new Date(2025, 0, 10)
expect(intervalHitsToday(3, 2, friday)).toBe(true)
})
it('interval=1 hits every day', () => {
// interval of 1 means every day; diffDays % 1 === 0 always
// Note: interval_days=1 is disallowed in UI (min 2) but logic should still work
const monday = new Date(2025, 0, 6)
expect(intervalHitsToday(1, 1, monday)).toBe(true)
})
})
// ---------------------------------------------------------------------------
// isScheduledToday
// ---------------------------------------------------------------------------
describe('isScheduledToday', () => {
const daysSchedule: ChoreSchedule = {
child_id: 'c1',
task_id: 't1',
mode: 'days',
day_configs: [
{ day: 1, hour: 8, minute: 0 }, // Monday
{ day: 3, hour: 9, minute: 30 }, // Wednesday
],
interval_days: 2,
anchor_weekday: 0,
interval_hour: 0,
interval_minute: 0,
}
it('returns true on a scheduled weekday', () => {
const monday = new Date(2025, 0, 6) // Jan 6, 2025 = Monday
expect(isScheduledToday(daysSchedule, monday)).toBe(true)
})
it('returns false on an unscheduled weekday', () => {
const tuesday = new Date(2025, 0, 7) // Tuesday
expect(isScheduledToday(daysSchedule, tuesday)).toBe(false)
})
it('interval mode delegates to intervalHitsToday', () => {
const intervalSchedule: ChoreSchedule = {
child_id: 'c1',
task_id: 't1',
mode: 'interval',
day_configs: [],
interval_days: 2,
anchor_weekday: 3, // Wednesday
interval_hour: 8,
interval_minute: 0,
}
const wednesday = new Date(2025, 0, 8)
expect(isScheduledToday(intervalSchedule, wednesday)).toBe(true)
const thursday = new Date(2025, 0, 9)
expect(isScheduledToday(intervalSchedule, thursday)).toBe(false)
})
})
// ---------------------------------------------------------------------------
// getDueTimeToday
// ---------------------------------------------------------------------------
describe('getDueTimeToday', () => {
const daysSchedule: ChoreSchedule = {
child_id: 'c1',
task_id: 't1',
mode: 'days',
day_configs: [{ day: 1, hour: 8, minute: 30 }],
interval_days: 2,
anchor_weekday: 0,
interval_hour: 0,
interval_minute: 0,
}
it('returns correct time for a scheduled day', () => {
const monday = new Date(2025, 0, 6)
expect(getDueTimeToday(daysSchedule, monday)).toEqual({ hour: 8, minute: 30 })
})
it('returns null for an unscheduled day', () => {
const tuesday = new Date(2025, 0, 7)
expect(getDueTimeToday(daysSchedule, tuesday)).toBeNull()
})
it('interval mode returns interval time on hit days', () => {
const intervalSchedule: ChoreSchedule = {
child_id: 'c1',
task_id: 't1',
mode: 'interval',
day_configs: [],
interval_days: 2,
anchor_weekday: 3,
interval_hour: 14,
interval_minute: 45,
}
const wednesday = new Date(2025, 0, 8)
expect(getDueTimeToday(intervalSchedule, wednesday)).toEqual({ hour: 14, minute: 45 })
})
it('interval mode returns null on non-hit days', () => {
const intervalSchedule: ChoreSchedule = {
child_id: 'c1',
task_id: 't1',
mode: 'interval',
day_configs: [],
interval_days: 2,
anchor_weekday: 3,
interval_hour: 14,
interval_minute: 45,
}
const thursday = new Date(2025, 0, 9)
expect(getDueTimeToday(intervalSchedule, thursday)).toBeNull()
})
})
// ---------------------------------------------------------------------------
// isPastTime
// ---------------------------------------------------------------------------
describe('isPastTime', () => {
it('returns true when current time equals due time', () => {
const now = new Date(2025, 0, 6, 8, 30, 0)
expect(isPastTime(8, 30, now)).toBe(true)
})
it('returns true when current time is past due time', () => {
const now = new Date(2025, 0, 6, 9, 0, 0)
expect(isPastTime(8, 30, now)).toBe(true)
})
it('returns false when current time is before due time', () => {
const now = new Date(2025, 0, 6, 8, 0, 0)
expect(isPastTime(8, 30, now)).toBe(false)
})
})
// ---------------------------------------------------------------------------
// isExtendedToday
// ---------------------------------------------------------------------------
describe('isExtendedToday', () => {
it('returns true when extension date matches today', () => {
const today = new Date(2025, 0, 15)
expect(isExtendedToday('2025-01-15', today)).toBe(true)
})
it('returns false when extension date does not match today', () => {
const today = new Date(2025, 0, 15)
expect(isExtendedToday('2025-01-14', today)).toBe(false)
})
it('returns false for null extension date', () => {
const today = new Date(2025, 0, 15)
expect(isExtendedToday(null, today)).toBe(false)
})
it('returns false for undefined extension date', () => {
const today = new Date(2025, 0, 15)
expect(isExtendedToday(undefined, today)).toBe(false)
})
})
// ---------------------------------------------------------------------------
// formatDueTimeLabel
// ---------------------------------------------------------------------------
describe('formatDueTimeLabel', () => {
it('formats midnight as 12:00 AM', () => {
expect(formatDueTimeLabel(0, 0)).toBe('12:00 AM')
})
it('formats noon as 12:00 PM', () => {
expect(formatDueTimeLabel(12, 0)).toBe('12:00 PM')
})
it('formats 8:30 AM', () => {
expect(formatDueTimeLabel(8, 30)).toBe('8:30 AM')
})
it('formats 14:45 as 2:45 PM', () => {
expect(formatDueTimeLabel(14, 45)).toBe('2:45 PM')
})
it('pads single-digit minutes', () => {
expect(formatDueTimeLabel(9, 5)).toBe('9:05 AM')
})
})
// ---------------------------------------------------------------------------
// msUntilExpiry
// ---------------------------------------------------------------------------
describe('msUntilExpiry', () => {
it('returns positive ms when due time is in the future', () => {
const now = new Date(2025, 0, 6, 8, 0, 0, 0)
const ms = msUntilExpiry(8, 30, now)
expect(ms).toBe(30 * 60 * 1000) // 30 minutes
})
it('returns 0 when due time is in the past', () => {
const now = new Date(2025, 0, 6, 9, 0, 0, 0)
expect(msUntilExpiry(8, 30, now)).toBe(0)
})
it('returns 0 when due time equals current time', () => {
const now = new Date(2025, 0, 6, 8, 30, 0, 0)
expect(msUntilExpiry(8, 30, now)).toBe(0)
})
it('correctly handles seconds and milliseconds offset', () => {
// 8:29:30.500 → due at 8:30:00.000 = 29500ms remaining
const now = new Date(2025, 0, 6, 8, 29, 30, 500)
expect(msUntilExpiry(8, 30, now)).toBe(29500)
})
})