270 lines
8.3 KiB
TypeScript
270 lines
8.3 KiB
TypeScript
import { TRPCError } from "@trpc/server";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../router/timeline-holiday-read.js", async () => {
|
|
const actual = await vi.importActual<typeof import("../router/timeline-holiday-read.js")>(
|
|
"../router/timeline-holiday-read.js",
|
|
);
|
|
|
|
return {
|
|
...actual,
|
|
loadTimelineHolidayOverlays: vi.fn(),
|
|
formatHolidayOverlays: vi.fn(actual.formatHolidayOverlays),
|
|
};
|
|
});
|
|
|
|
import {
|
|
loadTimelineProjectContextDetailArtifacts,
|
|
resolveTimelineProjectContextPeriod,
|
|
} from "../router/timeline-project-context-support.js";
|
|
import { buildTimelineProjectAssignmentConflicts } from "../router/timeline-project-conflict-support.js";
|
|
import {
|
|
formatHolidayOverlays,
|
|
loadTimelineHolidayOverlays,
|
|
} from "../router/timeline-holiday-read.js";
|
|
|
|
const loadTimelineHolidayOverlaysMock = vi.mocked(loadTimelineHolidayOverlays);
|
|
const formatHolidayOverlaysMock = vi.mocked(formatHolidayOverlays);
|
|
|
|
describe("timeline project context support", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("derives the detail period from explicit input and validates order", () => {
|
|
expect(resolveTimelineProjectContextPeriod({
|
|
requestedStartDate: "2026-04-03",
|
|
requestedEndDate: "2026-04-12",
|
|
projectStartDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
projectEndDate: new Date("2026-04-10T00:00:00.000Z"),
|
|
})).toEqual({
|
|
startDate: new Date("2026-04-03T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-12T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(() =>
|
|
resolveTimelineProjectContextPeriod({
|
|
requestedStartDate: "2026-04-12",
|
|
requestedEndDate: "2026-04-03",
|
|
}),
|
|
).toThrowError(new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "endDate must be on or after startDate.",
|
|
}));
|
|
});
|
|
|
|
it("builds assignment conflict summaries with cross-project overlap counts", () => {
|
|
expect(buildTimelineProjectAssignmentConflicts({
|
|
projectId: "project_1",
|
|
assignments: [
|
|
{
|
|
id: "assignment_1",
|
|
resourceId: "resource_1",
|
|
resource: { displayName: "Alice" },
|
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
|
hoursPerDay: 8,
|
|
},
|
|
{
|
|
id: "placeholder_1",
|
|
resourceId: null,
|
|
resource: null,
|
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
|
hoursPerDay: 4,
|
|
},
|
|
],
|
|
allResourceAllocations: [
|
|
{
|
|
id: "assignment_1",
|
|
resourceId: "resource_1",
|
|
projectId: "project_1",
|
|
project: { name: "Current", shortCode: "CUR" },
|
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
|
hoursPerDay: 8,
|
|
status: "ACTIVE",
|
|
},
|
|
{
|
|
id: "booking_2",
|
|
resourceId: "resource_1",
|
|
projectId: "project_2",
|
|
project: { name: "Other", shortCode: "OTH" },
|
|
startDate: new Date("2026-04-04T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-07T00:00:00.000Z"),
|
|
hoursPerDay: 6,
|
|
status: "ACTIVE",
|
|
},
|
|
{
|
|
id: "booking_3",
|
|
resourceId: "resource_1",
|
|
projectId: "project_1",
|
|
project: { name: "Current", shortCode: "CUR" },
|
|
startDate: new Date("2026-04-02T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-03T00:00:00.000Z"),
|
|
hoursPerDay: 2,
|
|
status: "PROPOSED",
|
|
},
|
|
],
|
|
})).toEqual([
|
|
{
|
|
assignmentId: "assignment_1",
|
|
resourceId: "resource_1",
|
|
resourceName: "Alice",
|
|
startDate: "2026-04-01",
|
|
endDate: "2026-04-05",
|
|
hoursPerDay: 8,
|
|
overlapCount: 2,
|
|
crossProjectOverlapCount: 1,
|
|
overlaps: [
|
|
{
|
|
id: "booking_2",
|
|
projectId: "project_2",
|
|
projectName: "Other",
|
|
projectShortCode: "OTH",
|
|
startDate: "2026-04-04",
|
|
endDate: "2026-04-07",
|
|
hoursPerDay: 6,
|
|
status: "ACTIVE",
|
|
sameProject: false,
|
|
},
|
|
{
|
|
id: "booking_3",
|
|
projectId: "project_1",
|
|
projectName: "Current",
|
|
projectShortCode: "CUR",
|
|
startDate: "2026-04-02",
|
|
endDate: "2026-04-03",
|
|
hoursPerDay: 2,
|
|
status: "PROPOSED",
|
|
sameProject: true,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("loads detail artifacts with formatted holiday overlays only when resources exist", async () => {
|
|
loadTimelineHolidayOverlaysMock.mockResolvedValueOnce([
|
|
{
|
|
id: "overlay_1",
|
|
resourceId: "resource_1",
|
|
startDate: new Date("2026-04-03T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-03T00:00:00.000Z"),
|
|
note: "Holiday",
|
|
scope: "COUNTRY",
|
|
calendarName: "DE",
|
|
sourceType: "CALENDAR",
|
|
countryCode: "DE",
|
|
countryName: "Germany",
|
|
federalState: null,
|
|
metroCityName: null,
|
|
},
|
|
] as never);
|
|
|
|
const artifacts = await loadTimelineProjectContextDetailArtifacts({} as never, {
|
|
projectId: "project_1",
|
|
requestedStartDate: "2026-04-01",
|
|
durationDays: 5,
|
|
projectStartDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
projectEndDate: new Date("2026-04-10T00:00:00.000Z"),
|
|
firstAssignmentStartDate: new Date("2026-04-02T00:00:00.000Z"),
|
|
firstDemandStartDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
assignments: [
|
|
{
|
|
id: "assignment_1",
|
|
resourceId: "resource_1",
|
|
resource: { displayName: "Alice" },
|
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
|
hoursPerDay: 8,
|
|
},
|
|
],
|
|
allResourceAllocations: [
|
|
{
|
|
id: "booking_1",
|
|
resourceId: "resource_1",
|
|
projectId: "project_2",
|
|
project: { name: "Other", shortCode: "OTH" },
|
|
startDate: new Date("2026-04-04T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-07T00:00:00.000Z"),
|
|
hoursPerDay: 6,
|
|
status: "ACTIVE",
|
|
},
|
|
],
|
|
resourceIds: ["resource_1"],
|
|
});
|
|
|
|
expect(loadTimelineHolidayOverlaysMock).toHaveBeenCalledWith(
|
|
{},
|
|
{
|
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
|
resourceIds: ["resource_1"],
|
|
projectIds: ["project_1"],
|
|
},
|
|
);
|
|
expect(formatHolidayOverlaysMock).toHaveBeenCalledTimes(1);
|
|
expect(artifacts).toEqual({
|
|
period: {
|
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
|
},
|
|
holidayOverlays: [
|
|
{
|
|
id: "overlay_1",
|
|
resourceId: "resource_1",
|
|
startDate: "2026-04-03",
|
|
endDate: "2026-04-03",
|
|
note: "Holiday",
|
|
scope: "COUNTRY",
|
|
calendarName: "DE",
|
|
sourceType: "CALENDAR",
|
|
countryCode: "DE",
|
|
countryName: "Germany",
|
|
federalState: null,
|
|
metroCityName: null,
|
|
},
|
|
],
|
|
assignmentConflicts: [
|
|
{
|
|
assignmentId: "assignment_1",
|
|
resourceId: "resource_1",
|
|
resourceName: "Alice",
|
|
startDate: "2026-04-01",
|
|
endDate: "2026-04-05",
|
|
hoursPerDay: 8,
|
|
overlapCount: 1,
|
|
crossProjectOverlapCount: 1,
|
|
overlaps: [
|
|
{
|
|
id: "booking_1",
|
|
projectId: "project_2",
|
|
projectName: "Other",
|
|
projectShortCode: "OTH",
|
|
startDate: "2026-04-04",
|
|
endDate: "2026-04-07",
|
|
hoursPerDay: 6,
|
|
status: "ACTIVE",
|
|
sameProject: false,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const emptyArtifacts = await loadTimelineProjectContextDetailArtifacts({} as never, {
|
|
projectId: "project_1",
|
|
requestedStartDate: "2026-04-01",
|
|
durationDays: 5,
|
|
assignments: [],
|
|
allResourceAllocations: [],
|
|
resourceIds: [],
|
|
});
|
|
|
|
expect(loadTimelineHolidayOverlaysMock).toHaveBeenCalledTimes(1);
|
|
expect(emptyArtifacts.holidayOverlays).toEqual([]);
|
|
expect(emptyArtifacts.assignmentConflicts).toEqual([]);
|
|
});
|
|
});
|