@@ -1,13 +1,21 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
busyIntervals,
|
||||
canonicalRepair,
|
||||
findConflicts,
|
||||
findFreeSlots,
|
||||
localTimeCandidates,
|
||||
mergeCalendars,
|
||||
parseCalendar,
|
||||
previewOccurrences,
|
||||
timezoneDiagnostics,
|
||||
} from "../../src/calendar/model";
|
||||
import {
|
||||
exportJCal,
|
||||
exportJSCalendar,
|
||||
importJCal,
|
||||
importJSCalendar,
|
||||
} from "../../src/calendar/interchange";
|
||||
|
||||
const recurring = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\nBEGIN:VEVENT\r\nUID:a\r\nDTSTAMP:20260101T000000Z\r\nDTSTART:20260901T100000Z\r\nDTEND:20260901T110000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nSUMMARY:A\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:b\r\nDTSTART:20260901T103000Z\r\nDTEND:20260901T120000Z\r\nSUMMARY:B\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n`;
|
||||
|
||||
@@ -24,6 +32,26 @@ describe("calendar model", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("applies moved, cancelled, duration, and THISANDFUTURE exceptions", () => {
|
||||
const source = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\nBEGIN:VEVENT\r\nUID:series\r\nDTSTART:20260901T100000Z\r\nDTEND:20260901T110000Z\r\nRRULE:FREQ=DAILY;COUNT=5\r\nSUMMARY:Original\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:series\r\nRECURRENCE-ID:20260902T100000Z\r\nDTSTART:20260902T120000Z\r\nDTEND:20260902T133000Z\r\nSUMMARY:Moved\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:series\r\nRECURRENCE-ID:20260903T100000Z\r\nDTSTART:20260903T100000Z\r\nDTEND:20260903T110000Z\r\nSTATUS:CANCELLED\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:series\r\nRECURRENCE-ID;RANGE=THISANDFUTURE:20260904T100000Z\r\nDTSTART:20260904T110000Z\r\nDTEND:20260904T130000Z\r\nSUMMARY:Shifted future\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n`;
|
||||
const preview = previewOccurrences(parseCalendar(source), 10, 30);
|
||||
expect(preview.occurrences).toHaveLength(4);
|
||||
expect(preview.occurrences.map((item) => item.startText)).toEqual([
|
||||
"2026-09-01T10:00:00",
|
||||
"2026-09-02T12:00:00",
|
||||
"2026-09-04T11:00:00",
|
||||
"2026-09-05T11:00:00",
|
||||
]);
|
||||
expect(preview.occurrences[1]).toMatchObject({
|
||||
summary: "Moved",
|
||||
endText: "2026-09-02T13:30:00",
|
||||
});
|
||||
expect(preview.occurrences[3]).toMatchObject({
|
||||
summary: "Shifted future",
|
||||
endText: "2026-09-05T13:00:00",
|
||||
});
|
||||
});
|
||||
|
||||
it("canonicalizes line endings and supplies required metadata", () => {
|
||||
const input = `BEGIN:VCALENDAR\nBEGIN:VEVENT\nUID:x\nDTSTART:20260901T100000Z\nDTEND:20260901T110000Z\nEND:VEVENT\nEND:VCALENDAR\n`;
|
||||
const repaired = canonicalRepair(input);
|
||||
@@ -76,6 +104,69 @@ describe("calendar model", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("inspects tasks, journals, free-busy periods, and alarms", () => {
|
||||
const source = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\nBEGIN:VTODO\r\nUID:task-1\r\nDTSTART:20260901T080000Z\r\nDUE:20260901T090000Z\r\nSUMMARY:Write tests\r\nPERCENT-COMPLETE:25\r\nPRIORITY:3\r\nBEGIN:VALARM\r\nACTION:DISPLAY\r\nTRIGGER:-PT15M\r\nDESCRIPTION:Reminder\r\nEND:VALARM\r\nEND:VTODO\r\nBEGIN:VJOURNAL\r\nUID:journal-1\r\nDTSTART;VALUE=DATE:20260901\r\nSUMMARY:Daily note\r\nDESCRIPTION:Done locally\r\nEND:VJOURNAL\r\nBEGIN:VFREEBUSY\r\nUID:busy-1\r\nDTSTART:20260901T080000Z\r\nDTEND:20260901T120000Z\r\nFREEBUSY:20260901T100000Z/20260901T110000Z\r\nEND:VFREEBUSY\r\nEND:VCALENDAR\r\n`;
|
||||
const document = parseCalendar(source);
|
||||
expect(document.tasks[0]).toMatchObject({
|
||||
uid: "task-1",
|
||||
percentComplete: 25,
|
||||
priority: 3,
|
||||
});
|
||||
expect(document.alarms[0]).toMatchObject({
|
||||
ownerUid: "task-1",
|
||||
trigger: "-PT15M",
|
||||
});
|
||||
expect(document.journals[0]).toMatchObject({
|
||||
uid: "journal-1",
|
||||
dateText: "2026-09-01",
|
||||
});
|
||||
expect(document.freeBusy[0]?.intervals[0]).toMatchObject({
|
||||
startMs: Date.parse("2026-09-01T10:00:00Z"),
|
||||
endMs: Date.parse("2026-09-01T11:00:00Z"),
|
||||
});
|
||||
const windowStart = Date.parse("2026-09-01T09:00:00Z");
|
||||
const windowEnd = Date.parse("2026-09-01T12:00:00Z");
|
||||
expect(busyIntervals(document, windowStart, windowEnd)).toHaveLength(1);
|
||||
expect(findFreeSlots(document, windowStart, windowEnd, 30)).toEqual([
|
||||
{
|
||||
startMs: windowStart,
|
||||
endMs: Date.parse("2026-09-01T10:00:00Z"),
|
||||
minutes: 60,
|
||||
},
|
||||
{
|
||||
startMs: Date.parse("2026-09-01T11:00:00Z"),
|
||||
endMs: windowEnd,
|
||||
minutes: 60,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("round-trips jCal and focused JSCalendar Event/Task groups", () => {
|
||||
const document = parseCalendar(recurring);
|
||||
expect(parseCalendar(importJCal(exportJCal(document))).events).toHaveLength(
|
||||
2,
|
||||
);
|
||||
const exported = exportJSCalendar(document);
|
||||
expect(exported.data).toMatchObject({ "@type": "Group" });
|
||||
const imported = parseCalendar(importJSCalendar(exported.data));
|
||||
expect(imported.events).toHaveLength(2);
|
||||
|
||||
const task = parseCalendar(
|
||||
importJSCalendar({
|
||||
"@type": "Task",
|
||||
uid: "task-json",
|
||||
title: "Imported task",
|
||||
due: "2026-09-02T12:00:00Z",
|
||||
progress: 0.5,
|
||||
}),
|
||||
);
|
||||
expect(task.tasks[0]).toMatchObject({
|
||||
uid: "task-json",
|
||||
summary: "Imported task",
|
||||
percentComplete: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects unbounded and malformed inputs", () => {
|
||||
expect(() => previewOccurrences(parseCalendar(recurring), 501, 30)).toThrow(
|
||||
/1–500/u,
|
||||
|
||||
Reference in New Issue
Block a user