Calendar/test/utils/DateService.edge-cases.test.ts
Janus C. H. Knudsen 9bc082eed4 Improves date handling and event stacking
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.
2025-10-04 00:32:26 +02:00

218 lines
8.2 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { DateService } from '../../src/utils/DateService';
describe('DateService - Edge Cases', () => {
const dateService = new DateService('Europe/Copenhagen');
describe('Leap Year Handling', () => {
it('should handle February 29 in leap year (2024)', () => {
const leapDate = new Date(2024, 1, 29); // Feb 29, 2024
expect(dateService.isValid(leapDate)).toBe(true);
expect(leapDate.getMonth()).toBe(1); // February
expect(leapDate.getDate()).toBe(29);
});
it('should reject February 29 in non-leap year (2023)', () => {
const invalidDate = new Date(2023, 1, 29); // Tries Feb 29, 2023
// JavaScript auto-corrects to March 1
expect(invalidDate.getMonth()).toBe(2); // March
expect(invalidDate.getDate()).toBe(1);
});
it('should handle February 28 in non-leap year', () => {
const validDate = new Date(2023, 1, 28); // Feb 28, 2023
expect(dateService.isValid(validDate)).toBe(true);
expect(validDate.getMonth()).toBe(1); // February
expect(validDate.getDate()).toBe(28);
});
it('should correctly add 1 year to Feb 29 (leap year)', () => {
const leapDate = new Date(2024, 1, 29);
const nextYear = dateService.addDays(leapDate, 365); // 2025 is not leap year
// Should be Feb 28, 2025 (or March 1 depending on implementation)
expect(nextYear.getFullYear()).toBe(2025);
expect(nextYear.getMonth()).toBeGreaterThanOrEqual(1); // Feb or March
});
it('should validate leap year dates with isWithinBounds', () => {
const leapDate2024 = new Date(2024, 1, 29);
const leapDate2000 = new Date(2000, 1, 29);
expect(dateService.isWithinBounds(leapDate2024)).toBe(true);
expect(dateService.isWithinBounds(leapDate2000)).toBe(true);
});
});
describe('ISO Week 53 Handling', () => {
it('should correctly identify week 53 in 2020 (has week 53)', () => {
const dec31_2020 = new Date(2020, 11, 31); // Dec 31, 2020
const weekNum = dateService.getWeekNumber(dec31_2020);
expect(weekNum).toBe(53);
});
it('should correctly identify week 53 in 2026 (has week 53)', () => {
const dec31_2026 = new Date(2026, 11, 31); // Dec 31, 2026
const weekNum = dateService.getWeekNumber(dec31_2026);
expect(weekNum).toBe(53);
});
it('should NOT have week 53 in 2021 (goes to week 52)', () => {
const dec31_2021 = new Date(2021, 11, 31); // Dec 31, 2021
const weekNum = dateService.getWeekNumber(dec31_2021);
expect(weekNum).toBe(52);
});
it('should handle transition from week 53 to week 1', () => {
const lastDayOf2020 = new Date(2020, 11, 31); // Week 53
const firstDayOf2021 = dateService.addDays(lastDayOf2020, 1);
expect(dateService.getWeekNumber(lastDayOf2020)).toBe(53);
expect(dateService.getWeekNumber(firstDayOf2021)).toBe(53); // Still week 53!
// Monday after should be week 1
const firstMonday2021 = new Date(2021, 0, 4);
expect(dateService.getWeekNumber(firstMonday2021)).toBe(1);
});
it('should get correct week bounds for week 53', () => {
const dec31_2020 = new Date(2020, 11, 31);
const weekBounds = dateService.getWeekBounds(dec31_2020);
// Week 53 of 2020 starts on Monday Dec 28, 2020
expect(weekBounds.start.getDate()).toBe(28);
expect(weekBounds.start.getMonth()).toBe(11); // December
// Ends on Sunday Jan 3, 2021
expect(weekBounds.end.getDate()).toBe(3);
expect(weekBounds.end.getMonth()).toBe(0); // January
expect(weekBounds.end.getFullYear()).toBe(2021);
});
});
describe('Month Boundary Edge Cases', () => {
it('should correctly add months across year boundary', () => {
const nov2024 = new Date(2024, 10, 15); // Nov 15, 2024
const feb2025 = dateService.addMonths(nov2024, 3);
expect(feb2025.getFullYear()).toBe(2025);
expect(feb2025.getMonth()).toBe(1); // February
expect(feb2025.getDate()).toBe(15);
});
it('should handle month-end overflow (Jan 31 + 1 month)', () => {
const jan31 = new Date(2024, 0, 31);
const result = dateService.addMonths(jan31, 1);
// date-fns addMonths handles this gracefully
expect(result.getMonth()).toBe(1); // February
expect(result.getFullYear()).toBe(2024);
// Will be Feb 29 (leap year) or last day of Feb
});
it('should handle adding negative months', () => {
const mar2024 = new Date(2024, 2, 15); // March 15, 2024
const dec2023 = dateService.addMonths(mar2024, -3);
expect(dec2023.getFullYear()).toBe(2023);
expect(dec2023.getMonth()).toBe(11); // December
expect(dec2023.getDate()).toBe(15);
});
});
describe('Year Boundary Edge Cases', () => {
it('should handle year transition (Dec 31 -> Jan 1)', () => {
const dec31 = new Date(2024, 11, 31);
const jan1 = dateService.addDays(dec31, 1);
expect(jan1.getFullYear()).toBe(2025);
expect(jan1.getMonth()).toBe(0); // January
expect(jan1.getDate()).toBe(1);
});
it('should handle reverse year transition (Jan 1 -> Dec 31)', () => {
const jan1 = new Date(2024, 0, 1);
const dec31 = dateService.addDays(jan1, -1);
expect(dec31.getFullYear()).toBe(2023);
expect(dec31.getMonth()).toBe(11); // December
expect(dec31.getDate()).toBe(31);
});
it('should correctly calculate week bounds at year boundary', () => {
const jan1_2024 = new Date(2024, 0, 1);
const weekBounds = dateService.getWeekBounds(jan1_2024);
// Jan 1, 2024 is a Monday (week 1)
expect(weekBounds.start.getDate()).toBe(1);
expect(weekBounds.start.getMonth()).toBe(0);
expect(weekBounds.start.getFullYear()).toBe(2024);
});
});
describe('DST Transition Edge Cases', () => {
it('should handle spring DST transition (CET -> CEST)', () => {
// Last Sunday of March 2024: March 31, 02:00 -> 03:00
const beforeDST = new Date(2024, 2, 31, 1, 30); // 01:30 CET
const afterDST = new Date(2024, 2, 31, 3, 30); // 03:30 CEST
expect(dateService.isValid(beforeDST)).toBe(true);
expect(dateService.isValid(afterDST)).toBe(true);
// The hour 02:00-03:00 doesn't exist!
const nonExistentTime = new Date(2024, 2, 31, 2, 30);
// JavaScript auto-adjusts this
expect(nonExistentTime.getHours()).not.toBe(2);
});
it('should handle fall DST transition (CEST -> CET)', () => {
// Last Sunday of October 2024: October 27, 03:00 -> 02:00
const beforeDST = new Date(2024, 9, 27, 2, 30, 0, 0);
const afterDST = new Date(2024, 9, 27, 3, 30, 0, 0);
expect(dateService.isValid(beforeDST)).toBe(true);
expect(dateService.isValid(afterDST)).toBe(true);
// 02:00-03:00 exists TWICE (ambiguous hour)
// This is handled by timezone-aware libraries
});
it('should calculate duration correctly across DST', () => {
// Event spanning DST transition
const start = new Date(2024, 2, 31, 1, 0); // Before DST
const end = new Date(2024, 2, 31, 4, 0); // After DST
const duration = dateService.getDurationMinutes(start, end);
// Clock time: 3 hours, but actual duration: 2 hours (due to DST)
// date-fns should handle this correctly
expect(duration).toBeGreaterThan(0);
});
});
describe('Extreme Date Values', () => {
it('should reject dates before 1900', () => {
const oldDate = new Date(1899, 11, 31);
expect(dateService.isWithinBounds(oldDate)).toBe(false);
});
it('should reject dates after 2100', () => {
const futureDate = new Date(2101, 0, 1);
expect(dateService.isWithinBounds(futureDate)).toBe(false);
});
it('should accept boundary dates (1900 and 2100)', () => {
const minDate = new Date(1900, 0, 1);
const maxDate = new Date(2100, 11, 31);
expect(dateService.isWithinBounds(minDate)).toBe(true);
expect(dateService.isWithinBounds(maxDate)).toBe(true);
});
it('should validate invalid Date objects', () => {
const invalidDate = new Date('invalid');
expect(dateService.isValid(invalidDate)).toBe(false);
expect(dateService.isWithinBounds(invalidDate)).toBe(false);
});
});
});