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:
Janus C. H. Knudsen 2025-10-30 21:46:38 +01:00
parent a80e4a7603
commit 10cb3792f4
17 changed files with 531 additions and 2016 deletions

View file

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

View file

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