- Add frontend unit tests with Vitest for components, hooks, pages, and utilities - Add backend tests for webhooks, notifications, middleware, and edge cases - Add ForgotPassword, NotFound, and ResetPassword pages - Add migration for orphaned staff resources conversion - Add coverage directory to gitignore (generated reports) - Various bug fixes and improvements from previous work 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
721 lines
20 KiB
TypeScript
721 lines
20 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|