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); }); }); });