Calendar/test/utils/DateService.edge-cases.test.ts

221 lines
8.3 KiB
TypeScript
Raw Normal View History

import { describe, it, expect } from 'vitest';
import { DateService } from '../../src/utils/DateService';
import { CalendarConfig } from '../../src/core/CalendarConfig';
describe('DateService - Edge Cases', () => {
const config = new CalendarConfig();
const dateService = new DateService(config);
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);
});
});
});