Updates build config and removes unused dependencies
Migrates from Brandi DI to @novadi/core dependency injection Simplifies project structure by removing deprecated modules Adds Novadi unplugin to esbuild configuration for enhanced build process
This commit is contained in:
parent
a80e4a7603
commit
10cb3792f4
17 changed files with 531 additions and 2016 deletions
|
|
@ -1,656 +0,0 @@
|
|||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { DateService } from '../../src/utils/DateService';
|
||||
|
||||
describe('DateService - Midnight Crossing & Multi-Day Events', () => {
|
||||
const dateService = new DateService('Europe/Copenhagen');
|
||||
|
||||
describe('Midnight Crossing Events', () => {
|
||||
it('should handle event starting before midnight and ending after', () => {
|
||||
const start = new Date(2024, 0, 15, 23, 30); // Jan 15, 23:30
|
||||
const end = new Date(2024, 0, 16, 1, 30); // Jan 16, 01:30
|
||||
|
||||
expect(dateService.isMultiDay(start, end)).toBe(true);
|
||||
expect(dateService.isSameDay(start, end)).toBe(false);
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(120); // 2 hours
|
||||
});
|
||||
|
||||
it('should calculate duration correctly across midnight', () => {
|
||||
const start = new Date(2024, 0, 15, 22, 0); // 22:00
|
||||
const end = new Date(2024, 0, 16, 2, 0); // 02:00 next day
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(240); // 4 hours
|
||||
});
|
||||
|
||||
it('should handle event ending exactly at midnight', () => {
|
||||
const start = new Date(2024, 0, 15, 20, 0); // 20:00
|
||||
const end = new Date(2024, 0, 16, 0, 0); // 00:00 (midnight)
|
||||
|
||||
expect(dateService.isMultiDay(start, end)).toBe(true);
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(240); // 4 hours
|
||||
});
|
||||
|
||||
it('should handle event starting exactly at midnight', () => {
|
||||
const start = new Date(2024, 0, 15, 0, 0); // 00:00 (midnight)
|
||||
const end = new Date(2024, 0, 15, 3, 0); // 03:00 same day
|
||||
|
||||
expect(dateService.isMultiDay(start, end)).toBe(false);
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(180); // 3 hours
|
||||
});
|
||||
|
||||
it('should create date at specific time correctly across midnight', () => {
|
||||
const baseDate = new Date(2024, 0, 15);
|
||||
|
||||
// 1440 minutes = 24:00 = midnight next day
|
||||
const midnightNextDay = dateService.createDateAtTime(baseDate, 1440);
|
||||
expect(midnightNextDay.getDate()).toBe(16);
|
||||
expect(midnightNextDay.getHours()).toBe(0);
|
||||
expect(midnightNextDay.getMinutes()).toBe(0);
|
||||
|
||||
// 1500 minutes = 25:00 = 01:00 next day
|
||||
const oneAmNextDay = dateService.createDateAtTime(baseDate, 1500);
|
||||
expect(oneAmNextDay.getDate()).toBe(16);
|
||||
expect(oneAmNextDay.getHours()).toBe(1);
|
||||
expect(oneAmNextDay.getMinutes()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi-Day Events', () => {
|
||||
it('should detect 2-day event', () => {
|
||||
const start = new Date(2024, 0, 15, 10, 0);
|
||||
const end = new Date(2024, 0, 16, 14, 0);
|
||||
|
||||
expect(dateService.isMultiDay(start, end)).toBe(true);
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(28 * 60); // 28 hours
|
||||
});
|
||||
|
||||
it('should detect 3-day event', () => {
|
||||
const start = new Date(2024, 0, 15, 9, 0);
|
||||
const end = new Date(2024, 0, 17, 17, 0);
|
||||
|
||||
expect(dateService.isMultiDay(start, end)).toBe(true);
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(56 * 60); // 56 hours
|
||||
});
|
||||
|
||||
it('should detect week-long event', () => {
|
||||
const start = new Date(2024, 0, 15, 0, 0);
|
||||
const end = new Date(2024, 0, 22, 0, 0);
|
||||
|
||||
expect(dateService.isMultiDay(start, end)).toBe(true);
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(7 * 24 * 60); // 7 days
|
||||
});
|
||||
|
||||
it('should handle month-spanning multi-day event', () => {
|
||||
const start = new Date(2024, 0, 30, 12, 0); // Jan 30
|
||||
const end = new Date(2024, 1, 2, 12, 0); // Feb 2
|
||||
|
||||
expect(dateService.isMultiDay(start, end)).toBe(true);
|
||||
expect(start.getMonth()).toBe(0); // January
|
||||
expect(end.getMonth()).toBe(1); // February
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(3 * 24 * 60); // 3 days
|
||||
});
|
||||
|
||||
it('should handle year-spanning multi-day event', () => {
|
||||
const start = new Date(2024, 11, 30, 10, 0); // Dec 30, 2024
|
||||
const end = new Date(2025, 0, 2, 10, 0); // Jan 2, 2025
|
||||
|
||||
expect(dateService.isMultiDay(start, end)).toBe(true);
|
||||
expect(start.getFullYear()).toBe(2024);
|
||||
expect(end.getFullYear()).toBe(2025);
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(3 * 24 * 60); // 3 days
|
||||
});
|
||||
});
|
||||
|
||||
describe('Timezone Boundary Events', () => {
|
||||
it('should handle UTC to local timezone conversion across midnight', () => {
|
||||
// Event in UTC that crosses date boundary in local timezone
|
||||
const utcStart = '2024-01-15T23:00:00Z'; // 23:00 UTC
|
||||
const utcEnd = '2024-01-16T01:00:00Z'; // 01:00 UTC next day
|
||||
|
||||
const localStart = dateService.fromUTC(utcStart);
|
||||
const localEnd = dateService.fromUTC(utcEnd);
|
||||
|
||||
// Copenhagen is UTC+1 (or UTC+2 in summer)
|
||||
// So 23:00 UTC = 00:00 or 01:00 local (midnight crossing)
|
||||
expect(localStart.getDate()).toBeGreaterThanOrEqual(15);
|
||||
expect(localEnd.getDate()).toBeGreaterThanOrEqual(16);
|
||||
|
||||
const duration = dateService.getDurationMinutes(localStart, localEnd);
|
||||
expect(duration).toBe(120); // 2 hours
|
||||
});
|
||||
|
||||
it('should preserve duration when converting UTC to local', () => {
|
||||
const utcStart = '2024-06-15T10:00:00Z';
|
||||
const utcEnd = '2024-06-15T18:00:00Z';
|
||||
|
||||
const localStart = dateService.fromUTC(utcStart);
|
||||
const localEnd = dateService.fromUTC(utcEnd);
|
||||
|
||||
const utcDuration = 8 * 60; // 8 hours
|
||||
const localDuration = dateService.getDurationMinutes(localStart, localEnd);
|
||||
|
||||
expect(localDuration).toBe(utcDuration);
|
||||
});
|
||||
|
||||
it('should handle all-day events (00:00 to 00:00 next day)', () => {
|
||||
const start = new Date(2024, 0, 15, 0, 0, 0);
|
||||
const end = new Date(2024, 0, 16, 0, 0, 0);
|
||||
|
||||
expect(dateService.isMultiDay(start, end)).toBe(true);
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(24 * 60); // 24 hours
|
||||
});
|
||||
|
||||
it('should handle multi-day all-day events', () => {
|
||||
const start = new Date(2024, 0, 15, 0, 0, 0);
|
||||
const end = new Date(2024, 0, 18, 0, 0, 0); // 3-day event
|
||||
|
||||
expect(dateService.isMultiDay(start, end)).toBe(true);
|
||||
|
||||
const duration = dateService.getDurationMinutes(start, end);
|
||||
expect(duration).toBe(3 * 24 * 60); // 72 hours
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases with Minutes Since Midnight', () => {
|
||||
it('should calculate minutes since midnight correctly at day boundary', () => {
|
||||
const midnight = new Date(2024, 0, 15, 0, 0);
|
||||
const beforeMidnight = new Date(2024, 0, 14, 23, 59);
|
||||
const afterMidnight = new Date(2024, 0, 15, 0, 1);
|
||||
|
||||
expect(dateService.getMinutesSinceMidnight(midnight)).toBe(0);
|
||||
expect(dateService.getMinutesSinceMidnight(beforeMidnight)).toBe(23 * 60 + 59);
|
||||
expect(dateService.getMinutesSinceMidnight(afterMidnight)).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle createDateAtTime with overflow minutes (>1440)', () => {
|
||||
const baseDate = new Date(2024, 0, 15);
|
||||
|
||||
// 1500 minutes = 25 hours = next day at 01:00
|
||||
const result = dateService.createDateAtTime(baseDate, 1500);
|
||||
|
||||
expect(result.getDate()).toBe(16); // Next day
|
||||
expect(result.getHours()).toBe(1);
|
||||
expect(result.getMinutes()).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle createDateAtTime with large overflow (48+ hours)', () => {
|
||||
const baseDate = new Date(2024, 0, 15);
|
||||
|
||||
// 2880 minutes = 48 hours = 2 days later
|
||||
const result = dateService.createDateAtTime(baseDate, 2880);
|
||||
|
||||
expect(result.getDate()).toBe(17); // 2 days later
|
||||
expect(result.getHours()).toBe(0);
|
||||
expect(result.getMinutes()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Same Day vs Multi-Day Detection', () => {
|
||||
it('should correctly identify same-day events', () => {
|
||||
const start = new Date(2024, 0, 15, 8, 0);
|
||||
const end = new Date(2024, 0, 15, 17, 0);
|
||||
|
||||
expect(dateService.isSameDay(start, end)).toBe(true);
|
||||
expect(dateService.isMultiDay(start, end)).toBe(false);
|
||||
});
|
||||
|
||||
it('should correctly identify multi-day events', () => {
|
||||
const start = new Date(2024, 0, 15, 23, 0);
|
||||
const end = new Date(2024, 0, 16, 1, 0);
|
||||
|
||||
expect(dateService.isSameDay(start, end)).toBe(false);
|
||||
expect(dateService.isMultiDay(start, end)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle ISO string inputs for multi-day detection', () => {
|
||||
const startISO = '2024-01-15T23:00:00Z';
|
||||
const endISO = '2024-01-16T01:00:00Z';
|
||||
|
||||
// Convert UTC strings to local timezone first
|
||||
const startLocal = dateService.fromUTC(startISO);
|
||||
const endLocal = dateService.fromUTC(endISO);
|
||||
|
||||
const result = dateService.isMultiDay(startLocal, endLocal);
|
||||
|
||||
// 23:00 UTC = 00:00 CET (next day) in Copenhagen
|
||||
// So this IS a multi-day event in local time
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle mixed Date and string inputs', () => {
|
||||
const startDate = new Date(2024, 0, 15, 10, 0);
|
||||
const endISO = '2024-01-16T10:00:00Z';
|
||||
|
||||
const result = dateService.isMultiDay(startDate, endISO);
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue