Improves all-day event layout calculation
Refactors all-day event rendering to use a layout engine for overlap detection and positioning, ensuring events are placed in available rows and columns. Removes deprecated method and adds unit tests.
This commit is contained in:
parent
274753936e
commit
a624394ffb
11 changed files with 2898 additions and 145 deletions
138
test/managers/AllDayLayoutEngine.test.ts
Normal file
138
test/managers/AllDayLayoutEngine.test.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
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: '2 / 1 / 3 / 3' }, // row 2, columns 1-2 (2 dage, processed second)
|
||||
{ id: 'workshop', gridArea: '1 / 2 / 2 / 6' } // row 1, columns 2-5 (4 dage, processed first)
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
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: '3 / 2 / 4 / 4' }, // row 3, columns 2-3 (2 dage, processed last)
|
||||
{ id: '3', gridArea: '2 / 3 / 3 / 6' } // row 2, columns 3-5 (3 dage, processed second)
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
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: '4 / 1 / 5 / 3' }, // Autumn Equinox: row 4, columns 1-2 (2 dage, processed last)
|
||||
{ id: '122', gridArea: '1 / 1 / 2 / 4' }, // Multi-Day Conference: row 1, columns 1-3 (4 dage, starts 21/9, processed first)
|
||||
{ id: '123', gridArea: '2 / 1 / 3 / 5' }, // Project Sprint: row 2, columns 1-4 (4 dage, starts 22/9, processed second)
|
||||
{ id: '143', gridArea: '1 / 5 / 2 / 8' }, // Weekend Hackathon: row 1, columns 5-7 (3 dage, no overlap, reuse row 1)
|
||||
{ id: '161', gridArea: '3 / 2 / 4 / 6' } // Teknisk Workshop: row 3, columns 2-5 (4 dage, starts 23/9, processed third)
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
134
test/managers/AllDayManager.test.ts
Normal file
134
test/managers/AllDayManager.test.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { AllDayManager } from '../../src/managers/AllDayManager';
|
||||
import { setupMockDOM, createMockEvent } from '../helpers/dom-helpers';
|
||||
|
||||
describe('AllDayManager - Layout Calculation', () => {
|
||||
let allDayManager: AllDayManager;
|
||||
|
||||
beforeEach(() => {
|
||||
setupMockDOM();
|
||||
allDayManager = new AllDayManager();
|
||||
});
|
||||
|
||||
describe('Overlap Detection Scenarios', () => {
|
||||
it('Scenario 1: Non-overlapping single-day events', () => {
|
||||
// Event 1: Sept 22 (column 1)
|
||||
const event1 = createMockEvent('1', 'Event 1', '2024-09-22', '2024-09-22');
|
||||
const layout1 = allDayManager.calculateAllDayEventLayout(event1);
|
||||
|
||||
expect(layout1.startColumn).toBe(1);
|
||||
expect(layout1.row).toBe(1);
|
||||
|
||||
// Event 2: Sept 24 (column 3) - different column, should be row 1
|
||||
const event2 = createMockEvent('2', 'Event 2', '2024-09-24', '2024-09-24');
|
||||
const layout2 = allDayManager.calculateAllDayEventLayout(event2);
|
||||
|
||||
expect(layout2.startColumn).toBe(3);
|
||||
expect(layout2.row).toBe(1);
|
||||
});
|
||||
|
||||
it('Scenario 2: Overlapping multi-day events - Autumn Equinox vs Teknisk Workshop', () => {
|
||||
// Autumn Equinox: Sept 22-23 (columns 1-2)
|
||||
const autumnEvent = createMockEvent('autumn', 'Autumn Equinox', '2024-09-22', '2024-09-23');
|
||||
const autumnLayout = allDayManager.calculateAllDayEventLayout(autumnEvent);
|
||||
|
||||
expect(autumnLayout.startColumn).toBe(1);
|
||||
expect(autumnLayout.endColumn).toBe(2);
|
||||
expect(autumnLayout.row).toBe(1);
|
||||
|
||||
// Teknisk Workshop: Sept 23-26 (columns 2-5) - overlaps on Sept 23
|
||||
const workshopEvent = createMockEvent('workshop', 'Teknisk Workshop', '2024-09-23', '2024-09-26');
|
||||
const workshopLayout = allDayManager.calculateAllDayEventLayout(workshopEvent);
|
||||
|
||||
expect(workshopLayout.startColumn).toBe(2);
|
||||
expect(workshopLayout.endColumn).toBe(5);
|
||||
expect(workshopLayout.row).toBe(2); // Should be row 2 due to overlap
|
||||
});
|
||||
|
||||
it('Scenario 3: Multiple events in same column', () => {
|
||||
// Event 1: Sept 23 only
|
||||
const event1 = createMockEvent('1', 'Event 1', '2024-09-23', '2024-09-23');
|
||||
const layout1 = allDayManager.calculateAllDayEventLayout(event1);
|
||||
|
||||
expect(layout1.startColumn).toBe(2);
|
||||
expect(layout1.row).toBe(1);
|
||||
|
||||
// Event 2: Sept 23 only - same column, should be row 2
|
||||
const event2 = createMockEvent('2', 'Event 2', '2024-09-23', '2024-09-23');
|
||||
const layout2 = allDayManager.calculateAllDayEventLayout(event2);
|
||||
|
||||
expect(layout2.startColumn).toBe(2);
|
||||
expect(layout2.row).toBe(2);
|
||||
|
||||
// Event 3: Sept 23 only - same column, should be row 3
|
||||
const event3 = createMockEvent('3', 'Event 3', '2024-09-23', '2024-09-23');
|
||||
const layout3 = allDayManager.calculateAllDayEventLayout(event3);
|
||||
|
||||
expect(layout3.startColumn).toBe(2);
|
||||
expect(layout3.row).toBe(3);
|
||||
});
|
||||
|
||||
it('Scenario 4: Partial overlaps', () => {
|
||||
// Event 1: Sept 22-23 (columns 1-2)
|
||||
const event1 = createMockEvent('1', 'Event 1', '2024-09-22', '2024-09-23');
|
||||
const layout1 = allDayManager.calculateAllDayEventLayout(event1);
|
||||
|
||||
expect(layout1.startColumn).toBe(1);
|
||||
expect(layout1.endColumn).toBe(2);
|
||||
expect(layout1.row).toBe(1);
|
||||
|
||||
// Event 2: Sept 23-24 (columns 2-3) - overlaps on Sept 23
|
||||
const event2 = createMockEvent('2', 'Event 2', '2024-09-23', '2024-09-24');
|
||||
const layout2 = allDayManager.calculateAllDayEventLayout(event2);
|
||||
|
||||
expect(layout2.startColumn).toBe(2);
|
||||
expect(layout2.endColumn).toBe(3);
|
||||
expect(layout2.row).toBe(2); // Should be row 2 due to overlap
|
||||
|
||||
// Event 3: Sept 25-26 (columns 4-5) - no overlap, should be row 1
|
||||
const event3 = createMockEvent('3', 'Event 3', '2024-09-25', '2024-09-26');
|
||||
const layout3 = allDayManager.calculateAllDayEventLayout(event3);
|
||||
|
||||
expect(layout3.startColumn).toBe(4);
|
||||
expect(layout3.endColumn).toBe(5);
|
||||
expect(layout3.row).toBe(1); // No overlap, back to row 1
|
||||
});
|
||||
|
||||
it('Scenario 5: Complex overlapping pattern', () => {
|
||||
// Event 1: Sept 22-25 (columns 1-4) - spans most of week
|
||||
const event1 = createMockEvent('1', 'Long Event', '2024-09-22', '2024-09-25');
|
||||
const layout1 = allDayManager.calculateAllDayEventLayout(event1);
|
||||
|
||||
expect(layout1.startColumn).toBe(1);
|
||||
expect(layout1.endColumn).toBe(4);
|
||||
expect(layout1.row).toBe(1);
|
||||
|
||||
// Event 2: Sept 23-24 (columns 2-3) - overlaps with Event 1
|
||||
const event2 = createMockEvent('2', 'Short Event', '2024-09-23', '2024-09-24');
|
||||
const layout2 = allDayManager.calculateAllDayEventLayout(event2);
|
||||
|
||||
expect(layout2.startColumn).toBe(2);
|
||||
expect(layout2.endColumn).toBe(3);
|
||||
expect(layout2.row).toBe(2);
|
||||
|
||||
// Event 3: Sept 24-26 (columns 3-5) - overlaps with both
|
||||
const event3 = createMockEvent('3', 'Another Event', '2024-09-24', '2024-09-26');
|
||||
const layout3 = allDayManager.calculateAllDayEventLayout(event3);
|
||||
|
||||
expect(layout3.startColumn).toBe(3);
|
||||
expect(layout3.endColumn).toBe(5);
|
||||
expect(layout3.row).toBe(3); // Should be row 3 due to overlaps with both
|
||||
});
|
||||
|
||||
it('Scenario 6: Drag-drop target date override', () => {
|
||||
// Multi-day event dragged to specific date should use single column
|
||||
const event = createMockEvent('drag', 'Dragged Event', '2024-09-22', '2024-09-25');
|
||||
const layout = allDayManager.calculateAllDayEventLayout(event, '2024-09-24');
|
||||
|
||||
expect(layout.startColumn).toBe(3); // Sept 24 is column 3
|
||||
expect(layout.endColumn).toBe(3); // Single column when targetDate specified
|
||||
expect(layout.columnSpan).toBe(1);
|
||||
expect(layout.row).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
13
test/setup.ts
Normal file
13
test/setup.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { beforeEach } from 'vitest';
|
||||
|
||||
// Global test setup
|
||||
beforeEach(() => {
|
||||
// Clear DOM before each test
|
||||
document.body.innerHTML = '';
|
||||
document.head.innerHTML = '';
|
||||
|
||||
// Reset any global state
|
||||
if (typeof window !== 'undefined') {
|
||||
// Clear any event listeners or global variables if needed
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue