1df208dbcc
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>
110 lines
3.3 KiB
TypeScript
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");
|
|
});
|
|
});
|