Files
calendar-tools/tests/calendar/model.test.ts
T
2026-09-01 13:04:50 +02:00

88 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import {
canonicalRepair,
findConflicts,
localTimeCandidates,
mergeCalendars,
parseCalendar,
previewOccurrences,
timezoneDiagnostics,
} from "../../src/calendar/model";
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`;
describe("calendar model", () => {
it("parses and expands recurrence under a bound", () => {
const document = parseCalendar(recurring);
expect(document.events).toHaveLength(2);
const preview = previewOccurrences(document, 10, 30);
expect(preview.occurrences.filter((item) => item.uid === "a")).toHaveLength(
3,
);
expect(findConflicts(preview.occurrences)).toEqual(
expect.arrayContaining([expect.objectContaining({ kind: "overlap" })]),
);
});
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);
expect(repaired.source).toContain("VERSION:2.0\r\n");
expect(repaired.source).toContain("PRODID:");
expect(repaired.source).not.toMatch(/(?<!\r)\n/u);
});
it("merges event identity using sequence then stamp", () => {
const newer = recurring.replace(
"SUMMARY:A",
"SEQUENCE:2\r\nSUMMARY:Updated A",
);
const merged = mergeCalendars([
parseCalendar(recurring),
parseCalendar(newer),
]);
expect(merged.deduplicated).toBe(2);
expect(merged.source).toContain("SUMMARY:Updated A");
expect(parseCalendar(merged.source).events).toHaveLength(2);
});
it("never deduplicates separate UID-less events", () => {
const uidless = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\nBEGIN:VEVENT\r\nDTSTART:20260901T100000Z\r\nDTEND:20260901T110000Z\r\nSUMMARY:No identity\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n`;
const merged = mergeCalendars([
parseCalendar(uidless),
parseCalendar(uidless),
]);
expect(merged.deduplicated).toBe(0);
expect(parseCalendar(merged.source).events).toHaveLength(2);
expect(merged.diagnostics.map((item) => item.message).join(" ")).toMatch(
/UID-less/u,
);
});
it("uses embedded VTIMEZONE observances for custom TZIDs", () => {
const custom = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Custom/Test\r\nBEGIN:STANDARD\r\nDTSTART:19700101T000000\r\nTZOFFSETFROM:+0230\r\nTZOFFSETTO:+0230\r\nTZNAME:CST\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:custom\r\nDTSTART;TZID=Custom/Test:20260901T100000\r\nDTEND;TZID=Custom/Test:20260901T110000\r\nSUMMARY:Custom zone\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n`;
const document = parseCalendar(custom);
expect(document.timezones[0]?.tzid).toBe("Custom/Test");
expect(document.events[0]?.startMs).toBe(
Date.parse("2026-09-01T07:30:00.000Z"),
);
});
it("classifies DST overlaps and reports yearly transitions", () => {
const candidates = localTimeCandidates("2026-10-25T02:30", "Europe/Berlin");
expect(candidates).toHaveLength(2);
expect(timezoneDiagnostics("Europe/Berlin", 2026).transitions).toHaveLength(
2,
);
});
it("rejects unbounded and malformed inputs", () => {
expect(() => previewOccurrences(parseCalendar(recurring), 501, 30)).toThrow(
/1500/u,
);
expect(() => parseCalendar("not a calendar")).toThrow(
/parse failed|root component/u,
);
});
});