2025-10-04 00:32:26 +02:00
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
|
|
import { NavigationManager } from '../../src/managers/NavigationManager';
|
|
|
|
|
import { EventBus } from '../../src/core/EventBus';
|
|
|
|
|
import { EventRenderingService } from '../../src/renderers/EventRendererManager';
|
|
|
|
|
import { DateService } from '../../src/utils/DateService';
|
2025-10-30 23:47:30 +01:00
|
|
|
import { CalendarConfig } from '../../src/core/CalendarConfig';
|
2025-10-04 00:32:26 +02:00
|
|
|
|
|
|
|
|
describe('NavigationManager - Edge Cases', () => {
|
|
|
|
|
let navigationManager: NavigationManager;
|
|
|
|
|
let eventBus: EventBus;
|
|
|
|
|
let dateService: DateService;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
eventBus = new EventBus();
|
2025-10-30 23:47:30 +01:00
|
|
|
const config = new CalendarConfig();
|
|
|
|
|
dateService = new DateService(config);
|
2025-10-04 00:32:26 +02:00
|
|
|
const mockEventRenderer = {} as EventRenderingService;
|
2025-10-30 23:47:30 +01:00
|
|
|
const mockGridRenderer = {} as any;
|
|
|
|
|
const mockNavigationRenderer = {} as any;
|
|
|
|
|
navigationManager = new NavigationManager(eventBus, mockEventRenderer, mockGridRenderer, dateService, mockNavigationRenderer);
|
2025-10-04 00:32:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Week 53 Navigation', () => {
|
|
|
|
|
it('should correctly navigate to week 53 (year 2020)', () => {
|
|
|
|
|
// Dec 28, 2020 is start of week 53
|
|
|
|
|
const week53Start = new Date(2020, 11, 28);
|
|
|
|
|
|
|
|
|
|
const weekNum = dateService.getWeekNumber(week53Start);
|
|
|
|
|
expect(weekNum).toBe(53);
|
|
|
|
|
|
|
|
|
|
const weekBounds = dateService.getWeekBounds(week53Start);
|
|
|
|
|
expect(weekBounds.start.getDate()).toBe(28);
|
|
|
|
|
expect(weekBounds.start.getMonth()).toBe(11); // December
|
|
|
|
|
expect(weekBounds.start.getFullYear()).toBe(2020);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should navigate from week 53 to week 1 of next year', () => {
|
|
|
|
|
const week53 = new Date(2020, 11, 28); // Week 53, 2020
|
|
|
|
|
|
|
|
|
|
// Add 1 week should go to week 1 of 2021
|
|
|
|
|
const nextWeek = dateService.addWeeks(week53, 1);
|
|
|
|
|
const nextWeekNum = dateService.getWeekNumber(nextWeek);
|
|
|
|
|
|
|
|
|
|
expect(nextWeek.getFullYear()).toBe(2021);
|
|
|
|
|
expect(nextWeekNum).toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should navigate from week 1 back to week 53 of previous year', () => {
|
|
|
|
|
const week1_2021 = new Date(2021, 0, 4); // Monday Jan 4, 2021 (week 1)
|
|
|
|
|
|
|
|
|
|
// Subtract 1 week should go to week 53 of 2020
|
|
|
|
|
const prevWeek = dateService.addWeeks(week1_2021, -1);
|
|
|
|
|
const prevWeekNum = dateService.getWeekNumber(prevWeek);
|
|
|
|
|
|
|
|
|
|
expect(prevWeek.getFullYear()).toBe(2020);
|
|
|
|
|
expect(prevWeekNum).toBe(53);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle years without week 53 (2021)', () => {
|
|
|
|
|
const dec27_2021 = new Date(2021, 11, 27); // Monday Dec 27, 2021
|
|
|
|
|
const weekNum = dateService.getWeekNumber(dec27_2021);
|
|
|
|
|
|
|
|
|
|
expect(weekNum).toBe(52); // No week 53 in 2021
|
|
|
|
|
|
|
|
|
|
const nextWeek = dateService.addWeeks(dec27_2021, 1);
|
|
|
|
|
const nextWeekNum = dateService.getWeekNumber(nextWeek);
|
|
|
|
|
|
|
|
|
|
// ISO week logic: Adding 1 week from Dec 27 gives Jan 3, which is week 1 of 2022
|
|
|
|
|
expect(nextWeekNum).toBe(1); // Week 1 of 2022
|
|
|
|
|
|
|
|
|
|
// Jan 3, 2022 is indeed week 1
|
|
|
|
|
const jan3_2022 = new Date(2022, 0, 3);
|
|
|
|
|
expect(dateService.getWeekNumber(jan3_2022)).toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should correctly identify week 53 in 2026', () => {
|
|
|
|
|
const dec28_2026 = new Date(2026, 11, 28); // Monday Dec 28, 2026
|
|
|
|
|
const weekNum = dateService.getWeekNumber(dec28_2026);
|
|
|
|
|
|
|
|
|
|
expect(weekNum).toBe(53);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Year Boundary Navigation', () => {
|
|
|
|
|
it('should navigate across year boundary (Dec -> Jan)', () => {
|
|
|
|
|
const lastWeekDec = new Date(2024, 11, 23); // Dec 23, 2024
|
|
|
|
|
const firstWeekJan = dateService.addWeeks(lastWeekDec, 1);
|
|
|
|
|
|
|
|
|
|
// Adding 1 week gives Dec 30, which is in week 1 of 2025 (ISO week logic)
|
|
|
|
|
expect(firstWeekJan.getMonth()).toBe(11); // Still December
|
|
|
|
|
|
|
|
|
|
const weekNum = dateService.getWeekNumber(firstWeekJan);
|
|
|
|
|
// Week number can be 1 (of next year) or 52 depending on ISO week rules
|
|
|
|
|
expect(weekNum).toBeGreaterThanOrEqual(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should navigate across year boundary (Jan -> Dec)', () => {
|
|
|
|
|
const firstWeekJan = new Date(2024, 0, 1);
|
|
|
|
|
const lastWeekDec = dateService.addWeeks(firstWeekJan, -1);
|
|
|
|
|
|
|
|
|
|
expect(lastWeekDec.getFullYear()).toBe(2023);
|
|
|
|
|
|
|
|
|
|
const weekNum = dateService.getWeekNumber(lastWeekDec);
|
|
|
|
|
expect(weekNum).toBeGreaterThanOrEqual(52);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should get correct week bounds at year start', () => {
|
|
|
|
|
const jan1_2024 = new Date(2024, 0, 1); // Monday Jan 1, 2024
|
|
|
|
|
const weekBounds = dateService.getWeekBounds(jan1_2024);
|
|
|
|
|
|
|
|
|
|
// Week should start on Monday
|
|
|
|
|
const startDayOfWeek = weekBounds.start.getDay();
|
|
|
|
|
expect(startDayOfWeek).toBe(1); // Monday = 1
|
|
|
|
|
|
|
|
|
|
expect(weekBounds.start.getDate()).toBe(1);
|
|
|
|
|
expect(weekBounds.start.getMonth()).toBe(0); // January
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should get correct week bounds at year end', () => {
|
|
|
|
|
const dec31_2024 = new Date(2024, 11, 31); // Tuesday Dec 31, 2024
|
|
|
|
|
const weekBounds = dateService.getWeekBounds(dec31_2024);
|
|
|
|
|
|
|
|
|
|
// Week should start on Monday (Dec 30, 2024)
|
|
|
|
|
expect(weekBounds.start.getDate()).toBe(30);
|
|
|
|
|
expect(weekBounds.start.getMonth()).toBe(11);
|
|
|
|
|
expect(weekBounds.start.getFullYear()).toBe(2024);
|
|
|
|
|
|
|
|
|
|
// Week should end on Sunday (Jan 5, 2025)
|
|
|
|
|
expect(weekBounds.end.getDate()).toBe(5);
|
|
|
|
|
expect(weekBounds.end.getMonth()).toBe(0); // January
|
|
|
|
|
expect(weekBounds.end.getFullYear()).toBe(2025);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('DST Transition Navigation', () => {
|
|
|
|
|
it('should navigate across spring DST transition (March 2024)', () => {
|
|
|
|
|
// Spring DST: March 31, 2024, 02:00 -> 03:00
|
|
|
|
|
const beforeDST = new Date(2024, 2, 25); // Week before DST
|
|
|
|
|
const duringDST = dateService.addWeeks(beforeDST, 1);
|
|
|
|
|
|
|
|
|
|
expect(duringDST.getMonth()).toBe(3); // April
|
|
|
|
|
expect(dateService.isValid(duringDST)).toBe(true);
|
|
|
|
|
|
|
|
|
|
const weekBounds = dateService.getWeekBounds(duringDST);
|
|
|
|
|
expect(weekBounds.start.getMonth()).toBeGreaterThanOrEqual(2); // March or April
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should navigate across fall DST transition (October 2024)', () => {
|
|
|
|
|
// Fall DST: October 27, 2024, 03:00 -> 02:00
|
|
|
|
|
const beforeDST = new Date(2024, 9, 21); // Week before DST
|
|
|
|
|
const duringDST = dateService.addWeeks(beforeDST, 1);
|
|
|
|
|
|
|
|
|
|
expect(duringDST.getMonth()).toBe(9); // October
|
|
|
|
|
expect(dateService.isValid(duringDST)).toBe(true);
|
|
|
|
|
|
|
|
|
|
const weekBounds = dateService.getWeekBounds(duringDST);
|
|
|
|
|
expect(weekBounds.end.getMonth()).toBeLessThanOrEqual(10); // October or November
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should maintain week integrity across DST', () => {
|
|
|
|
|
const beforeDST = new Date(2024, 2, 25, 12, 0);
|
|
|
|
|
const afterDST = dateService.addWeeks(beforeDST, 1);
|
|
|
|
|
|
|
|
|
|
// Week bounds should still give 7-day span
|
|
|
|
|
const weekBounds = dateService.getWeekBounds(afterDST);
|
|
|
|
|
const daysDiff = (weekBounds.end.getTime() - weekBounds.start.getTime()) / (1000 * 60 * 60 * 24);
|
|
|
|
|
|
|
|
|
|
// Should be close to 7 days (accounting for DST hour change)
|
|
|
|
|
expect(daysDiff).toBeGreaterThanOrEqual(6.9);
|
|
|
|
|
expect(daysDiff).toBeLessThanOrEqual(7.1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Month Boundary Week Navigation', () => {
|
|
|
|
|
it('should handle week spanning month boundary', () => {
|
|
|
|
|
const endOfMonth = new Date(2024, 0, 29); // Jan 29, 2024 (Monday)
|
|
|
|
|
const weekBounds = dateService.getWeekBounds(endOfMonth);
|
|
|
|
|
|
|
|
|
|
// Week should span into February
|
|
|
|
|
expect(weekBounds.end.getMonth()).toBe(1); // February
|
|
|
|
|
expect(weekBounds.end.getDate()).toBe(4);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should navigate to next week across month boundary', () => {
|
|
|
|
|
const lastWeekJan = new Date(2024, 0, 29);
|
|
|
|
|
const firstWeekFeb = dateService.addWeeks(lastWeekJan, 1);
|
|
|
|
|
|
|
|
|
|
expect(firstWeekFeb.getMonth()).toBe(1); // February
|
|
|
|
|
expect(firstWeekFeb.getDate()).toBe(5);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle February-March boundary in leap year', () => {
|
|
|
|
|
const lastWeekFeb = new Date(2024, 1, 26); // Feb 26, 2024 (leap year)
|
|
|
|
|
const weekBounds = dateService.getWeekBounds(lastWeekFeb);
|
|
|
|
|
|
|
|
|
|
// Week should span from Feb into March
|
|
|
|
|
expect(weekBounds.start.getMonth()).toBe(1); // February
|
|
|
|
|
expect(weekBounds.end.getMonth()).toBe(2); // March
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Invalid Date Navigation', () => {
|
|
|
|
|
it('should reject navigation to invalid date', () => {
|
|
|
|
|
const invalidDate = new Date('invalid');
|
|
|
|
|
const validation = dateService.validateDate(invalidDate);
|
|
|
|
|
|
|
|
|
|
expect(validation.valid).toBe(false);
|
|
|
|
|
expect(validation.error).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should reject navigation to out-of-bounds date', () => {
|
|
|
|
|
const outOfBounds = new Date(2150, 0, 1);
|
|
|
|
|
const validation = dateService.validateDate(outOfBounds);
|
|
|
|
|
|
|
|
|
|
expect(validation.valid).toBe(false);
|
|
|
|
|
expect(validation.error).toContain('bounds');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should accept valid date within bounds', () => {
|
|
|
|
|
const validDate = new Date(2024, 6, 15);
|
|
|
|
|
const validation = dateService.validateDate(validDate);
|
|
|
|
|
|
|
|
|
|
expect(validation.valid).toBe(true);
|
|
|
|
|
expect(validation.error).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Week Number Edge Cases', () => {
|
|
|
|
|
it('should handle first day of year in previous year\'s week', () => {
|
|
|
|
|
// Jan 1, 2023 is a Sunday, part of week 52 of 2022
|
|
|
|
|
const jan1_2023 = new Date(2023, 0, 1);
|
|
|
|
|
const weekNum = dateService.getWeekNumber(jan1_2023);
|
|
|
|
|
|
|
|
|
|
expect(weekNum).toBe(52); // Part of 2022's last week
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle last day of year in next year\'s week', () => {
|
|
|
|
|
// Dec 31, 2023 is a Sunday, part of week 52 of 2023
|
|
|
|
|
const dec31_2023 = new Date(2023, 11, 31);
|
|
|
|
|
const weekNum = dateService.getWeekNumber(dec31_2023);
|
|
|
|
|
|
|
|
|
|
expect(weekNum).toBe(52);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should correctly number weeks in leap year', () => {
|
|
|
|
|
const dates2024 = [
|
|
|
|
|
new Date(2024, 0, 1), // Week 1
|
|
|
|
|
new Date(2024, 6, 1), // Mid-year
|
|
|
|
|
new Date(2024, 11, 31) // Last week
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
dates2024.forEach(date => {
|
|
|
|
|
const weekNum = dateService.getWeekNumber(date);
|
|
|
|
|
expect(weekNum).toBeGreaterThanOrEqual(1);
|
|
|
|
|
expect(weekNum).toBeLessThanOrEqual(53);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Navigation Continuity', () => {
|
|
|
|
|
it('should maintain continuity over multiple forward navigations', () => {
|
|
|
|
|
let currentWeek = new Date(2024, 0, 1);
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 60; i++) { // Navigate 60 weeks forward
|
|
|
|
|
currentWeek = dateService.addWeeks(currentWeek, 1);
|
|
|
|
|
expect(dateService.isValid(currentWeek)).toBe(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Should be in 2025
|
|
|
|
|
expect(currentWeek.getFullYear()).toBe(2025);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should maintain continuity over multiple backward navigations', () => {
|
|
|
|
|
let currentWeek = new Date(2024, 11, 31);
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 60; i++) { // Navigate 60 weeks backward
|
|
|
|
|
currentWeek = dateService.addWeeks(currentWeek, -1);
|
|
|
|
|
expect(dateService.isValid(currentWeek)).toBe(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Should be in 2023
|
|
|
|
|
expect(currentWeek.getFullYear()).toBe(2023);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return to same week after forward+backward navigation', () => {
|
|
|
|
|
const originalWeek = new Date(2024, 6, 15);
|
|
|
|
|
const weekBoundsOriginal = dateService.getWeekBounds(originalWeek);
|
|
|
|
|
|
|
|
|
|
// Navigate 10 weeks forward, then 10 weeks back
|
|
|
|
|
const forward = dateService.addWeeks(originalWeek, 10);
|
|
|
|
|
const backAgain = dateService.addWeeks(forward, -10);
|
|
|
|
|
|
|
|
|
|
const weekBoundsBack = dateService.getWeekBounds(backAgain);
|
|
|
|
|
|
|
|
|
|
expect(weekBoundsBack.start.getTime()).toBe(weekBoundsOriginal.start.getTime());
|
|
|
|
|
expect(weekBoundsBack.end.getTime()).toBe(weekBoundsOriginal.end.getTime());
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|