diff --git a/apps/web/src/components/timeline/renderHelpers.test.tsx b/apps/web/src/components/timeline/renderHelpers.test.tsx new file mode 100644 index 0000000..f2adb6d --- /dev/null +++ b/apps/web/src/components/timeline/renderHelpers.test.tsx @@ -0,0 +1,118 @@ +import { describe, expect, it } from "vitest"; +import { + buildVacationBlocksByResource, + renderOverbookingBlink, + renderRangeOverlay, +} from "./renderHelpers.js"; + +describe("renderHelpers", () => { + it("returns no vacation blocks when vacations are hidden", () => { + const result = buildVacationBlocksByResource( + new Map([ + [ + "resource_1", + [ + { + id: "vacation_1", + startDate: "2026-04-10", + endDate: "2026-04-12", + }, + ], + ], + ]) as never, + false, + () => 0, + () => 10, + 20, + 200, + ); + + expect(result.size).toBe(0); + }); + + it("keeps a minimum vacation block width and skips blocks beyond the visible canvas", () => { + const result = buildVacationBlocksByResource( + new Map([ + [ + "resource_1", + [ + { + id: "vacation_visible", + startDate: "2026-04-10", + endDate: "2026-04-10", + type: "VACATION", + status: "APPROVED", + }, + { + id: "vacation_offscreen", + startDate: "2026-04-20", + endDate: "2026-04-21", + type: "VACATION", + status: "APPROVED", + }, + ], + ], + ]) as never, + true, + (date) => (date.getUTCDate() === 10 ? 10 : 250), + () => 5, + 20, + 200, + ); + + expect(result.get("resource_1")).toEqual([ + expect.objectContaining({ + left: 10, + width: 20, + }), + ]); + }); + + it("renders a range overlay even when selection is dragged backwards", () => { + const overlay = renderRangeOverlay( + { + isSelecting: true, + resourceId: "resource_1", + startDate: new Date("2026-04-12T00:00:00.000Z"), + currentDate: new Date("2026-04-10T00:00:00.000Z"), + suggestedProjectId: null, + startClientX: 0, + }, + "resource_1", + 32, + (date) => date.getUTCDate() * 10, + (start, end) => (end.getUTCDate() - start.getUTCDate() + 1) * 10, + 10, + ); + + expect(overlay).not.toBeNull(); + expect(overlay?.props.style).toMatchObject({ + left: 100, + width: 30, + height: 24, + }); + }); + + it("renders overbooking blink overlays when booking factors exceed day capacity", () => { + const overlay = renderOverbookingBlink( + [ + { + startDate: "2026-04-10", + endDate: "2026-04-10", + hoursPerDay: 6, + }, + ] as never, + [new Date("2026-04-10T00:00:00.000Z")], + 24, + [8], + [1.5], + ); + + expect(overlay).not.toBeNull(); + expect(overlay).toHaveLength(1); + expect(overlay?.[0]?.props.style).toMatchObject({ + left: 0, + width: 24, + }); + }); +}); diff --git a/apps/web/src/components/timeline/renderHelpers.tsx b/apps/web/src/components/timeline/renderHelpers.tsx index df188b1..a11a7e7 100644 --- a/apps/web/src/components/timeline/renderHelpers.tsx +++ b/apps/web/src/components/timeline/renderHelpers.tsx @@ -3,6 +3,7 @@ * Extracted to avoid duplication of identical vacation blocks, range overlay, and overbooking blink logic. */ +import React from "react"; import { clsx } from "clsx"; import { VACATION_TIMELINE_COLORS,