Files
Nexus/apps/web/src/components/timeline/timelineHover.test.ts
T
Hartmut 1df208dbcc feat(timeline): add pulse animation for in-flight drag mutations
Allocation bars that have active optimistic overrides (post-drag,
awaiting server confirmation) now pulse subtly via animate-pulse.
The pending set is derived from the existing optimisticAllocations
map keys, requiring no additional state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:28:46 +02:00

110 lines
3.3 KiB
TypeScript

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");
});
});