2025-09-25 23:38:17 +02:00
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
|
import { AllDayLayoutEngine } from '../../src/utils/AllDayLayoutEngine';
|
|
|
|
|
import { CalendarEvent } from '../../src/types/CalendarTypes';
|
|
|
|
|
|
|
|
|
|
describe('AllDay Layout Engine - Pure Data Tests', () => {
|
|
|
|
|
const weekDates = [
|
|
|
|
|
'2025-09-22', '2025-09-23', '2025-09-24', '2025-09-25',
|
|
|
|
|
'2025-09-26', '2025-09-27', '2025-09-28'
|
|
|
|
|
];
|
|
|
|
|
const layoutEngine = new AllDayLayoutEngine(weekDates);
|
|
|
|
|
|
|
|
|
|
// Test data: events med start/end datoer og forventet grid-area
|
|
|
|
|
const testCases = [
|
|
|
|
|
{
|
|
|
|
|
name: 'Single day events - no overlap',
|
|
|
|
|
events: [
|
|
|
|
|
{ id: '1', title: 'Event 1', start: new Date('2025-09-22'), end: new Date('2025-09-22'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '2', title: 'Event 2', start: new Date('2025-09-24'), end: new Date('2025-09-24'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
|
|
|
|
],
|
|
|
|
|
expected: [
|
|
|
|
|
{ id: '1', gridArea: '1 / 1 / 2 / 2' }, // row 1, column 1 (Sept 22)
|
|
|
|
|
{ id: '2', gridArea: '1 / 3 / 2 / 4' } // row 1, column 3 (Sept 24)
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
name: 'Overlapping multi-day events - Autumn Equinox vs Teknisk Workshop',
|
|
|
|
|
events: [
|
|
|
|
|
{ id: 'autumn', title: 'Autumn Equinox', start: new Date('2025-09-22'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: 'workshop', title: 'Teknisk Workshop', start: new Date('2025-09-23'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
|
|
|
|
],
|
|
|
|
|
expected: [
|
2025-09-26 17:47:02 +02:00
|
|
|
{ id: 'autumn', gridArea: '1 / 1 / 2 / 3' }, // row 1, columns 1-2 (2 dage, processed first)
|
|
|
|
|
{ id: 'workshop', gridArea: '2 / 2 / 3 / 6' } // row 2, columns 2-5 (4 dage, processed second)
|
2025-09-25 23:38:17 +02:00
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
name: 'Multiple events same column',
|
|
|
|
|
events: [
|
|
|
|
|
{ id: '1', title: 'Event 1', start: new Date('2025-09-23'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '2', title: 'Event 2', start: new Date('2025-09-23'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '3', title: 'Event 3', start: new Date('2025-09-23'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
|
|
|
|
],
|
|
|
|
|
expected: [
|
|
|
|
|
{ id: '1', gridArea: '1 / 2 / 2 / 3' }, // row 1, column 2 (Sept 23)
|
|
|
|
|
{ id: '2', gridArea: '2 / 2 / 3 / 3' }, // row 2, column 2 (Sept 23)
|
|
|
|
|
{ id: '3', gridArea: '3 / 2 / 4 / 3' } // row 3, column 2 (Sept 23)
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
name: 'Partial overlaps',
|
|
|
|
|
events: [
|
|
|
|
|
{ id: '1', title: 'Event 1', start: new Date('2025-09-22'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '2', title: 'Event 2', start: new Date('2025-09-23'), end: new Date('2025-09-24'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '3', title: 'Event 3', start: new Date('2025-09-25'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
|
|
|
|
],
|
|
|
|
|
expected: [
|
|
|
|
|
{ id: '1', gridArea: '1 / 1 / 2 / 3' }, // row 1, columns 1-2 (Sept 22-23)
|
|
|
|
|
{ id: '2', gridArea: '2 / 2 / 3 / 4' }, // row 2, columns 2-3 (Sept 23-24, overlap på column 2)
|
|
|
|
|
{ id: '3', gridArea: '1 / 4 / 2 / 6' } // row 1, columns 4-5 (Sept 25-26, no overlap)
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
name: 'Complex overlapping pattern',
|
|
|
|
|
events: [
|
|
|
|
|
{ id: '1', title: 'Long Event', start: new Date('2025-09-22'), end: new Date('2025-09-25'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '2', title: 'Short Event', start: new Date('2025-09-23'), end: new Date('2025-09-24'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '3', title: 'Another Event', start: new Date('2025-09-24'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
|
|
|
|
],
|
|
|
|
|
expected: [
|
|
|
|
|
{ id: '1', gridArea: '1 / 1 / 2 / 5' }, // row 1, columns 1-4 (4 dage, processed first)
|
2025-09-26 17:47:02 +02:00
|
|
|
{ id: '2', gridArea: '2 / 2 / 3 / 4' }, // row 2, columns 2-3 (2 dage, processed second)
|
|
|
|
|
{ id: '3', gridArea: '3 / 3 / 4 / 6' } // row 3, columns 3-5 (3 dage, processed third)
|
2025-09-25 23:38:17 +02:00
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
name: 'Real-world bug scenario - Multiple overlapping events (Sept 21-28)',
|
|
|
|
|
events: [
|
|
|
|
|
{ id: '112', title: 'Autumn Equinox', start: new Date('2025-09-22'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '122', title: 'Multi-Day Conference', start: new Date('2025-09-21'), end: new Date('2025-09-24'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '123', title: 'Project Sprint', start: new Date('2025-09-22'), end: new Date('2025-09-25'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '143', title: 'Weekend Hackathon', start: new Date('2025-09-26'), end: new Date('2025-09-28'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
|
|
|
|
{ id: '161', title: 'Teknisk Workshop', start: new Date('2025-09-23'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
|
|
|
|
],
|
|
|
|
|
expected: [
|
2025-09-26 17:47:02 +02:00
|
|
|
{ id: '112', gridArea: '1 / 1 / 2 / 3' }, // Autumn Equinox: row 1, columns 1-2 (2 dage, processed first)
|
|
|
|
|
{ id: '122', gridArea: '2 / 1 / 3 / 4' }, // Multi-Day Conference: row 2, columns 1-3 (4 dage, starts 21/9, processed second)
|
|
|
|
|
{ id: '123', gridArea: '3 / 1 / 4 / 5' }, // Project Sprint: row 3, columns 1-4 (4 dage, starts 22/9, processed third)
|
2025-09-25 23:38:17 +02:00
|
|
|
{ id: '143', gridArea: '1 / 5 / 2 / 8' }, // Weekend Hackathon: row 1, columns 5-7 (3 dage, no overlap, reuse row 1)
|
2025-09-26 17:47:02 +02:00
|
|
|
{ id: '161', gridArea: '4 / 2 / 5 / 6' } // Teknisk Workshop: row 4, columns 2-5 (4 dage, starts 23/9, processed fourth)
|
2025-09-25 23:38:17 +02:00
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
testCases.forEach(testCase => {
|
|
|
|
|
it(testCase.name, () => {
|
|
|
|
|
// Calculate actual layouts using AllDayLayoutEngine
|
|
|
|
|
const layouts = layoutEngine.calculateLayout(testCase.events);
|
|
|
|
|
|
|
|
|
|
// Verify we got layouts for all events
|
|
|
|
|
expect(layouts.size).toBe(testCase.events.length);
|
|
|
|
|
|
|
|
|
|
// Check each expected result
|
|
|
|
|
testCase.expected.forEach(expected => {
|
|
|
|
|
const actualLayout = layouts.get(expected.id);
|
|
|
|
|
expect(actualLayout).toBeDefined();
|
|
|
|
|
expect(actualLayout!.gridArea).toBe(expected.gridArea);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Grid-area format validation', () => {
|
|
|
|
|
// Test at grid-area format er korrekt
|
|
|
|
|
const gridArea = '2 / 3 / 3 / 5'; // row 2, columns 3-4
|
|
|
|
|
const parts = gridArea.split(' / ');
|
|
|
|
|
|
|
|
|
|
const rowStart = parseInt(parts[0]); // 2
|
|
|
|
|
const colStart = parseInt(parts[1]); // 3
|
|
|
|
|
const rowEnd = parseInt(parts[2]); // 3
|
|
|
|
|
const colEnd = parseInt(parts[3]); // 5
|
|
|
|
|
|
|
|
|
|
expect(rowStart).toBe(2);
|
|
|
|
|
expect(colStart).toBe(3);
|
|
|
|
|
expect(rowEnd).toBe(3);
|
|
|
|
|
expect(colEnd).toBe(5);
|
|
|
|
|
|
|
|
|
|
// Verify spans
|
|
|
|
|
const rowSpan = rowEnd - rowStart; // 1 row
|
|
|
|
|
const colSpan = colEnd - colStart; // 2 columns
|
|
|
|
|
|
|
|
|
|
expect(rowSpan).toBe(1);
|
|
|
|
|
expect(colSpan).toBe(2);
|
|
|
|
|
});
|
2025-09-26 17:47:02 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('AllDay Layout Engine - Partial Week Views', () => {
|
|
|
|
|
describe('Date Range Filtering', () => {
|
|
|
|
|
it('should filter out events that do not overlap with visible dates', () => {
|
|
|
|
|
// 3-day workweek: Wed-Fri (like user's scenario)
|
|
|
|
|
const weekDates = ['2025-09-24', '2025-09-25', '2025-09-26']; // Wed, Thu, Fri
|
|
|
|
|
const engine = new AllDayLayoutEngine(weekDates);
|
|
|
|
|
|
|
|
|
|
const events: CalendarEvent[] = [
|
|
|
|
|
{
|
|
|
|
|
id: '112',
|
|
|
|
|
title: 'Autumn Equinox',
|
|
|
|
|
start: new Date('2025-09-22T00:00:00'), // Monday - OUTSIDE visible range
|
|
|
|
|
end: new Date('2025-09-24T00:00:00'), // Wednesday - OUTSIDE visible range
|
|
|
|
|
type: 'milestone',
|
|
|
|
|
allDay: true,
|
|
|
|
|
syncStatus: 'synced'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '113',
|
|
|
|
|
title: 'Visible Event',
|
|
|
|
|
start: new Date('2025-09-25T00:00:00'), // Thursday - INSIDE visible range
|
|
|
|
|
end: new Date('2025-09-26T00:00:00'), // Friday - INSIDE visible range
|
|
|
|
|
type: 'work',
|
|
|
|
|
allDay: true,
|
|
|
|
|
syncStatus: 'synced'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const layouts = engine.calculateLayout(events);
|
|
|
|
|
|
|
|
|
|
// Both events are now visible since '112' ends on Wednesday (visible range start)
|
|
|
|
|
expect(layouts.size).toBe(2);
|
|
|
|
|
expect(layouts.has('112')).toBe(true); // Now visible since it ends on Wed
|
|
|
|
|
expect(layouts.has('113')).toBe(true); // Still visible
|
|
|
|
|
|
|
|
|
|
const layout112 = layouts.get('112')!;
|
|
|
|
|
expect(layout112.startColumn).toBe(1); // Clipped to Wed (first visible day)
|
|
|
|
|
expect(layout112.endColumn).toBe(1); // Wed only
|
|
|
|
|
expect(layout112.row).toBe(1);
|
|
|
|
|
|
|
|
|
|
const layout113 = layouts.get('113')!;
|
|
|
|
|
expect(layout113.startColumn).toBe(2); // Thursday = column 2 in Wed-Fri view
|
|
|
|
|
expect(layout113.endColumn).toBe(3); // Friday = column 3
|
|
|
|
|
expect(layout113.row).toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should clip events that partially overlap with visible dates', () => {
|
|
|
|
|
// 3-day workweek: Wed-Fri
|
|
|
|
|
const weekDates = ['2025-09-24', '2025-09-25', '2025-09-26']; // Wed, Thu, Fri
|
|
|
|
|
const engine = new AllDayLayoutEngine(weekDates);
|
|
|
|
|
|
|
|
|
|
const events: CalendarEvent[] = [
|
|
|
|
|
{
|
|
|
|
|
id: '114',
|
|
|
|
|
title: 'Spans Before and Into Week',
|
|
|
|
|
start: new Date('2025-09-22T00:00:00'), // Monday - before visible range
|
|
|
|
|
end: new Date('2025-09-26T00:00:00'), // Friday - inside visible range
|
|
|
|
|
type: 'work',
|
|
|
|
|
allDay: true,
|
|
|
|
|
syncStatus: 'synced'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '115',
|
|
|
|
|
title: 'Spans From Week and After',
|
|
|
|
|
start: new Date('2025-09-25T00:00:00'), // Thursday - inside visible range
|
|
|
|
|
end: new Date('2025-09-29T00:00:00'), // Monday - after visible range
|
|
|
|
|
type: 'work',
|
|
|
|
|
allDay: true,
|
|
|
|
|
syncStatus: 'synced'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const layouts = engine.calculateLayout(events);
|
|
|
|
|
|
|
|
|
|
expect(layouts.size).toBe(2);
|
|
|
|
|
|
|
|
|
|
// First event should be clipped to start at Wed (column 1) and end at Fri (column 3)
|
|
|
|
|
const firstLayout = layouts.get('114')!;
|
|
|
|
|
expect(firstLayout.startColumn).toBe(1); // Clipped to Wed (first visible day)
|
|
|
|
|
expect(firstLayout.endColumn).toBe(3); // Fri (now ends on Friday due to 2025-09-26T00:00:00)
|
|
|
|
|
expect(firstLayout.columnSpan).toBe(3);
|
|
|
|
|
expect(firstLayout.gridArea).toBe('1 / 1 / 2 / 4');
|
|
|
|
|
|
|
|
|
|
// Second event should span Thu-Fri, but clipped beyond visible range
|
|
|
|
|
const secondLayout = layouts.get('115')!;
|
|
|
|
|
expect(secondLayout.startColumn).toBe(2); // Thu (actual start date) = column 2 in Wed-Fri view
|
|
|
|
|
expect(secondLayout.endColumn).toBe(3); // Clipped to Fri (last visible day) = column 3
|
|
|
|
|
expect(secondLayout.columnSpan).toBe(2);
|
|
|
|
|
expect(secondLayout.gridArea).toBe('2 / 2 / 3 / 4'); // Row 2 due to overlap
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle 5-day workweek correctly', () => {
|
|
|
|
|
// 5-day workweek: Mon-Fri
|
|
|
|
|
const weekDates = ['2025-09-22', '2025-09-23', '2025-09-24', '2025-09-25', '2025-09-26']; // Mon-Fri
|
|
|
|
|
const engine = new AllDayLayoutEngine(weekDates);
|
|
|
|
|
|
|
|
|
|
const events: CalendarEvent[] = [
|
|
|
|
|
{
|
|
|
|
|
id: '116',
|
|
|
|
|
title: 'Monday Event',
|
|
|
|
|
start: new Date('2025-09-22T00:00:00'), // Monday
|
|
|
|
|
end: new Date('2025-09-23T00:00:00'), // Tuesday
|
|
|
|
|
type: 'work',
|
|
|
|
|
allDay: true,
|
|
|
|
|
syncStatus: 'synced'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '117',
|
|
|
|
|
title: 'Weekend Event',
|
|
|
|
|
start: new Date('2025-09-27T00:00:00'), // Saturday - OUTSIDE visible range
|
|
|
|
|
end: new Date('2025-09-29T00:00:00'), // Monday - OUTSIDE visible range
|
|
|
|
|
type: 'personal',
|
|
|
|
|
allDay: true,
|
|
|
|
|
syncStatus: 'synced'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const layouts = engine.calculateLayout(events);
|
|
|
|
|
|
|
|
|
|
expect(layouts.size).toBe(1); // Only Monday event should be included - weekend event should be filtered out
|
|
|
|
|
expect(layouts.has('116')).toBe(true); // Monday event should be included
|
|
|
|
|
expect(layouts.has('117')).toBe(false); // Weekend event should be filtered out
|
|
|
|
|
|
|
|
|
|
const mondayLayout = layouts.get('116')!;
|
|
|
|
|
expect(mondayLayout.startColumn).toBe(1); // Monday = column 1
|
|
|
|
|
expect(mondayLayout.endColumn).toBe(2); // Now ends on Tuesday due to 2025-09-23T00:00:00
|
|
|
|
|
expect(mondayLayout.row).toBe(1);
|
|
|
|
|
expect(mondayLayout.gridArea).toBe('1 / 1 / 2 / 3');
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-09-25 23:38:17 +02:00
|
|
|
});
|