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

287 lines
No EOL
11 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { OverlapDetector } from '../../src/utils/OverlapDetector';
import { CalendarEvent } from '../../src/types/CalendarTypes';
describe('OverlapDetector', () => {
let detector: OverlapDetector;
beforeEach(() => {
detector = new OverlapDetector();
});
// Helper function to create test events
const createEvent = (id: string, startHour: number, startMin: number, endHour: number, endMin: number): CalendarEvent => {
const start = new Date(2024, 0, 1, startHour, startMin);
const end = new Date(2024, 0, 1, endHour, endMin);
return {
id,
title: `Event ${id}`,
start,
end,
type: 'meeting',
allDay: false,
syncStatus: 'synced'
};
};
describe('resolveOverlap', () => {
it('should detect no overlap when events do not overlap', () => {
const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00
const event2 = createEvent('2', 10, 0, 11, 0); // 10:00-11:00
const overlaps = detector.resolveOverlap(event1, [event2]);
expect(overlaps).toHaveLength(0);
});
it('should detect overlap when events partially overlap', () => {
const event1 = createEvent('1', 9, 0, 10, 30); // 09:00-10:30
const event2 = createEvent('2', 10, 0, 11, 0); // 10:00-11:00
const overlaps = detector.resolveOverlap(event1, [event2]);
expect(overlaps).toHaveLength(1);
expect(overlaps[0].id).toBe('2');
});
it('should detect overlap when one event contains another', () => {
const event1 = createEvent('1', 9, 0, 12, 0); // 09:00-12:00
const event2 = createEvent('2', 10, 0, 11, 0); // 10:00-11:00
const overlaps = detector.resolveOverlap(event1, [event2]);
expect(overlaps).toHaveLength(1);
expect(overlaps[0].id).toBe('2');
});
it('should detect overlap when events have same start time', () => {
const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00
const event2 = createEvent('2', 9, 0, 10, 30); // 09:00-10:30
const overlaps = detector.resolveOverlap(event1, [event2]);
expect(overlaps).toHaveLength(1);
expect(overlaps[0].id).toBe('2');
});
it('should detect overlap when events have same end time', () => {
const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00
const event2 = createEvent('2', 9, 30, 10, 0); // 09:30-10:00
const overlaps = detector.resolveOverlap(event1, [event2]);
expect(overlaps).toHaveLength(1);
expect(overlaps[0].id).toBe('2');
});
it('should detect multiple overlapping events', () => {
const event1 = createEvent('1', 9, 0, 11, 0); // 09:00-11:00
const event2 = createEvent('2', 9, 30, 10, 30); // 09:30-10:30
const event3 = createEvent('3', 10, 0, 11, 30); // 10:00-11:30
const event4 = createEvent('4', 12, 0, 13, 0); // 12:00-13:00 (no overlap)
const overlaps = detector.resolveOverlap(event1, [event2, event3, event4]);
expect(overlaps).toHaveLength(2);
expect(overlaps.map(e => e.id)).toEqual(['2', '3']);
});
it('should handle edge case where event ends exactly when another starts', () => {
const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00
const event2 = createEvent('2', 10, 0, 11, 0); // 10:00-11:00
const overlaps = detector.resolveOverlap(event1, [event2]);
// Events that touch at boundaries should NOT overlap
expect(overlaps).toHaveLength(0);
});
it('should handle events with 1-minute overlap', () => {
const event1 = createEvent('1', 9, 0, 10, 1); // 09:00-10:01
const event2 = createEvent('2', 10, 0, 11, 0); // 10:00-11:00
const overlaps = detector.resolveOverlap(event1, [event2]);
expect(overlaps).toHaveLength(1);
});
});
describe('decorateWithStackLinks', () => {
it('should return empty result when no overlapping events', () => {
const event1 = createEvent('1', 9, 0, 10, 0);
const result = detector.decorateWithStackLinks(event1, []);
expect(result.overlappingEvents).toHaveLength(0);
expect(result.stackLinks.size).toBe(0);
});
it('should assign stack levels based on start time order', () => {
const event1 = createEvent('1', 9, 0, 10, 30); // 09:00-10:30
const event2 = createEvent('2', 9, 30, 11, 0); // 09:30-11:00
const result = detector.decorateWithStackLinks(event1, [event2]);
expect(result.stackLinks.size).toBe(2);
const link1 = result.stackLinks.get('1' as any);
const link2 = result.stackLinks.get('2' as any);
expect(link1?.stackLevel).toBe(0);
expect(link1?.prev).toBeUndefined();
expect(link1?.next).toBe('2');
expect(link2?.stackLevel).toBe(1);
expect(link2?.prev).toBe('1');
expect(link2?.next).toBeUndefined();
});
it('should create linked chain for multiple overlapping events', () => {
const event1 = createEvent('1', 9, 0, 11, 0); // 09:00-11:00
const event2 = createEvent('2', 9, 30, 10, 30); // 09:30-10:30
const event3 = createEvent('3', 10, 0, 11, 30); // 10:00-11:30
const result = detector.decorateWithStackLinks(event1, [event2, event3]);
expect(result.stackLinks.size).toBe(3);
const link1 = result.stackLinks.get('1' as any);
const link2 = result.stackLinks.get('2' as any);
const link3 = result.stackLinks.get('3' as any);
// Check chain: 1 -> 2 -> 3
expect(link1?.stackLevel).toBe(0);
expect(link1?.prev).toBeUndefined();
expect(link1?.next).toBe('2');
expect(link2?.stackLevel).toBe(1);
expect(link2?.prev).toBe('1');
expect(link2?.next).toBe('3');
expect(link3?.stackLevel).toBe(2);
expect(link3?.prev).toBe('2');
expect(link3?.next).toBeUndefined();
});
it('should handle events with same start time', () => {
const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00
const event2 = createEvent('2', 9, 0, 10, 30); // 09:00-10:30
const result = detector.decorateWithStackLinks(event1, [event2]);
const link1 = result.stackLinks.get('1' as any);
const link2 = result.stackLinks.get('2' as any);
// Both start at same time - order may vary but levels should be 0 and 1
const levels = [link1?.stackLevel, link2?.stackLevel].sort();
expect(levels).toEqual([0, 1]);
// Verify they are linked together
expect(result.stackLinks.size).toBe(2);
});
it('KNOWN ISSUE: should NOT stack events that do not overlap', () => {
// This test documents the current bug
const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00
const event2 = createEvent('2', 9, 30, 10, 30); // 09:30-10:30 (overlaps with 1)
const event3 = createEvent('3', 11, 0, 12, 0); // 11:00-12:00 (NO overlap with 1 or 2)
const result = detector.decorateWithStackLinks(event1, [event2, event3]);
const link3 = result.stackLinks.get('3' as any);
// CURRENT BEHAVIOR (BUG): Event 3 gets stackLevel 2
expect(link3?.stackLevel).toBe(2);
// EXPECTED BEHAVIOR: Event 3 should get stackLevel 0 since it doesn't overlap
// expect(link3?.stackLevel).toBe(0);
// expect(link3?.prev).toBeUndefined();
// expect(link3?.next).toBeUndefined();
});
it('KNOWN ISSUE: should reuse stack levels when possible', () => {
// This test documents another aspect of the bug
const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00
const event2 = createEvent('2', 10, 30, 11, 30); // 10:30-11:30 (NO overlap)
const event3 = createEvent('3', 12, 0, 13, 0); // 12:00-13:00 (NO overlap)
const result = detector.decorateWithStackLinks(event1, [event2, event3]);
const link1 = result.stackLinks.get('1' as any);
const link2 = result.stackLinks.get('2' as any);
const link3 = result.stackLinks.get('3' as any);
// CURRENT BEHAVIOR (BUG): All get different stack levels
expect(link1?.stackLevel).toBe(0);
expect(link2?.stackLevel).toBe(1);
expect(link3?.stackLevel).toBe(2);
// EXPECTED BEHAVIOR: All should reuse level 0 since none overlap
// expect(link1?.stackLevel).toBe(0);
// expect(link2?.stackLevel).toBe(0);
// expect(link3?.stackLevel).toBe(0);
});
it('should handle complex overlapping pattern correctly', () => {
// Event 1: 09:00-11:00 (base)
// Event 2: 09:30-10:30 (overlaps with 1)
// Event 3: 10:00-11:30 (overlaps with 1 and 2)
// Event 4: 11:00-12:00 (overlaps with 3 only)
const event1 = createEvent('1', 9, 0, 11, 0);
const event2 = createEvent('2', 9, 30, 10, 30);
const event3 = createEvent('3', 10, 0, 11, 30);
const event4 = createEvent('4', 11, 0, 12, 0);
const result = detector.decorateWithStackLinks(event1, [event2, event3, event4]);
expect(result.stackLinks.size).toBe(4);
// All events are linked in one chain (current behavior)
const link1 = result.stackLinks.get('1' as any);
const link2 = result.stackLinks.get('2' as any);
const link3 = result.stackLinks.get('3' as any);
const link4 = result.stackLinks.get('4' as any);
expect(link1?.stackLevel).toBe(0);
expect(link2?.stackLevel).toBe(1);
expect(link3?.stackLevel).toBe(2);
expect(link4?.stackLevel).toBe(3);
});
});
describe('Edge Cases', () => {
it('should handle zero-duration events', () => {
const event1 = createEvent('1', 9, 0, 9, 0); // 09:00-09:00
const event2 = createEvent('2', 9, 0, 10, 0); // 09:00-10:00
const overlaps = detector.resolveOverlap(event1, [event2]);
// Zero-duration event at start of another should not overlap
expect(overlaps).toHaveLength(0);
});
it('should handle events spanning multiple hours', () => {
const event1 = createEvent('1', 8, 0, 17, 0); // 08:00-17:00 (9 hours)
const event2 = createEvent('2', 12, 0, 13, 0); // 12:00-13:00
const overlaps = detector.resolveOverlap(event1, [event2]);
expect(overlaps).toHaveLength(1);
});
it('should handle many events in same time slot', () => {
const event1 = createEvent('1', 9, 0, 10, 0);
const events = [
createEvent('2', 9, 0, 10, 0),
createEvent('3', 9, 0, 10, 0),
createEvent('4', 9, 0, 10, 0),
createEvent('5', 9, 0, 10, 0)
];
const overlaps = detector.resolveOverlap(event1, events);
expect(overlaps).toHaveLength(4);
});
});
});