import { describe, expect, it } from "vitest"; import { parseRRule, nextOccurrence, validateMinInterval, MIN_INTERVAL_MS } from "./rrule.js"; describe("parseRRule", () => { it("accepts a daily rule", () => { expect(() => parseRRule("FREQ=DAILY;BYHOUR=9;BYMINUTE=0")).not.toThrow(); }); it("rejects invalid syntax", () => { expect(() => parseRRule("not-a-rule")).toThrow(); }); }); describe("nextOccurrence", () => { it("returns the next firing time after `after`", () => { const rule = "FREQ=DAILY;BYHOUR=9;BYMINUTE=0"; const after = new Date("2026-05-03T08:00:00Z"); const next = nextOccurrence(rule, "Asia/Kuala_Lumpur", after); expect(next).toBeInstanceOf(Date); expect(next!.getTime()).toBeGreaterThan(after.getTime()); }); it("returns null when the rule has no further occurrences", () => { const past = "DTSTART:20200101T000000Z\nRRULE:FREQ=DAILY;COUNT=1"; expect(nextOccurrence(past, "Asia/Kuala_Lumpur", new Date())).toBeNull(); }); }); describe("validateMinInterval", () => { it("accepts a daily rule (interval > 5 min)", () => { expect(validateMinInterval("FREQ=DAILY;BYHOUR=9;BYMINUTE=0", "Asia/Kuala_Lumpur")) .toEqual({ ok: true }); }); it("rejects a rule firing every minute", () => { const result = validateMinInterval("FREQ=MINUTELY", "Asia/Kuala_Lumpur"); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toMatch(/minimum interval/i); } }); }); describe("MIN_INTERVAL_MS", () => { it("equals 5 minutes", () => { expect(MIN_INTERVAL_MS).toBe(5 * 60 * 1000); }); });