import { describe, expect, it } from "vitest"; import type { CalendarEvent } from "../types/events"; import { buildOverlapGroups, calculateEventLayouts, createLayoutMap, sortEvents } from "./overlap"; describe("overlap utility", () => { describe("sortEvents", () => { it("should sort events by start time ascending", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T09:00:00"), end: new Date("2024-01-01T10:00:00"), }, { id: 3, title: "Event 3", start: new Date("2024-01-01T11:00:00"), end: new Date("2024-01-01T12:00:00"), }, ]; const sorted = sortEvents(events); expect(sorted[0].id).toBe(2); expect(sorted[1].id).toBe(1); expect(sorted[2].id).toBe(3); }); it("should sort events with same start time by end time descending (longer first)", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T12:00:00"), }, { id: 3, title: "Event 3", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T10:30:00"), }, ]; const sorted = sortEvents(events); expect(sorted[0].id).toBe(2); // Longest (2 hours) expect(sorted[1].id).toBe(1); // Medium (1 hour) expect(sorted[2].id).toBe(3); // Shortest (30 min) }); it("should not mutate the original array", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T09:00:00"), end: new Date("2024-01-01T10:00:00"), }, ]; const originalFirstId = events[0].id; sortEvents(events); expect(events[0].id).toBe(originalFirstId); }); }); describe("buildOverlapGroups", () => { it("should return empty array for no events", () => { const groups = buildOverlapGroups([]); expect(groups).toEqual([]); }); it("should return single group for single event", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, ]; const groups = buildOverlapGroups(events); expect(groups).toHaveLength(1); expect(groups[0]).toHaveLength(1); expect(groups[0][0].id).toBe(1); }); it("should group two overlapping events together", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T10:30:00"), end: new Date("2024-01-01T11:30:00"), }, ]; const sorted = sortEvents(events); const groups = buildOverlapGroups(sorted); expect(groups).toHaveLength(1); expect(groups[0]).toHaveLength(2); expect(groups[0][0].id).toBe(1); expect(groups[0][1].id).toBe(2); }); it("should separate non-overlapping events into different groups", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T11:00:00"), end: new Date("2024-01-01T12:00:00"), }, ]; const sorted = sortEvents(events); const groups = buildOverlapGroups(sorted); expect(groups).toHaveLength(2); expect(groups[0]).toHaveLength(1); expect(groups[1]).toHaveLength(1); expect(groups[0][0].id).toBe(1); expect(groups[1][0].id).toBe(2); }); it("should handle chain overlaps (A overlaps B, B overlaps C)", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T10:30:00"), end: new Date("2024-01-01T11:30:00"), }, { id: 3, title: "Event 3", start: new Date("2024-01-01T11:00:00"), end: new Date("2024-01-01T12:00:00"), }, ]; const sorted = sortEvents(events); const groups = buildOverlapGroups(sorted); expect(groups).toHaveLength(1); expect(groups[0]).toHaveLength(3); expect(groups[0][0].id).toBe(1); expect(groups[0][1].id).toBe(2); expect(groups[0][2].id).toBe(3); }); it("should handle multiple separate overlap groups", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T09:00:00"), end: new Date("2024-01-01T10:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T09:30:00"), end: new Date("2024-01-01T10:30:00"), }, { id: 3, title: "Event 3", start: new Date("2024-01-01T11:00:00"), end: new Date("2024-01-01T12:00:00"), }, { id: 4, title: "Event 4", start: new Date("2024-01-01T11:30:00"), end: new Date("2024-01-01T12:30:00"), }, ]; const sorted = sortEvents(events); const groups = buildOverlapGroups(sorted); expect(groups).toHaveLength(2); expect(groups[0]).toHaveLength(2); expect(groups[1]).toHaveLength(2); expect(groups[0][0].id).toBe(1); expect(groups[0][1].id).toBe(2); expect(groups[1][0].id).toBe(3); expect(groups[1][1].id).toBe(4); }); it("should handle events that start at the same time", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T12:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 3, title: "Event 3", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T10:30:00"), }, ]; const sorted = sortEvents(events); const groups = buildOverlapGroups(sorted); expect(groups).toHaveLength(1); expect(groups[0]).toHaveLength(3); }); }); describe("calculateEventLayouts", () => { it("should calculate layout for single event", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, ]; const layouts = calculateEventLayouts(events); expect(layouts).toHaveLength(1); expect(layouts[0].event.id).toBe(1); expect(layouts[0].leftOffsetPercent).toBe(0); expect(layouts[0].widthPercent).toBe(99.5); expect(layouts[0].baseZIndex).toBe(60); expect(layouts[0].groupIndex).toBe(0); expect(layouts[0].indexInGroup).toBe(0); }); it("should calculate cascading layout for two overlapping events", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T10:30:00"), end: new Date("2024-01-01T11:30:00"), }, ]; const layouts = calculateEventLayouts(events); expect(layouts).toHaveLength(2); expect(layouts[0].event.id).toBe(1); expect(layouts[0].leftOffsetPercent).toBe(0); expect(layouts[0].widthPercent).toBe(80); expect(layouts[0].baseZIndex).toBe(60); expect(layouts[1].event.id).toBe(2); expect(layouts[1].leftOffsetPercent).toBe(49.5); expect(layouts[1].widthPercent).toBe(50); expect(layouts[1].baseZIndex).toBe(61); }); it("should calculate cascading layout for three overlapping events", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T10:30:00"), end: new Date("2024-01-01T11:30:00"), }, { id: 3, title: "Event 3", start: new Date("2024-01-01T11:00:00"), end: new Date("2024-01-01T12:00:00"), }, ]; const layouts = calculateEventLayouts(events); expect(layouts).toHaveLength(3); expect(layouts[0].leftOffsetPercent).toBe(0); expect(layouts[1].leftOffsetPercent).toBeCloseTo(35.315, 1); expect(layouts[2].leftOffsetPercent).toBe(66.5); expect(layouts[0].baseZIndex).toBe(60); expect(layouts[1].baseZIndex).toBe(61); expect(layouts[2].baseZIndex).toBe(62); }); it("should respect custom baseZIndex configuration", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T10:30:00"), end: new Date("2024-01-01T11:30:00"), }, ]; const layouts = calculateEventLayouts(events, { baseZIndex: 50, }); expect(layouts[0].baseZIndex).toBe(50); expect(layouts[1].baseZIndex).toBe(51); }); it("should handle non-overlapping events in separate groups", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T11:00:00"), end: new Date("2024-01-01T12:00:00"), }, ]; const layouts = calculateEventLayouts(events); expect(layouts).toHaveLength(2); expect(layouts[0].groupIndex).toBe(0); expect(layouts[0].indexInGroup).toBe(0); expect(layouts[0].leftOffsetPercent).toBe(0); expect(layouts[1].groupIndex).toBe(1); expect(layouts[1].indexInGroup).toBe(0); expect(layouts[1].leftOffsetPercent).toBe(0); }); it("should prevent overflow with many overlapping events (dense scenario)", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T09:00:00"), end: new Date("2024-01-01T09:30:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T09:15:00"), end: new Date("2024-01-01T10:00:00"), }, { id: 3, title: "Event 3", start: new Date("2024-01-01T09:45:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 4, title: "Event 4", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T10:30:00"), }, { id: 5, title: "Event 5", start: new Date("2024-01-01T10:15:00"), end: new Date("2024-01-01T11:30:00"), }, { id: 6, title: "Event 6", start: new Date("2024-01-01T11:00:00"), end: new Date("2024-01-01T12:00:00"), }, { id: 7, title: "Event 7", start: new Date("2024-01-01T11:30:00"), end: new Date("2024-01-01T12:30:00"), }, { id: 8, title: "Event 8", start: new Date("2024-01-01T12:00:00"), end: new Date("2024-01-01T13:30:00"), }, { id: 9, title: "Event 9", start: new Date("2024-01-01T12:30:00"), end: new Date("2024-01-01T13:00:00"), }, { id: 10, title: "Event 10", start: new Date("2024-01-01T13:00:00"), end: new Date("2024-01-01T14:00:00"), }, { id: 11, title: "Event 11", start: new Date("2024-01-01T13:15:00"), end: new Date("2024-01-01T14:00:00"), }, ]; const layouts = calculateEventLayouts(events); layouts.forEach((layout) => { const totalWidth = layout.leftOffsetPercent + layout.widthPercent; expect(totalWidth).toBeLessThanOrEqual(100 - 0.5); }); expect(layouts).toHaveLength(11); }); it("should respect safety margin with 20+ overlapping events", () => { const events: CalendarEvent[] = Array.from({ length: 21 }, (_, i) => ({ id: i + 1, title: `Event ${i + 1}`, start: new Date(`2024-01-01T09:${String(i * 2).padStart(2, "0")}:00`), end: new Date(`2024-01-01T10:${String(i * 2).padStart(2, "0")}:00`), })); const layouts = calculateEventLayouts(events); expect(layouts).toHaveLength(21); layouts.forEach((layout) => { const totalWidth = layout.leftOffsetPercent + layout.widthPercent; expect(totalWidth).toBeLessThanOrEqual(100 - 0.5); }); const lastLayout = layouts[layouts.length - 1]; expect(lastLayout.leftOffsetPercent + lastLayout.widthPercent).toBeLessThanOrEqual(99.5); }); it("should compress offset step for dense overlaps while maintaining cascade", () => { const events: CalendarEvent[] = Array.from({ length: 12 }, (_, i) => ({ id: i + 1, title: `Event ${i + 1}`, start: new Date(`2024-01-01T10:${String(i * 5).padStart(2, "0")}:00`), end: new Date(`2024-01-01T11:${String(i * 5).padStart(2, "0")}:00`), })); const layouts = calculateEventLayouts(events); expect(layouts).toHaveLength(12); layouts.forEach((layout, index) => { expect(layout.leftOffsetPercent + layout.widthPercent).toBeLessThanOrEqual(100); if (index > 0) { expect(layout.leftOffsetPercent).toBeGreaterThan(layouts[index - 1].leftOffsetPercent); } }); const lastLayout = layouts[layouts.length - 1]; expect(lastLayout.leftOffsetPercent + lastLayout.widthPercent).toBeLessThanOrEqual(100); }); it("should use dynamic width for three overlapping events", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T10:30:00"), end: new Date("2024-01-01T11:30:00"), }, { id: 3, title: "Event 3", start: new Date("2024-01-01T11:00:00"), end: new Date("2024-01-01T12:00:00"), }, ]; const layouts = calculateEventLayouts(events); expect(layouts[0].widthPercent).toBe(55); expect(layouts[1].widthPercent).toBeCloseTo(41.9, 0); expect(layouts[2].widthPercent).toBe(33); expect(layouts[0].leftOffsetPercent).toBe(0); expect(layouts[1].leftOffsetPercent).toBeCloseTo(35.315, 1); expect(layouts[2].leftOffsetPercent).toBe(66.5); }); }); describe("createLayoutMap", () => { it("should create a map from event ID to layout", () => { const events: CalendarEvent[] = [ { id: 1, title: "Event 1", start: new Date("2024-01-01T10:00:00"), end: new Date("2024-01-01T11:00:00"), }, { id: 2, title: "Event 2", start: new Date("2024-01-01T10:30:00"), end: new Date("2024-01-01T11:30:00"), }, ]; const layouts = calculateEventLayouts(events); const map = createLayoutMap(layouts); expect(map.size).toBe(2); expect(map.get(1)?.event.id).toBe(1); expect(map.get(2)?.event.id).toBe(2); expect(map.get(1)?.leftOffsetPercent).toBe(0); expect(map.get(2)?.leftOffsetPercent).toBe(49.5); expect(map.get(1)?.widthPercent).toBe(80); expect(map.get(2)?.widthPercent).toBe(50); }); it("should handle empty layouts", () => { const map = createLayoutMap([]); expect(map.size).toBe(0); }); }); });