Updates the all-day event layout engine for better event rendering, especially when dealing with partial week views. The layout engine now correctly clips events that start before or end after the visible date range, ensuring that only relevant portions of events are displayed. It also fixes event ordering. Includes new unit tests to validate date range filtering and clipping logic.
270 lines
No EOL
13 KiB
TypeScript
270 lines
No EOL
13 KiB
TypeScript
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: [
|
|
{ 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)
|
|
]
|
|
},
|
|
|
|
{
|
|
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)
|
|
{ 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)
|
|
]
|
|
},
|
|
|
|
{
|
|
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: [
|
|
{ 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)
|
|
{ id: '143', gridArea: '1 / 5 / 2 / 8' }, // Weekend Hackathon: row 1, columns 5-7 (3 dage, no overlap, reuse row 1)
|
|
{ id: '161', gridArea: '4 / 2 / 5 / 6' } // Teknisk Workshop: row 4, columns 2-5 (4 dage, starts 23/9, processed fourth)
|
|
]
|
|
}
|
|
];
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
}); |