import { describe, it, expect } from 'vitest'; import { calculateLayout, Event } from '../layoutAlgorithm'; describe('calculateLayout', () => { describe('basic cases', () => { it('should return empty array for empty input', () => { const result = calculateLayout([]); expect(result).toEqual([]); }); it('should assign laneIndex 0 to a single event', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', serviceName: 'Service A', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), }, ]; const result = calculateLayout(events); expect(result).toHaveLength(1); expect(result[0].laneIndex).toBe(0); }); it('should preserve all event properties', () => { const events: Event[] = [ { id: 1, resourceId: 2, title: 'Haircut', serviceName: 'Basic Haircut', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), status: 'CONFIRMED', isPaid: true, }, ]; const result = calculateLayout(events); expect(result[0]).toMatchObject({ id: 1, resourceId: 2, title: 'Haircut', serviceName: 'Basic Haircut', status: 'CONFIRMED', isPaid: true, }); expect(result[0].start).toEqual(events[0].start); expect(result[0].end).toEqual(events[0].end); }); }); describe('non-overlapping events', () => { it('should assign laneIndex 0 to all non-overlapping sequential events', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T09:00:00'), end: new Date('2025-01-01T10:00:00'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), }, { id: 3, resourceId: 1, title: 'Event 3', start: new Date('2025-01-01T11:00:00'), end: new Date('2025-01-01T12:00:00'), }, ]; const result = calculateLayout(events); expect(result).toHaveLength(3); expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(0); expect(result[2].laneIndex).toBe(0); }); it('should assign laneIndex 0 to non-overlapping events with gaps', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T09:00:00'), end: new Date('2025-01-01T10:00:00'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T11:00:00'), end: new Date('2025-01-01T12:00:00'), }, { id: 3, resourceId: 1, title: 'Event 3', start: new Date('2025-01-01T14:00:00'), end: new Date('2025-01-01T15:00:00'), }, ]; const result = calculateLayout(events); expect(result.every(event => event.laneIndex === 0)).toBe(true); }); }); describe('edge case: events ending when another starts', () => { it('should assign same lane when event ends exactly when another starts', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T11:00:00'), end: new Date('2025-01-01T12:00:00'), }, ]; const result = calculateLayout(events); expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(0); // Can reuse lane since end == start }); it('should handle millisecond precision for exact boundaries', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T10:00:00.000Z'), end: new Date('2025-01-01T11:00:00.000Z'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T11:00:00.000Z'), end: new Date('2025-01-01T12:00:00.000Z'), }, ]; const result = calculateLayout(events); expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(0); }); it('should assign different lane when event starts one millisecond before another ends', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T10:00:00.000Z'), end: new Date('2025-01-01T11:00:00.000Z'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T10:59:59.999Z'), end: new Date('2025-01-01T12:00:00.000Z'), }, ]; const result = calculateLayout(events); expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(1); // Overlaps by 1ms }); }); describe('two overlapping events', () => { it('should assign different lanes to overlapping events', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T10:30:00'), end: new Date('2025-01-01T11:30:00'), }, ]; const result = calculateLayout(events); expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(1); }); it('should handle events where one completely contains another', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Long Event', start: new Date('2025-01-01T09:00:00'), end: new Date('2025-01-01T13:00:00'), }, { id: 2, resourceId: 1, title: 'Short Event', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), }, ]; const result = calculateLayout(events); expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(1); }); it('should handle events starting at the same time', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T12:00:00'), }, ]; const result = calculateLayout(events); expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(1); }); }); describe('multiple overlapping events', () => { it('should handle three overlapping events requiring three lanes', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T12:00:00'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T10:30:00'), end: new Date('2025-01-01T12:30:00'), }, { id: 3, resourceId: 1, title: 'Event 3', start: new Date('2025-01-01T11:00:00'), end: new Date('2025-01-01T13:00:00'), }, ]; const result = calculateLayout(events); expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(1); expect(result[2].laneIndex).toBe(2); }); it('should efficiently reuse lanes when earlier events end', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T09:00:00'), end: new Date('2025-01-01T10:00:00'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T09:30:00'), end: new Date('2025-01-01T11:00:00'), }, { id: 3, resourceId: 1, title: 'Event 3', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T12:00:00'), }, ]; const result = calculateLayout(events); expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(1); expect(result[2].laneIndex).toBe(0); // Can reuse lane 0 since event 1 ended }); it('should handle complex pattern with five overlapping events', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T09:00:00'), end: new Date('2025-01-01T15:00:00'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), }, { id: 3, resourceId: 1, title: 'Event 3', start: new Date('2025-01-01T10:30:00'), end: new Date('2025-01-01T12:00:00'), }, { id: 4, resourceId: 1, title: 'Event 4', start: new Date('2025-01-01T11:00:00'), end: new Date('2025-01-01T13:00:00'), }, { id: 5, resourceId: 1, title: 'Event 5', start: new Date('2025-01-01T11:30:00'), end: new Date('2025-01-01T14:00:00'), }, ]; const result = calculateLayout(events); // Event 1 (9-15): lane 0 // Event 2 (10-11): lane 1 // Event 3 (10:30-12): lane 2 (overlaps with 1 and 2) // Event 4 (11-13): lane 1 (can reuse, event 2 ended) // Event 5 (11:30-14): lane 3 (overlaps with 1, 3, and 4) expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(1); expect(result[2].laneIndex).toBe(2); expect(result[3].laneIndex).toBe(1); // Reuses lane 1 expect(result[4].laneIndex).toBe(3); }); }); describe('event ordering', () => { it('should produce same result regardless of input order', () => { const events: Event[] = [ { id: 3, resourceId: 1, title: 'Event 3', start: new Date('2025-01-01T11:00:00'), end: new Date('2025-01-01T12:00:00'), }, { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:30:00'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T10:30:00'), end: new Date('2025-01-01T11:00:00'), }, ]; const result = calculateLayout(events); // Should be sorted by start time internally expect(result[0].id).toBe(1); // Starts at 10:00 expect(result[1].id).toBe(2); // Starts at 10:30 expect(result[2].id).toBe(3); // Starts at 11:00 expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(1); expect(result[2].laneIndex).toBe(1); // Can reuse lane 1 }); it('should handle reverse chronological order', () => { const events: Event[] = [ { id: 3, resourceId: 1, title: 'Event 3', start: new Date('2025-01-01T14:00:00'), end: new Date('2025-01-01T15:00:00'), }, { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T12:00:00'), end: new Date('2025-01-01T13:00:00'), }, { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), }, ]; const result = calculateLayout(events); expect(result[0].id).toBe(1); expect(result[1].id).toBe(2); expect(result[2].id).toBe(3); expect(result.every(event => event.laneIndex === 0)).toBe(true); }); it('should not modify the original events array', () => { const events: Event[] = [ { id: 2, resourceId: 1, title: 'Event 2', start: new Date('2025-01-01T11:00:00'), end: new Date('2025-01-01T12:00:00'), }, { id: 1, resourceId: 1, title: 'Event 1', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), }, ]; const originalOrder = events.map(e => e.id); calculateLayout(events); const afterOrder = events.map(e => e.id); expect(afterOrder).toEqual(originalOrder); }); }); describe('different statuses and properties', () => { it('should handle events with all different statuses', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Pending', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), status: 'PENDING', }, { id: 2, resourceId: 1, title: 'Confirmed', start: new Date('2025-01-01T10:30:00'), end: new Date('2025-01-01T11:30:00'), status: 'CONFIRMED', }, { id: 3, resourceId: 1, title: 'Completed', start: new Date('2025-01-01T11:00:00'), end: new Date('2025-01-01T12:00:00'), status: 'COMPLETED', }, { id: 4, resourceId: 1, title: 'Cancelled', start: new Date('2025-01-01T11:30:00'), end: new Date('2025-01-01T12:30:00'), status: 'CANCELLED', }, { id: 5, resourceId: 1, title: 'No Show', start: new Date('2025-01-01T12:00:00'), end: new Date('2025-01-01T13:00:00'), status: 'NO_SHOW', }, ]; const result = calculateLayout(events); expect(result).toHaveLength(5); // All statuses should be preserved expect(result[0].status).toBe('PENDING'); expect(result[1].status).toBe('CONFIRMED'); expect(result[2].status).toBe('COMPLETED'); expect(result[3].status).toBe('CANCELLED'); expect(result[4].status).toBe('NO_SHOW'); }); it('should handle events with mixed isPaid values', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Paid', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), isPaid: true, }, { id: 2, resourceId: 1, title: 'Unpaid', start: new Date('2025-01-01T10:30:00'), end: new Date('2025-01-01T11:30:00'), isPaid: false, }, { id: 3, resourceId: 1, title: 'No payment info', start: new Date('2025-01-01T11:00:00'), end: new Date('2025-01-01T12:00:00'), }, ]; const result = calculateLayout(events); expect(result[0].isPaid).toBe(true); expect(result[1].isPaid).toBe(false); expect(result[2].isPaid).toBeUndefined(); }); it('should handle events with different resourceIds', () => { // Note: The algorithm doesn't filter by resourceId, it assigns lanes globally const events: Event[] = [ { id: 1, resourceId: 1, title: 'Resource 1', start: new Date('2025-01-01T10:00:00'), end: new Date('2025-01-01T11:00:00'), }, { id: 2, resourceId: 2, title: 'Resource 2', start: new Date('2025-01-01T10:30:00'), end: new Date('2025-01-01T11:30:00'), }, ]; const result = calculateLayout(events); // Even though different resources, they still get different lanes expect(result[0].laneIndex).toBe(0); expect(result[1].laneIndex).toBe(1); }); }); describe('real-world scenarios', () => { it('should handle a busy day with overlapping appointments', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Client A - Haircut', serviceName: 'Haircut', start: new Date('2025-01-15T09:00:00'), end: new Date('2025-01-15T09:30:00'), status: 'CONFIRMED', isPaid: true, }, { id: 2, resourceId: 1, title: 'Client B - Color', serviceName: 'Hair Color', start: new Date('2025-01-15T09:15:00'), end: new Date('2025-01-15T11:00:00'), status: 'CONFIRMED', isPaid: false, }, { id: 3, resourceId: 1, title: 'Client C - Trim', serviceName: 'Trim', start: new Date('2025-01-15T09:30:00'), end: new Date('2025-01-15T10:00:00'), status: 'PENDING', isPaid: false, }, { id: 4, resourceId: 1, title: 'Client D - Consultation', serviceName: 'Consultation', start: new Date('2025-01-15T11:00:00'), end: new Date('2025-01-15T11:30:00'), status: 'CONFIRMED', isPaid: true, }, ]; const result = calculateLayout(events); expect(result).toHaveLength(4); expect(result[0].laneIndex).toBe(0); // 9:00-9:30 expect(result[1].laneIndex).toBe(1); // 9:15-11:00 overlaps with first expect(result[2].laneIndex).toBe(0); // 9:30-10:00 can reuse lane 0 expect(result[3].laneIndex).toBe(0); // 11:00-11:30 can reuse lane 0 }); it('should handle lunch break pattern', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Morning Appointment', start: new Date('2025-01-15T11:00:00'), end: new Date('2025-01-15T12:00:00'), }, { id: 2, resourceId: 1, title: 'Afternoon Appointment', start: new Date('2025-01-15T13:00:00'), end: new Date('2025-01-15T14:00:00'), }, { id: 3, resourceId: 1, title: 'Late Appointment', start: new Date('2025-01-15T14:00:00'), end: new Date('2025-01-15T15:00:00'), }, ]; const result = calculateLayout(events); expect(result.every(event => event.laneIndex === 0)).toBe(true); }); it('should handle back-to-back events spanning multiple days', () => { const events: Event[] = [ { id: 1, resourceId: 1, title: 'Day 1 Morning', start: new Date('2025-01-15T09:00:00'), end: new Date('2025-01-15T10:00:00'), }, { id: 2, resourceId: 1, title: 'Day 1 Afternoon', start: new Date('2025-01-15T14:00:00'), end: new Date('2025-01-15T15:00:00'), }, { id: 3, resourceId: 1, title: 'Day 2 Morning', start: new Date('2025-01-16T09:00:00'), end: new Date('2025-01-16T10:00:00'), }, ]; const result = calculateLayout(events); expect(result.every(event => event.laneIndex === 0)).toBe(true); }); }); });