init
This commit is contained in:
285
calendar/calendarUtil.test.js
Normal file
285
calendar/calendarUtil.test.js
Normal file
@@ -0,0 +1,285 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import calendarUtil from './calendarUtil.js'
|
||||
|
||||
// ─── rangesOverlap ────────────────────────────────────────────────────────────
|
||||
describe('rangesOverlap', () => {
|
||||
const d = (h) => new Date(`2024-01-01T${String(h).padStart(2,'0')}:00:00`)
|
||||
|
||||
it('overlapping ranges', () => {
|
||||
expect(calendarUtil.rangesOverlap(d(1), d(5), d(3), d(7))).toBe(true)
|
||||
})
|
||||
it('adjacent ranges (touching) do not overlap', () => {
|
||||
expect(calendarUtil.rangesOverlap(d(1), d(3), d(3), d(5))).toBe(false)
|
||||
})
|
||||
it('non-overlapping ranges', () => {
|
||||
expect(calendarUtil.rangesOverlap(d(1), d(2), d(5), d(7))).toBe(false)
|
||||
})
|
||||
it('one range contained within the other', () => {
|
||||
expect(calendarUtil.rangesOverlap(d(1), d(10), d(3), d(7))).toBe(true)
|
||||
})
|
||||
it('identical ranges overlap', () => {
|
||||
expect(calendarUtil.rangesOverlap(d(1), d(5), d(1), d(5))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── startOfWeek ─────────────────────────────────────────────────────────────
|
||||
describe('startOfWeek', () => {
|
||||
it('Sunday start — Wednesday lands on previous Sunday', () => {
|
||||
const wed = new Date(2024, 0, 3) // Wednesday Jan 3 (local)
|
||||
const result = calendarUtil.startOfWeek(wed, 0)
|
||||
expect(result.getDay()).toBe(0)
|
||||
expect(result.getDate()).toBe(31) // Dec 31
|
||||
})
|
||||
it('Monday start — Wednesday lands on previous Monday', () => {
|
||||
const wed = new Date(2024, 0, 3) // Wednesday Jan 3 (local)
|
||||
const result = calendarUtil.startOfWeek(wed, 1)
|
||||
expect(result.getDay()).toBe(1)
|
||||
expect(result.getDate()).toBe(1) // Jan 1
|
||||
})
|
||||
it('returns start of same day when date is already weekStartsOn', () => {
|
||||
const sun = new Date(2024, 0, 7) // Sunday Jan 7 (local)
|
||||
const result = calendarUtil.startOfWeek(sun, 0)
|
||||
expect(result.toDateString()).toBe(sun.toDateString())
|
||||
})
|
||||
it('zeros out the time', () => {
|
||||
const d = new Date(2024, 0, 3, 15, 30) // Jan 3 15:30 local
|
||||
const result = calendarUtil.startOfWeek(d)
|
||||
expect(result.getHours()).toBe(0)
|
||||
expect(result.getMinutes()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── buildMonthGrid ───────────────────────────────────────────────────────────
|
||||
describe('buildMonthGrid', () => {
|
||||
it('always returns 42 dates', () => {
|
||||
expect(calendarUtil.buildMonthGrid(new Date(2024, 0, 1))).toHaveLength(42)
|
||||
expect(calendarUtil.buildMonthGrid(new Date(2024, 1, 1))).toHaveLength(42)
|
||||
})
|
||||
it('grid starts on a Sunday when weekStartsOn=0', () => {
|
||||
const grid = calendarUtil.buildMonthGrid(new Date(2024, 0, 1), 0)
|
||||
expect(grid[0].getDay()).toBe(0)
|
||||
})
|
||||
it('grid starts on a Monday when weekStartsOn=1', () => {
|
||||
const grid = calendarUtil.buildMonthGrid(new Date(2024, 0, 1), 1)
|
||||
expect(grid[0].getDay()).toBe(1)
|
||||
})
|
||||
it('grid includes all days of the month', () => {
|
||||
const grid = calendarUtil.buildMonthGrid(new Date(2024, 2, 1)) // March 2024
|
||||
const marchDays = grid.filter(d => d.getMonth() === 2)
|
||||
expect(marchDays).toHaveLength(31)
|
||||
})
|
||||
it('consecutive dates are exactly 1 day apart', () => {
|
||||
const grid = calendarUtil.buildMonthGrid(new Date(2024, 0, 1))
|
||||
for (let i = 1; i < grid.length; i++) {
|
||||
const diff = grid[i] - grid[i - 1]
|
||||
expect(diff).toBe(86400000)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ─── getWeekNumber ────────────────────────────────────────────────────────────
|
||||
describe('getWeekNumber', () => {
|
||||
it('Jan 1 2024 is week 1', () => {
|
||||
expect(calendarUtil.getWeekNumber(new Date(2024, 0, 1))).toBe(1)
|
||||
})
|
||||
it('Dec 31 2023 is week 52', () => {
|
||||
expect(calendarUtil.getWeekNumber(new Date(2023, 11, 31))).toBe(52)
|
||||
})
|
||||
it('Jan 4 2021 is week 1 (ISO rule: first week has Thursday)', () => {
|
||||
expect(calendarUtil.getWeekNumber(new Date(2021, 0, 4))).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── timedEnd ─────────────────────────────────────────────────────────────────
|
||||
describe('timedEnd', () => {
|
||||
const t = (iso) => new Date(iso)
|
||||
|
||||
it('returns time_end when event has positive duration', () => {
|
||||
const event = { time_start: t('2024-01-01T10:00'), time_end: t('2024-01-01T11:00') }
|
||||
expect(calendarUtil.timedEnd(event)).toEqual(event.time_end)
|
||||
})
|
||||
it('returns start+1ms for zero-duration event', () => {
|
||||
const start = t('2024-01-01T10:00')
|
||||
const event = { time_start: start, time_end: start }
|
||||
expect(calendarUtil.timedEnd(event).getTime()).toBe(start.getTime() + 1)
|
||||
})
|
||||
it('returns start+1ms when end < start', () => {
|
||||
const event = { time_start: t('2024-01-01T10:00'), time_end: t('2024-01-01T09:00') }
|
||||
expect(calendarUtil.timedEnd(event).getTime()).toBe(event.time_start.getTime() + 1)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── toISO ───────────────────────────────────────────────────────────────────
|
||||
describe('toISO', () => {
|
||||
it('non-all-day: parses string directly', () => {
|
||||
const str = '2024-06-15T14:30:00'
|
||||
expect(new Date(calendarUtil.toISO(str, false)).getFullYear()).toBe(2024)
|
||||
})
|
||||
it('all-day start: midnight', () => {
|
||||
const iso = calendarUtil.toISO('2024-06-15', true, false)
|
||||
expect(new Date(iso).getHours()).toBe(0)
|
||||
expect(new Date(iso).getMinutes()).toBe(0)
|
||||
})
|
||||
it('all-day end: 23:59:59.999', () => {
|
||||
const iso = calendarUtil.toISO('2024-06-15', true, true)
|
||||
const d = new Date(iso)
|
||||
expect(d.getHours()).toBe(23)
|
||||
expect(d.getMinutes()).toBe(59)
|
||||
expect(d.getSeconds()).toBe(59)
|
||||
expect(d.getMilliseconds()).toBe(999)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── generateOccurrenceDates ──────────────────────────────────────────────────
|
||||
describe('generateOccurrenceDates', () => {
|
||||
const range = [new Date('2024-01-01'), new Date('2024-12-31')]
|
||||
|
||||
const makeTemplate = (overrides) => ({
|
||||
time_start: new Date('2024-01-01T10:00'),
|
||||
time_end: new Date('2024-01-01T11:00'),
|
||||
recurrence: { frequency: 'daily', interval: 1 },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
it('daily — correct count for January', () => {
|
||||
const tmpl = makeTemplate({ recurrence: { frequency: 'daily', interval: 1 } })
|
||||
const rangeEnd = new Date('2024-01-31T23:59:59')
|
||||
const dates = calendarUtil.generateOccurrenceDates(tmpl, range[0], rangeEnd)
|
||||
expect(dates).toHaveLength(31)
|
||||
})
|
||||
|
||||
it('daily with interval=2 — every other day', () => {
|
||||
const tmpl = makeTemplate({ recurrence: { frequency: 'daily', interval: 2 } })
|
||||
const rangeEnd = new Date('2024-01-10')
|
||||
const dates = calendarUtil.generateOccurrenceDates(tmpl, range[0], rangeEnd)
|
||||
// Jan 1, 3, 5, 7, 9 = 5
|
||||
expect(dates).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('daily with count limit', () => {
|
||||
const tmpl = makeTemplate({ recurrence: { frequency: 'daily', interval: 1, count: 5 } })
|
||||
const dates = calendarUtil.generateOccurrenceDates(tmpl, range[0], range[1])
|
||||
expect(dates).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('daily with end_date stops before end_date', () => {
|
||||
const tmpl = makeTemplate({ recurrence: { frequency: 'daily', interval: 1, end_date: '2024-01-06' } })
|
||||
const dates = calendarUtil.generateOccurrenceDates(tmpl, range[0], range[1])
|
||||
// Jan 1-5 (end_date exclusive)
|
||||
expect(dates).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('weekly — every Monday for 4 weeks', () => {
|
||||
const tmpl = makeTemplate({
|
||||
time_start: new Date('2024-01-01T10:00'), // Monday
|
||||
recurrence: { frequency: 'weekly', interval: 1, count: 4 },
|
||||
})
|
||||
const dates = calendarUtil.generateOccurrenceDates(tmpl, range[0], range[1])
|
||||
expect(dates).toHaveLength(4)
|
||||
dates.forEach(d => expect(d.getDay()).toBe(1)) // all Mondays
|
||||
})
|
||||
|
||||
it('weekly with days_of_week — Mon+Wed+Fri', () => {
|
||||
const tmpl = makeTemplate({
|
||||
time_start: new Date('2024-01-01T10:00'), // Monday
|
||||
recurrence: { frequency: 'weekly', interval: 1, days_of_week: [1, 3, 5], count: 6 },
|
||||
})
|
||||
const dates = calendarUtil.generateOccurrenceDates(tmpl, range[0], range[1])
|
||||
expect(dates).toHaveLength(6)
|
||||
dates.forEach(d => expect([1, 3, 5]).toContain(d.getDay()))
|
||||
})
|
||||
|
||||
it('monthly — same day each month for 3 months', () => {
|
||||
const tmpl = makeTemplate({ recurrence: { frequency: 'monthly', interval: 1, count: 3 } })
|
||||
const dates = calendarUtil.generateOccurrenceDates(tmpl, range[0], range[1])
|
||||
expect(dates).toHaveLength(3)
|
||||
dates.forEach(d => expect(d.getDate()).toBe(1))
|
||||
})
|
||||
|
||||
it('yearly — same day each year', () => {
|
||||
const tmpl = makeTemplate({ recurrence: { frequency: 'yearly', interval: 1, count: 3 } })
|
||||
const rangeEnd = new Date('2026-12-31')
|
||||
const dates = calendarUtil.generateOccurrenceDates(tmpl, range[0], rangeEnd)
|
||||
expect(dates).toHaveLength(3)
|
||||
expect(dates[0].getFullYear()).toBe(2024)
|
||||
expect(dates[1].getFullYear()).toBe(2025)
|
||||
expect(dates[2].getFullYear()).toBe(2026)
|
||||
})
|
||||
|
||||
it('no recurrence rule returns empty array', () => {
|
||||
const tmpl = { time_start: new Date(), time_end: new Date(), recurrence: null }
|
||||
expect(calendarUtil.generateOccurrenceDates(tmpl, range[0], range[1])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
// ─── expandRecurringEvents ───────────────────────────────────────────────────
|
||||
describe('expandRecurringEvents', () => {
|
||||
const rangeStart = new Date('2024-01-01')
|
||||
const rangeEnd = new Date('2024-01-31')
|
||||
|
||||
const baseTemplate = {
|
||||
id: 1,
|
||||
title: 'Standup',
|
||||
recurrence_id: 'abc',
|
||||
recurrence_parent_id: null,
|
||||
time_start: new Date(2024, 0, 1, 9, 0),
|
||||
time_end: new Date(2024, 0, 1, 9, 30),
|
||||
recurrence: { frequency: 'daily', interval: 1, count: 5 },
|
||||
all_day: false,
|
||||
}
|
||||
|
||||
it('non-recurring events pass through unchanged', () => {
|
||||
const regular = { id: 99, title: 'One-off', recurrence_id: null, recurrence_parent_id: null }
|
||||
const result = calendarUtil.expandRecurringEvents([regular], rangeStart, rangeEnd)
|
||||
expect(result).toContain(regular)
|
||||
})
|
||||
|
||||
it('template expands into correct number of occurrences', () => {
|
||||
const result = calendarUtil.expandRecurringEvents([baseTemplate], rangeStart, rangeEnd)
|
||||
expect(result).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('occurrences have _isOccurrence flag', () => {
|
||||
const result = calendarUtil.expandRecurringEvents([baseTemplate], rangeStart, rangeEnd)
|
||||
result.forEach(e => expect(e._isOccurrence).toBe(true))
|
||||
})
|
||||
|
||||
it('override replaces its occurrence', () => {
|
||||
const override = {
|
||||
id: 2,
|
||||
title: 'Standup (rescheduled)',
|
||||
recurrence_id: null,
|
||||
recurrence_parent_id: 1,
|
||||
recurrence_exception_date: new Date(2024, 0, 2, 9, 0),
|
||||
time_start: new Date(2024, 0, 2, 10, 0),
|
||||
time_end: new Date(2024, 0, 2, 10, 30),
|
||||
is_cancelled: false,
|
||||
}
|
||||
const result = calendarUtil.expandRecurringEvents([baseTemplate, override], rangeStart, rangeEnd)
|
||||
const jan2 = result.filter(e => {
|
||||
const d = new Date(e.time_start)
|
||||
return d.getMonth() === 0 && d.getDate() === 2
|
||||
})
|
||||
expect(jan2).toHaveLength(1)
|
||||
expect(jan2[0].title).toBe('Standup (rescheduled)')
|
||||
})
|
||||
|
||||
it('cancelled override removes the occurrence', () => {
|
||||
const cancelled = {
|
||||
id: 3,
|
||||
recurrence_id: null,
|
||||
recurrence_parent_id: 1,
|
||||
recurrence_exception_date: new Date(2024, 0, 2, 9, 0),
|
||||
time_start: new Date(2024, 0, 2, 9, 0),
|
||||
time_end: new Date(2024, 0, 2, 9, 30),
|
||||
is_cancelled: true,
|
||||
}
|
||||
const result = calendarUtil.expandRecurringEvents([baseTemplate, cancelled], rangeStart, rangeEnd)
|
||||
const jan2 = result.filter(e => {
|
||||
const d = new Date(e.time_start)
|
||||
return d.getMonth() === 0 && d.getDate() === 2
|
||||
})
|
||||
expect(jan2).toHaveLength(0)
|
||||
expect(result).toHaveLength(4) // 5 minus the cancelled one
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user