Calendar/test/utils/DateService.validation.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

376 lines
12 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { DateService } from '../../src/utils/DateService';
describe('DateService - Validation', () => {
const dateService = new DateService('Europe/Copenhagen');
describe('isValid() - Basic Date Validation', () => {
it('should validate normal dates', () => {
const validDate = new Date(2024, 5, 15);
expect(dateService.isValid(validDate)).toBe(true);
});
it('should reject invalid date strings', () => {
const invalidDate = new Date('not a date');
expect(dateService.isValid(invalidDate)).toBe(false);
});
it('should reject NaN dates', () => {
const nanDate = new Date(NaN);
expect(dateService.isValid(nanDate)).toBe(false);
});
it('should reject dates created from invalid input', () => {
const invalidDate = new Date('2024-13-45'); // Invalid month and day
expect(dateService.isValid(invalidDate)).toBe(false);
});
it('should validate leap year dates', () => {
const leapDay = new Date(2024, 1, 29); // Feb 29, 2024
expect(dateService.isValid(leapDay)).toBe(true);
});
it('should detect invalid leap year dates', () => {
const invalidLeapDay = new Date(2023, 1, 29); // Feb 29, 2023 (not leap year)
// JavaScript auto-corrects this to March 1
expect(invalidLeapDay.getMonth()).toBe(2); // March
expect(invalidLeapDay.getDate()).toBe(1);
});
});
describe('isWithinBounds() - Date Range Validation', () => {
it('should accept dates within bounds (1900-2100)', () => {
const dates = [
new Date(1900, 0, 1), // Min bound
new Date(1950, 6, 15),
new Date(2000, 0, 1),
new Date(2024, 5, 15),
new Date(2100, 11, 31) // Max bound
];
dates.forEach(date => {
expect(dateService.isWithinBounds(date)).toBe(true);
});
});
it('should reject dates before 1900', () => {
const tooEarly = [
new Date(1899, 11, 31),
new Date(1800, 0, 1),
new Date(1000, 6, 15)
];
tooEarly.forEach(date => {
expect(dateService.isWithinBounds(date)).toBe(false);
});
});
it('should reject dates after 2100', () => {
const tooLate = [
new Date(2101, 0, 1),
new Date(2200, 6, 15),
new Date(3000, 0, 1)
];
tooLate.forEach(date => {
expect(dateService.isWithinBounds(date)).toBe(false);
});
});
it('should reject invalid dates', () => {
const invalidDate = new Date('invalid');
expect(dateService.isWithinBounds(invalidDate)).toBe(false);
});
it('should handle boundary dates exactly', () => {
const minDate = new Date(1900, 0, 1, 0, 0, 0);
const maxDate = new Date(2100, 11, 31, 23, 59, 59);
expect(dateService.isWithinBounds(minDate)).toBe(true);
expect(dateService.isWithinBounds(maxDate)).toBe(true);
});
});
describe('isValidRange() - Date Range Validation', () => {
it('should validate correct date ranges', () => {
const start = new Date(2024, 0, 15);
const end = new Date(2024, 0, 20);
expect(dateService.isValidRange(start, end)).toBe(true);
});
it('should accept equal start and end dates', () => {
const date = new Date(2024, 0, 15, 10, 0);
expect(dateService.isValidRange(date, date)).toBe(true);
});
it('should reject reversed date ranges', () => {
const start = new Date(2024, 0, 20);
const end = new Date(2024, 0, 15);
expect(dateService.isValidRange(start, end)).toBe(false);
});
it('should reject ranges with invalid start date', () => {
const invalidStart = new Date('invalid');
const validEnd = new Date(2024, 0, 20);
expect(dateService.isValidRange(invalidStart, validEnd)).toBe(false);
});
it('should reject ranges with invalid end date', () => {
const validStart = new Date(2024, 0, 15);
const invalidEnd = new Date('invalid');
expect(dateService.isValidRange(validStart, invalidEnd)).toBe(false);
});
it('should validate ranges across year boundaries', () => {
const start = new Date(2024, 11, 30);
const end = new Date(2025, 0, 5);
expect(dateService.isValidRange(start, end)).toBe(true);
});
it('should validate multi-year ranges', () => {
const start = new Date(2020, 0, 1);
const end = new Date(2024, 11, 31);
expect(dateService.isValidRange(start, end)).toBe(true);
});
});
describe('validateDate() - Comprehensive Validation', () => {
it('should validate normal dates without options', () => {
const date = new Date(2024, 5, 15);
const result = dateService.validateDate(date);
expect(result.valid).toBe(true);
expect(result.error).toBeUndefined();
});
it('should reject invalid dates', () => {
const invalidDate = new Date('invalid');
const result = dateService.validateDate(invalidDate);
expect(result.valid).toBe(false);
expect(result.error).toBe('Invalid date');
});
it('should reject out-of-bounds dates', () => {
const tooEarly = new Date(1899, 0, 1);
const result = dateService.validateDate(tooEarly);
expect(result.valid).toBe(false);
expect(result.error).toContain('out of bounds');
});
it('should validate future dates with requireFuture option', () => {
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 10);
const result = dateService.validateDate(futureDate, { requireFuture: true });
expect(result.valid).toBe(true);
});
it('should reject past dates with requireFuture option', () => {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - 10);
const result = dateService.validateDate(pastDate, { requireFuture: true });
expect(result.valid).toBe(false);
expect(result.error).toContain('future');
});
it('should validate past dates with requirePast option', () => {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - 10);
const result = dateService.validateDate(pastDate, { requirePast: true });
expect(result.valid).toBe(true);
});
it('should reject future dates with requirePast option', () => {
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 10);
const result = dateService.validateDate(futureDate, { requirePast: true });
expect(result.valid).toBe(false);
expect(result.error).toContain('past');
});
it('should validate dates with minDate constraint', () => {
const minDate = new Date(2024, 0, 1);
const testDate = new Date(2024, 6, 15);
const result = dateService.validateDate(testDate, { minDate });
expect(result.valid).toBe(true);
});
it('should reject dates before minDate', () => {
const minDate = new Date(2024, 6, 1);
const testDate = new Date(2024, 5, 15);
const result = dateService.validateDate(testDate, { minDate });
expect(result.valid).toBe(false);
expect(result.error).toContain('after');
});
it('should validate dates with maxDate constraint', () => {
const maxDate = new Date(2024, 11, 31);
const testDate = new Date(2024, 6, 15);
const result = dateService.validateDate(testDate, { maxDate });
expect(result.valid).toBe(true);
});
it('should reject dates after maxDate', () => {
const maxDate = new Date(2024, 6, 31);
const testDate = new Date(2024, 7, 15);
const result = dateService.validateDate(testDate, { maxDate });
expect(result.valid).toBe(false);
expect(result.error).toContain('before');
});
it('should validate dates with both minDate and maxDate', () => {
const minDate = new Date(2024, 0, 1);
const maxDate = new Date(2024, 11, 31);
const testDate = new Date(2024, 6, 15);
const result = dateService.validateDate(testDate, { minDate, maxDate });
expect(result.valid).toBe(true);
});
it('should reject dates outside min/max range', () => {
const minDate = new Date(2024, 6, 1);
const maxDate = new Date(2024, 6, 31);
const testDate = new Date(2024, 7, 15);
const result = dateService.validateDate(testDate, { minDate, maxDate });
expect(result.valid).toBe(false);
});
});
describe('Invalid Date Scenarios', () => {
it('should handle February 30 (auto-corrects to March)', () => {
const invalidDate = new Date(2024, 1, 30); // Tries Feb 30, 2024
// JavaScript auto-corrects to March
expect(invalidDate.getMonth()).toBe(2); // March
expect(invalidDate.getDate()).toBe(1);
});
it('should handle month overflow (month 13)', () => {
const date = new Date(2024, 12, 1); // Month 13 = January next year
expect(date.getFullYear()).toBe(2025);
expect(date.getMonth()).toBe(0); // January
});
it('should handle negative months', () => {
const date = new Date(2024, -1, 1); // Month -1 = December previous year
expect(date.getFullYear()).toBe(2023);
expect(date.getMonth()).toBe(11); // December
});
it('should handle day 0 (last day of previous month)', () => {
const date = new Date(2024, 1, 0); // Day 0 of Feb = Last day of Jan
expect(date.getMonth()).toBe(0); // January
expect(date.getDate()).toBe(31);
});
it('should handle negative days', () => {
const date = new Date(2024, 1, -1); // Day -1 of Feb
expect(date.getMonth()).toBe(0); // January
expect(date.getDate()).toBe(30);
});
});
describe('Timezone-aware Validation', () => {
it('should validate UTC dates converted to local timezone', () => {
const utcString = '2024-06-15T12:00:00Z';
const localDate = dateService.fromUTC(utcString);
expect(dateService.isValid(localDate)).toBe(true);
expect(dateService.isWithinBounds(localDate)).toBe(true);
});
it('should maintain validation across timezone conversion', () => {
const localDate = new Date(2024, 6, 15, 12, 0);
const utcString = dateService.toUTC(localDate);
const convertedBack = dateService.fromUTC(utcString);
expect(dateService.isValid(convertedBack)).toBe(true);
// Should be same day (accounting for timezone)
const validation = dateService.validateDate(convertedBack);
expect(validation.valid).toBe(true);
});
it('should validate dates during DST transitions', () => {
// Spring DST: March 31, 2024 in Copenhagen
const dstDate = new Date(2024, 2, 31, 2, 30); // Non-existent hour
// JavaScript handles this, should still be valid
expect(dateService.isValid(dstDate)).toBe(true);
});
});
describe('Edge Case Validation Combinations', () => {
it('should reject invalid date even with lenient options', () => {
const invalidDate = new Date('completely invalid');
const result = dateService.validateDate(invalidDate, {
minDate: new Date(1900, 0, 1),
maxDate: new Date(2100, 11, 31)
});
expect(result.valid).toBe(false);
expect(result.error).toBe('Invalid date');
});
it('should validate boundary dates with constraints', () => {
const boundaryDate = new Date(1900, 0, 1);
const result = dateService.validateDate(boundaryDate, {
minDate: new Date(1900, 0, 1)
});
expect(result.valid).toBe(true);
});
it('should provide meaningful error messages', () => {
const testCases = [
{ date: new Date('invalid'), expectedError: 'Invalid date' },
{ date: new Date(1800, 0, 1), expectedError: 'bounds' },
];
testCases.forEach(({ date, expectedError }) => {
const result = dateService.validateDate(date);
expect(result.valid).toBe(false);
expect(result.error).toContain(expectedError);
});
});
it('should validate leap year boundaries correctly', () => {
const leapYearEnd = new Date(2024, 1, 29); // Last day of Feb in leap year
const nonLeapYearEnd = new Date(2023, 1, 28); // Last day of Feb in non-leap year
expect(dateService.validateDate(leapYearEnd).valid).toBe(true);
expect(dateService.validateDate(nonLeapYearEnd).valid).toBe(true);
});
});
});