import { describe, expect, it } from 'vitest'; import type { CalendarEvent } from '@/lib/jmap/types'; import { buildWeekSegments, buildAllDayDuration, getEventDayBounds, getEventDisplayEndDate, getEventEndDate, normalizeAllDayDuration, } from '../calendar-utils'; function expectLocalDateParts(date: Date, year: number, month: number, day: number, hour: number, minute = 0, second = 0, millisecond = 0) { expect(date.getFullYear()).toBe(year); expect(date.getMonth()).toBe(month - 1); expect(date.getDate()).toBe(day); expect(date.getHours()).toBe(hour); expect(date.getMinutes()).toBe(minute); expect(date.getSeconds()).toBe(second); expect(date.getMilliseconds()).toBe(millisecond); } function makeEvent(overrides: Partial = {}): CalendarEvent { return { id: 'evt-1', calendarIds: { 'cal-1': true }, isDraft: false, isOrigin: true, utcStart: '2026-03-14T00:00:00Z', utcEnd: '2026-03-17T00:00:00Z', '@type': 'Event', uid: 'uid-1', title: 'Test Event', description: '', descriptionContentType: 'text/plain', created: null, updated: '2026-03-01T09:00:00Z', sequence: 0, start: '2026-03-14T00:00:00', duration: 'P3D', timeZone: 'UTC', showWithoutTime: true, status: 'confirmed', freeBusyStatus: 'busy', privacy: 'public', color: null, keywords: null, categories: null, locale: null, replyTo: null, participants: null, mayInviteSelf: false, mayInviteOthers: false, hideAttendees: false, recurrenceId: null, recurrenceIdTimeZone: null, recurrenceRules: null, recurrenceOverrides: null, excludedRecurrenceRules: null, useDefaultAlerts: false, alerts: null, locations: null, virtualLocations: null, links: null, relatedTo: null, ...overrides, }; } describe('calendar-utils all-day handling', () => { it('treats all-day event end as exclusive for display', () => { const event = makeEvent({ start: '2026-03-14T00:00:00', duration: 'P3D', showWithoutTime: true, }); expectLocalDateParts(getEventEndDate(event), 2026, 3, 17, 0); expectLocalDateParts(getEventDisplayEndDate(event), 2026, 3, 16, 23, 59, 59, 999); const { startDay, endDay } = getEventDayBounds(event); expectLocalDateParts(startDay, 2026, 3, 14, 0); expectLocalDateParts(endDay, 2026, 3, 16, 0); }); it('leaves timed event display end unchanged', () => { const event = makeEvent({ start: '2026-03-14T09:00:00', duration: 'PT2H', showWithoutTime: false, utcStart: '2026-03-14T09:00:00Z', utcEnd: '2026-03-14T11:00:00Z', }); expectLocalDateParts(getEventDisplayEndDate(event), 2026, 3, 14, 11); }); it('normalizes imported all-day durations to day units', () => { expect(normalizeAllDayDuration('PT24H')).toBe('P1D'); expect(normalizeAllDayDuration('PT72H')).toBe('P3D'); expect(normalizeAllDayDuration('P1DT12H')).toBe('P2D'); expect(normalizeAllDayDuration(undefined)).toBeUndefined(); }); it('builds an inclusive all-day duration from editor dates', () => { const start = new Date('2026-03-14T00:00:00Z'); const inclusiveEnd = new Date('2026-03-16T00:00:00Z'); expect(buildAllDayDuration(start, inclusiveEnd)).toBe('P3D'); }); it('builds a single week segment for a five-day event instead of one entry per day', () => { const week = [ new Date('2026-03-16T00:00:00Z'), new Date('2026-03-17T00:00:00Z'), new Date('2026-03-18T00:00:00Z'), new Date('2026-03-19T00:00:00Z'), new Date('2026-03-20T00:00:00Z'), new Date('2026-03-21T00:00:00Z'), new Date('2026-03-22T00:00:00Z'), ]; const event = makeEvent({ start: '2026-03-16T00:00:00', duration: 'P5D', title: 'Full day', showWithoutTime: true, }); const segments = buildWeekSegments([event], week); expect(segments).toHaveLength(1); expect(segments[0]).toMatchObject({ startIndex: 0, span: 5, row: 0, continuesBefore: false, continuesAfter: false, }); }); it('splits a continuing event across weeks without snaking inside a week row', () => { const week = [ new Date('2026-03-16T00:00:00Z'), new Date('2026-03-17T00:00:00Z'), new Date('2026-03-18T00:00:00Z'), new Date('2026-03-19T00:00:00Z'), new Date('2026-03-20T00:00:00Z'), new Date('2026-03-21T00:00:00Z'), new Date('2026-03-22T00:00:00Z'), ]; const event = makeEvent({ start: '2026-03-14T00:00:00', duration: 'P10D', title: 'Long event', showWithoutTime: true, }); const segments = buildWeekSegments([event], week); expect(segments).toHaveLength(1); expect(segments[0]).toMatchObject({ startIndex: 0, span: 7, row: 0, continuesBefore: true, continuesAfter: true, }); }); });