import { describe, expect, it, vi } from "vitest"; import { buildDemandHoverData, findVacationHit, scheduleVacationHoverUpdate, } from "./timelineHover.js"; describe("timelineHover", () => { it("matches vacation hits inclusively across differing time components", () => { // Use local-noon timestamps so the date is unambiguous in any timezone // (findVacationHit uses local midnight truncation for comparison) const localNoon = (iso: string) => { const d = new Date(iso); d.setHours(12, 0, 0, 0); return d; }; const hit = findVacationHit( [ { id: "vacation_1", startDate: localNoon("2026-04-10"), endDate: localNoon("2026-04-12"), }, ], localNoon("2026-04-12"), ); expect(hit?.id).toBe("vacation_1"); }); it("does not schedule a second hover frame while one is already pending", () => { const frameRef = { current: 42 }; const hoveredKeyRef = { current: null }; const onHoverChange = vi.fn(); const requestAnimationFrameMock = vi.fn(); vi.stubGlobal("requestAnimationFrame", requestAnimationFrameMock); scheduleVacationHoverUpdate({ frameRef, hoveredKeyRef, resourceId: "resource_1", clientX: 160, rect: {} as DOMRect, xToDate: () => new Date("2026-04-10T00:00:00.000Z"), vacations: [{ id: "vacation_1", startDate: "2026-04-10", endDate: "2026-04-12" }], onHoverChange, }); expect(requestAnimationFrameMock).not.toHaveBeenCalled(); expect(onHoverChange).not.toHaveBeenCalled(); }); it("suppresses duplicate hover notifications when the hovered vacation does not change", () => { const scheduledFrames: Array<(timestamp: number) => void> = []; vi.stubGlobal("requestAnimationFrame", vi.fn((callback: (timestamp: number) => void) => { scheduledFrames.push(callback); return 7; })); const frameRef = { current: null as number | null }; const hoveredKeyRef = { current: "resource_1:vacation_1" }; const onHoverChange = vi.fn(); scheduleVacationHoverUpdate({ frameRef, hoveredKeyRef, resourceId: "resource_1", clientX: 160, rect: {} as DOMRect, xToDate: () => new Date("2026-04-11T12:00:00.000Z"), vacations: [{ id: "vacation_1", startDate: "2026-04-10", endDate: "2026-04-12" }], onHoverChange, }); expect(frameRef.current).toBe(7); expect(scheduledFrames).toHaveLength(1); scheduledFrames[0]!(0); expect(frameRef.current).toBeNull(); expect(onHoverChange).not.toHaveBeenCalled(); expect(hoveredKeyRef.current).toBe("resource_1:vacation_1"); }); it("omits cost data and clamps total hours to at least one day for inverted demand ranges", () => { const hoverData = buildDemandHoverData({ roleEntity: null, role: null, project: { name: "Project Atlas", shortCode: "ATL", }, requestedHeadcount: 2, unfilledHeadcount: 1, startDate: "2026-04-14", endDate: "2026-04-13", hoursPerDay: 6, percentage: 50, status: "open", dailyCostCents: 0, } as never); expect(hoverData.roleName).toBe("Open demand"); expect(hoverData.totalHours).toBe(6); expect(hoverData).not.toHaveProperty("dailyCostCents"); expect(hoverData).not.toHaveProperty("totalCostCents"); }); });