Calendar/test/managers/EventStackManager.test.ts

657 lines
20 KiB
TypeScript
Raw Normal View History

/**
* 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
*
* NOTE: This test file is SKIPPED as it tests removed methods (createStackLinks, findOverlappingEvents)
* See EventStackManager.flexbox.test.ts for current implementation tests
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { EventStackManager, StackLink } from '../../src/managers/EventStackManager';
describe.skip('EventStackManager - TDD Suite (DEPRECATED - uses removed methods)', () => {
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');
});
});
});