287 lines
11 KiB
TypeScript
287 lines
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);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|