Enhances date validation and timezone handling using DateService, ensuring data integrity and consistency. Refactors event rendering and dragging to correctly handle date transformations. Adds a test plan for event stacking and z-index management. Fixes edge cases in navigation and date calculations for week/year boundaries and DST transitions.
246 lines
9.1 KiB
TypeScript
246 lines
9.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { DateService } from '../../src/utils/DateService';
|
|
|
|
describe('DateService - Midnight Crossing & Multi-Day Events', () => {
|
|
const dateService = new DateService('Europe/Copenhagen');
|
|
|
|
describe('Midnight Crossing Events', () => {
|
|
it('should handle event starting before midnight and ending after', () => {
|
|
const start = new Date(2024, 0, 15, 23, 30); // Jan 15, 23:30
|
|
const end = new Date(2024, 0, 16, 1, 30); // Jan 16, 01:30
|
|
|
|
expect(dateService.isMultiDay(start, end)).toBe(true);
|
|
expect(dateService.isSameDay(start, end)).toBe(false);
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(120); // 2 hours
|
|
});
|
|
|
|
it('should calculate duration correctly across midnight', () => {
|
|
const start = new Date(2024, 0, 15, 22, 0); // 22:00
|
|
const end = new Date(2024, 0, 16, 2, 0); // 02:00 next day
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(240); // 4 hours
|
|
});
|
|
|
|
it('should handle event ending exactly at midnight', () => {
|
|
const start = new Date(2024, 0, 15, 20, 0); // 20:00
|
|
const end = new Date(2024, 0, 16, 0, 0); // 00:00 (midnight)
|
|
|
|
expect(dateService.isMultiDay(start, end)).toBe(true);
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(240); // 4 hours
|
|
});
|
|
|
|
it('should handle event starting exactly at midnight', () => {
|
|
const start = new Date(2024, 0, 15, 0, 0); // 00:00 (midnight)
|
|
const end = new Date(2024, 0, 15, 3, 0); // 03:00 same day
|
|
|
|
expect(dateService.isMultiDay(start, end)).toBe(false);
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(180); // 3 hours
|
|
});
|
|
|
|
it('should create date at specific time correctly across midnight', () => {
|
|
const baseDate = new Date(2024, 0, 15);
|
|
|
|
// 1440 minutes = 24:00 = midnight next day
|
|
const midnightNextDay = dateService.createDateAtTime(baseDate, 1440);
|
|
expect(midnightNextDay.getDate()).toBe(16);
|
|
expect(midnightNextDay.getHours()).toBe(0);
|
|
expect(midnightNextDay.getMinutes()).toBe(0);
|
|
|
|
// 1500 minutes = 25:00 = 01:00 next day
|
|
const oneAmNextDay = dateService.createDateAtTime(baseDate, 1500);
|
|
expect(oneAmNextDay.getDate()).toBe(16);
|
|
expect(oneAmNextDay.getHours()).toBe(1);
|
|
expect(oneAmNextDay.getMinutes()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Multi-Day Events', () => {
|
|
it('should detect 2-day event', () => {
|
|
const start = new Date(2024, 0, 15, 10, 0);
|
|
const end = new Date(2024, 0, 16, 14, 0);
|
|
|
|
expect(dateService.isMultiDay(start, end)).toBe(true);
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(28 * 60); // 28 hours
|
|
});
|
|
|
|
it('should detect 3-day event', () => {
|
|
const start = new Date(2024, 0, 15, 9, 0);
|
|
const end = new Date(2024, 0, 17, 17, 0);
|
|
|
|
expect(dateService.isMultiDay(start, end)).toBe(true);
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(56 * 60); // 56 hours
|
|
});
|
|
|
|
it('should detect week-long event', () => {
|
|
const start = new Date(2024, 0, 15, 0, 0);
|
|
const end = new Date(2024, 0, 22, 0, 0);
|
|
|
|
expect(dateService.isMultiDay(start, end)).toBe(true);
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(7 * 24 * 60); // 7 days
|
|
});
|
|
|
|
it('should handle month-spanning multi-day event', () => {
|
|
const start = new Date(2024, 0, 30, 12, 0); // Jan 30
|
|
const end = new Date(2024, 1, 2, 12, 0); // Feb 2
|
|
|
|
expect(dateService.isMultiDay(start, end)).toBe(true);
|
|
expect(start.getMonth()).toBe(0); // January
|
|
expect(end.getMonth()).toBe(1); // February
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(3 * 24 * 60); // 3 days
|
|
});
|
|
|
|
it('should handle year-spanning multi-day event', () => {
|
|
const start = new Date(2024, 11, 30, 10, 0); // Dec 30, 2024
|
|
const end = new Date(2025, 0, 2, 10, 0); // Jan 2, 2025
|
|
|
|
expect(dateService.isMultiDay(start, end)).toBe(true);
|
|
expect(start.getFullYear()).toBe(2024);
|
|
expect(end.getFullYear()).toBe(2025);
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(3 * 24 * 60); // 3 days
|
|
});
|
|
});
|
|
|
|
describe('Timezone Boundary Events', () => {
|
|
it('should handle UTC to local timezone conversion across midnight', () => {
|
|
// Event in UTC that crosses date boundary in local timezone
|
|
const utcStart = '2024-01-15T23:00:00Z'; // 23:00 UTC
|
|
const utcEnd = '2024-01-16T01:00:00Z'; // 01:00 UTC next day
|
|
|
|
const localStart = dateService.fromUTC(utcStart);
|
|
const localEnd = dateService.fromUTC(utcEnd);
|
|
|
|
// Copenhagen is UTC+1 (or UTC+2 in summer)
|
|
// So 23:00 UTC = 00:00 or 01:00 local (midnight crossing)
|
|
expect(localStart.getDate()).toBeGreaterThanOrEqual(15);
|
|
expect(localEnd.getDate()).toBeGreaterThanOrEqual(16);
|
|
|
|
const duration = dateService.getDurationMinutes(localStart, localEnd);
|
|
expect(duration).toBe(120); // 2 hours
|
|
});
|
|
|
|
it('should preserve duration when converting UTC to local', () => {
|
|
const utcStart = '2024-06-15T10:00:00Z';
|
|
const utcEnd = '2024-06-15T18:00:00Z';
|
|
|
|
const localStart = dateService.fromUTC(utcStart);
|
|
const localEnd = dateService.fromUTC(utcEnd);
|
|
|
|
const utcDuration = 8 * 60; // 8 hours
|
|
const localDuration = dateService.getDurationMinutes(localStart, localEnd);
|
|
|
|
expect(localDuration).toBe(utcDuration);
|
|
});
|
|
|
|
it('should handle all-day events (00:00 to 00:00 next day)', () => {
|
|
const start = new Date(2024, 0, 15, 0, 0, 0);
|
|
const end = new Date(2024, 0, 16, 0, 0, 0);
|
|
|
|
expect(dateService.isMultiDay(start, end)).toBe(true);
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(24 * 60); // 24 hours
|
|
});
|
|
|
|
it('should handle multi-day all-day events', () => {
|
|
const start = new Date(2024, 0, 15, 0, 0, 0);
|
|
const end = new Date(2024, 0, 18, 0, 0, 0); // 3-day event
|
|
|
|
expect(dateService.isMultiDay(start, end)).toBe(true);
|
|
|
|
const duration = dateService.getDurationMinutes(start, end);
|
|
expect(duration).toBe(3 * 24 * 60); // 72 hours
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases with Minutes Since Midnight', () => {
|
|
it('should calculate minutes since midnight correctly at day boundary', () => {
|
|
const midnight = new Date(2024, 0, 15, 0, 0);
|
|
const beforeMidnight = new Date(2024, 0, 14, 23, 59);
|
|
const afterMidnight = new Date(2024, 0, 15, 0, 1);
|
|
|
|
expect(dateService.getMinutesSinceMidnight(midnight)).toBe(0);
|
|
expect(dateService.getMinutesSinceMidnight(beforeMidnight)).toBe(23 * 60 + 59);
|
|
expect(dateService.getMinutesSinceMidnight(afterMidnight)).toBe(1);
|
|
});
|
|
|
|
it('should handle createDateAtTime with overflow minutes (>1440)', () => {
|
|
const baseDate = new Date(2024, 0, 15);
|
|
|
|
// 1500 minutes = 25 hours = next day at 01:00
|
|
const result = dateService.createDateAtTime(baseDate, 1500);
|
|
|
|
expect(result.getDate()).toBe(16); // Next day
|
|
expect(result.getHours()).toBe(1);
|
|
expect(result.getMinutes()).toBe(0);
|
|
});
|
|
|
|
it('should handle createDateAtTime with large overflow (48+ hours)', () => {
|
|
const baseDate = new Date(2024, 0, 15);
|
|
|
|
// 2880 minutes = 48 hours = 2 days later
|
|
const result = dateService.createDateAtTime(baseDate, 2880);
|
|
|
|
expect(result.getDate()).toBe(17); // 2 days later
|
|
expect(result.getHours()).toBe(0);
|
|
expect(result.getMinutes()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Same Day vs Multi-Day Detection', () => {
|
|
it('should correctly identify same-day events', () => {
|
|
const start = new Date(2024, 0, 15, 8, 0);
|
|
const end = new Date(2024, 0, 15, 17, 0);
|
|
|
|
expect(dateService.isSameDay(start, end)).toBe(true);
|
|
expect(dateService.isMultiDay(start, end)).toBe(false);
|
|
});
|
|
|
|
it('should correctly identify multi-day events', () => {
|
|
const start = new Date(2024, 0, 15, 23, 0);
|
|
const end = new Date(2024, 0, 16, 1, 0);
|
|
|
|
expect(dateService.isSameDay(start, end)).toBe(false);
|
|
expect(dateService.isMultiDay(start, end)).toBe(true);
|
|
});
|
|
|
|
it('should handle ISO string inputs for multi-day detection', () => {
|
|
const startISO = '2024-01-15T23:00:00Z';
|
|
const endISO = '2024-01-16T01:00:00Z';
|
|
|
|
// Convert UTC strings to local timezone first
|
|
const startLocal = dateService.fromUTC(startISO);
|
|
const endLocal = dateService.fromUTC(endISO);
|
|
|
|
const result = dateService.isMultiDay(startLocal, endLocal);
|
|
|
|
// 23:00 UTC = 00:00 CET (next day) in Copenhagen
|
|
// So this IS a multi-day event in local time
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle mixed Date and string inputs', () => {
|
|
const startDate = new Date(2024, 0, 15, 10, 0);
|
|
const endISO = '2024-01-16T10:00:00Z';
|
|
|
|
const result = dateService.isMultiDay(startDate, endISO);
|
|
expect(typeof result).toBe('boolean');
|
|
});
|
|
});
|
|
});
|