openclaw/apps/web/app/components/workspace/object-calendar.test.ts

157 lines
5.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
eachDayOfInterval, format,
parseISO, addDays,
} from "date-fns";
/**
* Pure calendar grid computation extracted for testing.
* These functions mirror the logic inside ObjectCalendar's MonthView.
*/
function buildMonthGrid(date: Date): { days: Date[]; weekCount: number } {
const monthStart = startOfMonth(date);
const monthEnd = endOfMonth(date);
const calStart = startOfWeek(monthStart, { weekStartsOn: 1 });
const calEnd = endOfWeek(monthEnd, { weekStartsOn: 1 });
const days = eachDayOfInterval({ start: calStart, end: calEnd });
return { days, weekCount: days.length / 7 };
}
function groupEventsByDay(
events: { id: string; date: Date; endDate?: Date }[],
): Map<string, string[]> {
const map = new Map<string, string[]>();
for (const ev of events) {
const key = format(ev.date, "yyyy-MM-dd");
if (!map.has(key)) {map.set(key, []);}
map.get(key)!.push(ev.id);
if (ev.endDate && format(ev.date, "yyyy-MM-dd") !== format(ev.endDate, "yyyy-MM-dd")) {
const spanDays = eachDayOfInterval({ start: addDays(ev.date, 1), end: ev.endDate });
for (const d of spanDays) {
const dk = format(d, "yyyy-MM-dd");
if (!map.has(dk)) {map.set(dk, []);}
map.get(dk)!.push(ev.id);
}
}
}
return map;
}
// ---------------------------------------------------------------------------
// Month grid generation
// ---------------------------------------------------------------------------
describe("buildMonthGrid", () => {
it("always produces a multiple of 7 days (grid must be rectangular)", () => {
for (let m = 0; m < 12; m++) {
const { days } = buildMonthGrid(new Date(2026, m, 15));
expect(days.length % 7).toBe(0);
}
});
it("includes padding days from previous and next months (prevents empty cells)", () => {
const { days } = buildMonthGrid(new Date(2026, 2, 15)); // March 2026
const marchStart = startOfMonth(new Date(2026, 2, 15));
const firstDay = days[0];
const lastDay = days[days.length - 1];
expect(firstDay <= marchStart).toBe(true);
expect(lastDay >= endOfMonth(new Date(2026, 2, 15))).toBe(true);
});
it("starts grid on Monday (weekStartsOn: 1)", () => {
const { days } = buildMonthGrid(new Date(2026, 0, 15)); // January 2026
expect(format(days[0], "EEEE")).toBe("Monday");
});
it("ends grid on Sunday", () => {
const { days } = buildMonthGrid(new Date(2026, 5, 15)); // June 2026
expect(format(days[days.length - 1], "EEEE")).toBe("Sunday");
});
it("produces 4-6 weeks for any month (standard calendar range)", () => {
for (let m = 0; m < 12; m++) {
const { weekCount } = buildMonthGrid(new Date(2026, m, 1));
expect(weekCount).toBeGreaterThanOrEqual(4);
expect(weekCount).toBeLessThanOrEqual(6);
}
});
});
// ---------------------------------------------------------------------------
// Event grouping by day
// ---------------------------------------------------------------------------
describe("groupEventsByDay", () => {
it("groups single-day events by their date key", () => {
const events = [
{ id: "a", date: parseISO("2026-03-10") },
{ id: "b", date: parseISO("2026-03-10") },
{ id: "c", date: parseISO("2026-03-11") },
];
const grouped = groupEventsByDay(events);
expect(grouped.get("2026-03-10")).toEqual(["a", "b"]);
expect(grouped.get("2026-03-11")).toEqual(["c"]);
});
it("spans multi-day events across all covered days (prevents disappearing events)", () => {
const events = [
{
id: "multiday",
date: parseISO("2026-03-10"),
endDate: parseISO("2026-03-13"),
},
];
const grouped = groupEventsByDay(events);
expect(grouped.get("2026-03-10")).toContain("multiday");
expect(grouped.get("2026-03-11")).toContain("multiday");
expect(grouped.get("2026-03-12")).toContain("multiday");
expect(grouped.get("2026-03-13")).toContain("multiday");
});
it("does not span same-day events (endDate === date)", () => {
const events = [
{
id: "sameday",
date: parseISO("2026-03-10"),
endDate: parseISO("2026-03-10"),
},
];
const grouped = groupEventsByDay(events);
expect(grouped.get("2026-03-10")).toEqual(["sameday"]);
expect(grouped.has("2026-03-11")).toBe(false);
});
it("returns empty map for no events", () => {
const grouped = groupEventsByDay([]);
expect(grouped.size).toBe(0);
});
});
// ---------------------------------------------------------------------------
// Date parsing edge cases
// ---------------------------------------------------------------------------
describe("calendar date parsing", () => {
it("parseISO handles ISO date strings correctly", () => {
const d = parseISO("2026-03-15");
expect(d.getFullYear()).toBe(2026);
expect(d.getMonth()).toBe(2); // 0-indexed
expect(d.getDate()).toBe(15);
});
it("parseISO handles datetime strings (calendar should use date portion only)", () => {
const d = parseISO("2026-03-15T14:30:00Z");
expect(d.getFullYear()).toBe(2026);
expect(d.getMonth()).toBe(2);
expect(d.getDate()).toBe(15);
});
it("parseISO returns Invalid Date for garbage input (calendar should skip these)", () => {
const d = parseISO("not-a-date");
expect(Number.isNaN(d.getTime())).toBe(true);
});
});