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.
This commit is contained in:
parent
a86a736340
commit
9bc082eed4
20 changed files with 1641 additions and 41 deletions
376
test/utils/DateService.validation.test.ts
Normal file
376
test/utils/DateService.validation.test.ts
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue