654 lines
20 KiB
TypeScript
654 lines
20 KiB
TypeScript
|
|
/**
|
||
|
|
* TDD Test Suite for EventStackManager
|
||
|
|
*
|
||
|
|
* This test suite follows Test-Driven Development principles:
|
||
|
|
* 1. Write a failing test (RED)
|
||
|
|
* 2. Write minimal code to make it pass (GREEN)
|
||
|
|
* 3. Refactor if needed (REFACTOR)
|
||
|
|
*
|
||
|
|
* @see STACKING_CONCEPT.md for concept documentation
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||
|
|
import { EventStackManager, StackLink } from '../../src/managers/EventStackManager';
|
||
|
|
|
||
|
|
describe('EventStackManager - TDD Suite', () => {
|
||
|
|
let manager: EventStackManager;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
manager = new EventStackManager();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Overlap Detection', () => {
|
||
|
|
it('should detect overlap when event A starts before event B ends and event A ends after event B starts', () => {
|
||
|
|
// RED - This test will fail initially
|
||
|
|
const eventA = {
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventB = {
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T12:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
// Expected: true (events overlap from 10:00 to 11:00)
|
||
|
|
expect(manager.doEventsOverlap(eventA, eventB)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return false when events do not overlap', () => {
|
||
|
|
const eventA = {
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T10:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventB = {
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T11:00:00'),
|
||
|
|
end: new Date('2025-01-01T12:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
expect(manager.doEventsOverlap(eventA, eventB)).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should detect overlap when one event completely contains another', () => {
|
||
|
|
const eventA = {
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T13:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventB = {
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
expect(manager.doEventsOverlap(eventA, eventB)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return false when events touch but do not overlap', () => {
|
||
|
|
const eventA = {
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T10:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventB = {
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T10:00:00'), // Exactly when A ends
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
expect(manager.doEventsOverlap(eventA, eventB)).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Find Overlapping Events', () => {
|
||
|
|
it('should find all events that overlap with a given event', () => {
|
||
|
|
const targetEvent = {
|
||
|
|
id: 'target',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const columnEvents = [
|
||
|
|
{
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T10:30:00') // Overlaps
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T12:00:00'),
|
||
|
|
end: new Date('2025-01-01T13:00:00') // Does not overlap
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'event-c',
|
||
|
|
start: new Date('2025-01-01T10:30:00'),
|
||
|
|
end: new Date('2025-01-01T11:30:00') // Overlaps
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const overlapping = manager.findOverlappingEvents(targetEvent, columnEvents);
|
||
|
|
|
||
|
|
expect(overlapping).toHaveLength(2);
|
||
|
|
expect(overlapping.map(e => e.id)).toContain('event-a');
|
||
|
|
expect(overlapping.map(e => e.id)).toContain('event-c');
|
||
|
|
expect(overlapping.map(e => e.id)).not.toContain('event-b');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return empty array when no events overlap', () => {
|
||
|
|
const targetEvent = {
|
||
|
|
id: 'target',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const columnEvents = [
|
||
|
|
{
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T09:30:00')
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T12:00:00'),
|
||
|
|
end: new Date('2025-01-01T13:00:00')
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const overlapping = manager.findOverlappingEvents(targetEvent, columnEvents);
|
||
|
|
|
||
|
|
expect(overlapping).toHaveLength(0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Create Stack Links', () => {
|
||
|
|
it('should create stack links for overlapping events sorted by start time', () => {
|
||
|
|
const events = [
|
||
|
|
{
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T12:00:00')
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'event-c',
|
||
|
|
start: new Date('2025-01-01T11:00:00'),
|
||
|
|
end: new Date('2025-01-01T13:00:00')
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const stackLinks = manager.createStackLinks(events);
|
||
|
|
|
||
|
|
// Should be sorted by start time: event-a, event-b, event-c
|
||
|
|
expect(stackLinks.size).toBe(3);
|
||
|
|
|
||
|
|
const linkA = stackLinks.get('event-a');
|
||
|
|
expect(linkA).toEqual({
|
||
|
|
stackLevel: 0,
|
||
|
|
next: 'event-b'
|
||
|
|
// no prev
|
||
|
|
});
|
||
|
|
|
||
|
|
const linkB = stackLinks.get('event-b');
|
||
|
|
expect(linkB).toEqual({
|
||
|
|
stackLevel: 1,
|
||
|
|
prev: 'event-a',
|
||
|
|
next: 'event-c'
|
||
|
|
});
|
||
|
|
|
||
|
|
const linkC = stackLinks.get('event-c');
|
||
|
|
expect(linkC).toEqual({
|
||
|
|
stackLevel: 2,
|
||
|
|
prev: 'event-b'
|
||
|
|
// no next
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return empty map for empty event array', () => {
|
||
|
|
const stackLinks = manager.createStackLinks([]);
|
||
|
|
|
||
|
|
expect(stackLinks.size).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should create single stack link for single event', () => {
|
||
|
|
const events = [
|
||
|
|
{
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T10:00:00')
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const stackLinks = manager.createStackLinks(events);
|
||
|
|
|
||
|
|
expect(stackLinks.size).toBe(1);
|
||
|
|
|
||
|
|
const link = stackLinks.get('event-a');
|
||
|
|
expect(link).toEqual({
|
||
|
|
stackLevel: 0
|
||
|
|
// no prev, no next
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle events with same start time by sorting by end time', () => {
|
||
|
|
const events = [
|
||
|
|
{
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T12:00:00') // Longer event
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00') // Shorter event (should come first)
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const stackLinks = manager.createStackLinks(events);
|
||
|
|
|
||
|
|
// Shorter event should have lower stack level
|
||
|
|
expect(stackLinks.get('event-a')?.stackLevel).toBe(0);
|
||
|
|
expect(stackLinks.get('event-b')?.stackLevel).toBe(1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Calculate Visual Styling', () => {
|
||
|
|
it('should calculate marginLeft based on stack level', () => {
|
||
|
|
const stackLevel = 0;
|
||
|
|
expect(manager.calculateMarginLeft(stackLevel)).toBe(0);
|
||
|
|
|
||
|
|
const stackLevel1 = 1;
|
||
|
|
expect(manager.calculateMarginLeft(stackLevel1)).toBe(15);
|
||
|
|
|
||
|
|
const stackLevel2 = 2;
|
||
|
|
expect(manager.calculateMarginLeft(stackLevel2)).toBe(30);
|
||
|
|
|
||
|
|
const stackLevel5 = 5;
|
||
|
|
expect(manager.calculateMarginLeft(stackLevel5)).toBe(75);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should calculate zIndex based on stack level', () => {
|
||
|
|
const stackLevel = 0;
|
||
|
|
expect(manager.calculateZIndex(stackLevel)).toBe(100);
|
||
|
|
|
||
|
|
const stackLevel1 = 1;
|
||
|
|
expect(manager.calculateZIndex(stackLevel1)).toBe(101);
|
||
|
|
|
||
|
|
const stackLevel2 = 2;
|
||
|
|
expect(manager.calculateZIndex(stackLevel2)).toBe(102);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Stack Link Serialization', () => {
|
||
|
|
it('should serialize stack link to JSON string', () => {
|
||
|
|
const stackLink: StackLink = {
|
||
|
|
stackLevel: 1,
|
||
|
|
prev: 'event-a',
|
||
|
|
next: 'event-c'
|
||
|
|
};
|
||
|
|
|
||
|
|
const serialized = manager.serializeStackLink(stackLink);
|
||
|
|
|
||
|
|
expect(serialized).toBe('{"stackLevel":1,"prev":"event-a","next":"event-c"}');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should deserialize JSON string to stack link', () => {
|
||
|
|
const json = '{"stackLevel":1,"prev":"event-a","next":"event-c"}';
|
||
|
|
|
||
|
|
const stackLink = manager.deserializeStackLink(json);
|
||
|
|
|
||
|
|
expect(stackLink).toEqual({
|
||
|
|
stackLevel: 1,
|
||
|
|
prev: 'event-a',
|
||
|
|
next: 'event-c'
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle stack link without prev/next', () => {
|
||
|
|
const stackLink: StackLink = {
|
||
|
|
stackLevel: 0
|
||
|
|
};
|
||
|
|
|
||
|
|
const serialized = manager.serializeStackLink(stackLink);
|
||
|
|
const deserialized = manager.deserializeStackLink(serialized);
|
||
|
|
|
||
|
|
expect(deserialized).toEqual({
|
||
|
|
stackLevel: 0
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return null when deserializing invalid JSON', () => {
|
||
|
|
const invalid = 'not-valid-json';
|
||
|
|
|
||
|
|
const result = manager.deserializeStackLink(invalid);
|
||
|
|
|
||
|
|
expect(result).toBeNull();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('DOM Integration', () => {
|
||
|
|
it('should apply stack link to DOM element', () => {
|
||
|
|
const element = document.createElement('div');
|
||
|
|
element.dataset.eventId = 'event-a';
|
||
|
|
|
||
|
|
const stackLink: StackLink = {
|
||
|
|
stackLevel: 1,
|
||
|
|
prev: 'event-b',
|
||
|
|
next: 'event-c'
|
||
|
|
};
|
||
|
|
|
||
|
|
manager.applyStackLinkToElement(element, stackLink);
|
||
|
|
|
||
|
|
expect(element.dataset.stackLink).toBe('{"stackLevel":1,"prev":"event-b","next":"event-c"}');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should read stack link from DOM element', () => {
|
||
|
|
const element = document.createElement('div');
|
||
|
|
element.dataset.stackLink = '{"stackLevel":2,"prev":"event-a"}';
|
||
|
|
|
||
|
|
const stackLink = manager.getStackLinkFromElement(element);
|
||
|
|
|
||
|
|
expect(stackLink).toEqual({
|
||
|
|
stackLevel: 2,
|
||
|
|
prev: 'event-a'
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return null when element has no stack link', () => {
|
||
|
|
const element = document.createElement('div');
|
||
|
|
|
||
|
|
const stackLink = manager.getStackLinkFromElement(element);
|
||
|
|
|
||
|
|
expect(stackLink).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should apply visual styling to element based on stack level', () => {
|
||
|
|
const element = document.createElement('div');
|
||
|
|
|
||
|
|
manager.applyVisualStyling(element, 2);
|
||
|
|
|
||
|
|
expect(element.style.marginLeft).toBe('30px');
|
||
|
|
expect(element.style.zIndex).toBe('102');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should clear stack link from element', () => {
|
||
|
|
const element = document.createElement('div');
|
||
|
|
element.dataset.stackLink = '{"stackLevel":1}';
|
||
|
|
|
||
|
|
manager.clearStackLinkFromElement(element);
|
||
|
|
|
||
|
|
expect(element.dataset.stackLink).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should clear visual styling from element', () => {
|
||
|
|
const element = document.createElement('div');
|
||
|
|
element.style.marginLeft = '30px';
|
||
|
|
element.style.zIndex = '102';
|
||
|
|
|
||
|
|
manager.clearVisualStyling(element);
|
||
|
|
|
||
|
|
expect(element.style.marginLeft).toBe('');
|
||
|
|
expect(element.style.zIndex).toBe('');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Edge Cases', () => {
|
||
|
|
it('should optimize stack levels when events do not overlap each other but both overlap a parent event', () => {
|
||
|
|
// Visual representation:
|
||
|
|
// Event A: 09:00 ════════════════════════════ 14:00
|
||
|
|
// Event B: 10:00 ═════ 12:00
|
||
|
|
// Event C: 12:30 ═══ 13:00
|
||
|
|
//
|
||
|
|
// Expected stacking:
|
||
|
|
// Event A: stackLevel 0 (base)
|
||
|
|
// Event B: stackLevel 1 (conflicts with A)
|
||
|
|
// Event C: stackLevel 1 (conflicts with A, but NOT with B - can share same level!)
|
||
|
|
|
||
|
|
const eventA = {
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T14:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventB = {
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T12:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventC = {
|
||
|
|
id: 'event-c',
|
||
|
|
start: new Date('2025-01-01T12:30:00'),
|
||
|
|
end: new Date('2025-01-01T13:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const stackLinks = manager.createOptimizedStackLinks([eventA, eventB, eventC]);
|
||
|
|
|
||
|
|
expect(stackLinks.size).toBe(3);
|
||
|
|
|
||
|
|
// Event A is the base (contains both B and C)
|
||
|
|
expect(stackLinks.get('event-a')?.stackLevel).toBe(0);
|
||
|
|
|
||
|
|
// Event B and C should both be at stackLevel 1 (they don't overlap each other)
|
||
|
|
expect(stackLinks.get('event-b')?.stackLevel).toBe(1);
|
||
|
|
expect(stackLinks.get('event-c')?.stackLevel).toBe(1);
|
||
|
|
|
||
|
|
// Verify they are NOT linked to each other (no prev/next between B and C)
|
||
|
|
expect(stackLinks.get('event-b')?.next).toBeUndefined();
|
||
|
|
expect(stackLinks.get('event-c')?.prev).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should create multiple parallel tracks when events at same level do not overlap', () => {
|
||
|
|
// Complex scenario with multiple parallel tracks:
|
||
|
|
// Event A: 09:00 ════════════════════════════════════ 15:00
|
||
|
|
// Event B: 10:00 ═══ 11:00
|
||
|
|
// Event C: 11:30 ═══ 12:30
|
||
|
|
// Event D: 13:00 ═══ 14:00
|
||
|
|
//
|
||
|
|
// Expected:
|
||
|
|
// - A at level 0 (base)
|
||
|
|
// - B, C, D all at level 1 (they don't overlap each other, only with A)
|
||
|
|
|
||
|
|
const eventA = {
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T15:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventB = {
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventC = {
|
||
|
|
id: 'event-c',
|
||
|
|
start: new Date('2025-01-01T11:30:00'),
|
||
|
|
end: new Date('2025-01-01T12:30:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventD = {
|
||
|
|
id: 'event-d',
|
||
|
|
start: new Date('2025-01-01T13:00:00'),
|
||
|
|
end: new Date('2025-01-01T14:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const stackLinks = manager.createOptimizedStackLinks([eventA, eventB, eventC, eventD]);
|
||
|
|
|
||
|
|
expect(stackLinks.size).toBe(4);
|
||
|
|
expect(stackLinks.get('event-a')?.stackLevel).toBe(0);
|
||
|
|
expect(stackLinks.get('event-b')?.stackLevel).toBe(1);
|
||
|
|
expect(stackLinks.get('event-c')?.stackLevel).toBe(1);
|
||
|
|
expect(stackLinks.get('event-d')?.stackLevel).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle nested overlaps with optimal stacking', () => {
|
||
|
|
// Scenario:
|
||
|
|
// Event A: 09:00 ════════════════════════════════════ 15:00
|
||
|
|
// Event B: 10:00 ════════════════════ 13:00
|
||
|
|
// Event C: 11:00 ═══ 12:00
|
||
|
|
// Event D: 12:30 ═══ 13:30
|
||
|
|
//
|
||
|
|
// Expected:
|
||
|
|
// - A at level 0 (base, contains all)
|
||
|
|
// - B at level 1 (overlaps with A)
|
||
|
|
// - C at level 2 (overlaps with A and B)
|
||
|
|
// - D at level 2 (overlaps with A and B, but NOT with C - can share level with C)
|
||
|
|
|
||
|
|
const eventA = {
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T15:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventB = {
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T13:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventC = {
|
||
|
|
id: 'event-c',
|
||
|
|
start: new Date('2025-01-01T11:00:00'),
|
||
|
|
end: new Date('2025-01-01T12:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventD = {
|
||
|
|
id: 'event-d',
|
||
|
|
start: new Date('2025-01-01T12:30:00'),
|
||
|
|
end: new Date('2025-01-01T13:30:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const stackLinks = manager.createOptimizedStackLinks([eventA, eventB, eventC, eventD]);
|
||
|
|
|
||
|
|
expect(stackLinks.size).toBe(4);
|
||
|
|
expect(stackLinks.get('event-a')?.stackLevel).toBe(0);
|
||
|
|
expect(stackLinks.get('event-b')?.stackLevel).toBe(1);
|
||
|
|
expect(stackLinks.get('event-c')?.stackLevel).toBe(2);
|
||
|
|
expect(stackLinks.get('event-d')?.stackLevel).toBe(2); // Can share level with C
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle events with identical start and end times', () => {
|
||
|
|
const eventA = {
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventB = {
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
expect(manager.doEventsOverlap(eventA, eventB)).toBe(true);
|
||
|
|
|
||
|
|
const stackLinks = manager.createStackLinks([eventA, eventB]);
|
||
|
|
expect(stackLinks.size).toBe(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle events with zero duration', () => {
|
||
|
|
const eventA = {
|
||
|
|
id: 'event-a',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T10:00:00') // Zero duration
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventB = {
|
||
|
|
id: 'event-b',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
// Zero-duration event should not overlap
|
||
|
|
expect(manager.doEventsOverlap(eventA, eventB)).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle large number of overlapping events', () => {
|
||
|
|
const events = Array.from({ length: 100 }, (_, i) => ({
|
||
|
|
id: `event-${i}`,
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date(`2025-01-01T${10 + i}:00:00`)
|
||
|
|
}));
|
||
|
|
|
||
|
|
const stackLinks = manager.createStackLinks(events);
|
||
|
|
|
||
|
|
expect(stackLinks.size).toBe(100);
|
||
|
|
expect(stackLinks.get('event-0')?.stackLevel).toBe(0);
|
||
|
|
expect(stackLinks.get('event-99')?.stackLevel).toBe(99);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Integration Tests', () => {
|
||
|
|
it('should create complete stack for new event with overlapping events', () => {
|
||
|
|
// Scenario: Adding new event that overlaps with existing events
|
||
|
|
const newEvent = {
|
||
|
|
id: 'new-event',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const existingEvents = [
|
||
|
|
{
|
||
|
|
id: 'existing-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T10:30:00')
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'existing-b',
|
||
|
|
start: new Date('2025-01-01T10:30:00'),
|
||
|
|
end: new Date('2025-01-01T12:00:00')
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
// Find overlapping
|
||
|
|
const overlapping = manager.findOverlappingEvents(newEvent, existingEvents);
|
||
|
|
|
||
|
|
// Create stack links for all events
|
||
|
|
const allEvents = [...overlapping, newEvent];
|
||
|
|
const stackLinks = manager.createStackLinks(allEvents);
|
||
|
|
|
||
|
|
// Verify complete stack
|
||
|
|
expect(stackLinks.size).toBe(3);
|
||
|
|
expect(stackLinks.get('existing-a')?.stackLevel).toBe(0);
|
||
|
|
expect(stackLinks.get('new-event')?.stackLevel).toBe(1);
|
||
|
|
expect(stackLinks.get('existing-b')?.stackLevel).toBe(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle complete workflow: detect, create, apply to DOM', () => {
|
||
|
|
const newEvent = {
|
||
|
|
id: 'new-event',
|
||
|
|
start: new Date('2025-01-01T10:00:00'),
|
||
|
|
end: new Date('2025-01-01T11:00:00')
|
||
|
|
};
|
||
|
|
|
||
|
|
const existingEvents = [
|
||
|
|
{
|
||
|
|
id: 'existing-a',
|
||
|
|
start: new Date('2025-01-01T09:00:00'),
|
||
|
|
end: new Date('2025-01-01T10:30:00')
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
// Step 1: Find overlapping
|
||
|
|
const overlapping = manager.findOverlappingEvents(newEvent, existingEvents);
|
||
|
|
expect(overlapping).toHaveLength(1);
|
||
|
|
|
||
|
|
// Step 2: Create stack links
|
||
|
|
const allEvents = [...overlapping, newEvent];
|
||
|
|
const stackLinks = manager.createStackLinks(allEvents);
|
||
|
|
expect(stackLinks.size).toBe(2);
|
||
|
|
|
||
|
|
// Step 3: Apply to DOM
|
||
|
|
const elementA = document.createElement('div');
|
||
|
|
elementA.dataset.eventId = 'existing-a';
|
||
|
|
|
||
|
|
const elementNew = document.createElement('div');
|
||
|
|
elementNew.dataset.eventId = 'new-event';
|
||
|
|
|
||
|
|
manager.applyStackLinkToElement(elementA, stackLinks.get('existing-a')!);
|
||
|
|
manager.applyStackLinkToElement(elementNew, stackLinks.get('new-event')!);
|
||
|
|
|
||
|
|
manager.applyVisualStyling(elementA, stackLinks.get('existing-a')!.stackLevel);
|
||
|
|
manager.applyVisualStyling(elementNew, stackLinks.get('new-event')!.stackLevel);
|
||
|
|
|
||
|
|
// Verify DOM state
|
||
|
|
expect(elementA.dataset.stackLink).toContain('"stackLevel":0');
|
||
|
|
expect(elementA.style.marginLeft).toBe('0px');
|
||
|
|
|
||
|
|
expect(elementNew.dataset.stackLink).toContain('"stackLevel":1');
|
||
|
|
expect(elementNew.style.marginLeft).toBe('15px');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|