Release Calendar Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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(
|
||||
/1–500/u,
|
||||
);
|
||||
expect(() => parseCalendar("not a calendar")).toThrow(
|
||||
/parse failed|root component/u,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user