import { describe, expect, it } from "@suaveplan/testing/runner"; import * as fc from "fast-check"; import { formatRRule, parseRRule } from "./recurrence.js"; describe("parseRRule", () => { it("parses FREQ=DAILY", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=DAILY"); expect(rule.freq).toBe("daily"); }); it("parses FREQ=WEEKLY", { timeout: 5000 }, () => { expect(parseRRule("FREQ=WEEKLY").freq).toBe("weekly"); }); it("parses FREQ=MONTHLY", { timeout: 5000 }, () => { expect(parseRRule("FREQ=MONTHLY").freq).toBe("monthly"); }); it("parses FREQ=YEARLY", { timeout: 5000 }, () => { expect(parseRRule("FREQ=YEARLY").freq).toBe("yearly"); }); it("parses INTERVAL", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=WEEKLY;INTERVAL=2"); expect(rule.interval).toBe(2); }); it("parses COUNT", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=DAILY;COUNT=5"); expect(rule.count).toBe(5); }); it("parses UNTIL (YYYYMMDD)", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=DAILY;UNTIL=20241231"); expect(rule.until?.getFullYear()).toBe(2024); expect(rule.until?.getMonth()).toBe(11); expect(rule.until?.getDate()).toBe(31); }); it("parses UNTIL with time suffix", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=DAILY;UNTIL=20241231T000000Z"); expect(rule.until?.getFullYear()).toBe(2024); }); it("parses BYDAY", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=WEEKLY;BYDAY=MO,WE,FR"); expect(rule.byDay).toEqual([1, 3, 5]); }); it("parses BYMONTHDAY", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=MONTHLY;BYMONTHDAY=15"); expect(rule.byMonthDay).toEqual([15]); }); it("ignores parts without '=' sign", { timeout: 5000 }, () => { // Covers the `if (eq === -1) continue` branch in the parser loop const rule = parseRRule("FREQ=DAILY;BADPART;COUNT=3"); expect(rule.freq).toBe("daily"); expect(rule.count).toBe(3); }); it("throws for missing FREQ", { timeout: 5000 }, () => { let err: unknown; try { parseRRule("INTERVAL=2"); } catch (e) { err = e; } expect((err as { code?: string }).code).toBe( "DATETIME.PARSE.INVALID_INPUT" ); }); it("throws for unknown FREQ", { timeout: 5000 }, () => { let err: unknown; try { parseRRule("FREQ=HOURLY"); } catch (e) { err = e; } expect((err as { code?: string }).code).toBe( "DATETIME.PARSE.INVALID_INPUT" ); }); it("handles all day abbreviations", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=WEEKLY;BYDAY=SU,MO,TU,WE,TH,FR,SA"); expect(rule.byDay).toEqual([0, 1, 2, 3, 4, 5, 6]); }); }); describe("formatRRule", () => { it("formats basic rule", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=DAILY;COUNT=5"); expect(formatRRule(rule)).toBe("FREQ=DAILY;COUNT=5"); }); it("formats UNTIL", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=WEEKLY;UNTIL=20241231"); const formatted = formatRRule(rule); expect(formatted).toContain("UNTIL=20241231"); }); it("formats BYDAY", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=6"); const formatted = formatRRule(rule); expect(formatted).toContain("BYDAY=MO,WE,FR"); expect(formatted).toContain("FR"); }); it("formats all day abbreviations round-trip", { timeout: 5000 }, () => { const rule = parseRRule( "FREQ=WEEKLY;BYDAY=SU,MO,TU,WE,TH,FR,SA;COUNT=1" ); const formatted = formatRRule(rule); expect(formatted).toContain("SU"); expect(formatted).toContain("MO"); expect(formatted).toContain("TU"); expect(formatted).toContain("WE"); expect(formatted).toContain("TH"); expect(formatted).toContain("FR"); expect(formatted).toContain("SA"); }); it("formats BYMONTHDAY", { timeout: 5000 }, () => { const rule = parseRRule("FREQ=MONTHLY;BYMONTHDAY=15;COUNT=3"); expect(formatRRule(rule)).toContain("BYMONTHDAY=15"); }); it("formats INTERVAL", { timeout: 5000 }, () => { // Ensures the `if (rule.interval !== undefined)` branch is taken const rule = parseRRule("FREQ=WEEKLY;INTERVAL=2;COUNT=4"); const formatted = formatRRule(rule); expect(formatted).toContain("INTERVAL=2"); }); it("parseRRule → formatRRule → parseRRule identity (property test)", { timeout: 30000, }, () => { const freqs = ["DAILY", "WEEKLY", "MONTHLY", "YEARLY"] as const; fc.assert( fc.property( fc.constantFrom(...freqs), fc.integer({ min: 1, max: 10 }), (freq, count) => { const input = `FREQ=${freq};COUNT=${count}`; const parsed = parseRRule(input); const formatted = formatRRule(parsed); const reparsed = parseRRule(formatted); expect(reparsed.freq).toBe(parsed.freq); expect(reparsed.count).toBe(parsed.count); } ), { numRuns: 100 } ); }); });