import { describe, expect, test } from "bun:test"; import { parserEvenements } from "./extension/evenements.js"; // Fixtures ICS inline et minimalistes (jamais l'export réel : données perso). // `aujourdhui` est injecté pour des tests déterministes. const AUJOURDHUI = new Date("2026-06-30T12:00:00Z"); function vcalendar(...lignes: string[]): string { return ["BEGIN:VCALENDAR", "VERSION:2.0", ...lignes, "END:VCALENDAR"].join( "\r\n", ) + "\r\n"; } function vevent(...lignes: string[]): string { return ["BEGIN:VEVENT", ...lignes, "END:VEVENT"].join("\r\n"); } const VTIMEZONE_PARIS = [ "BEGIN:VTIMEZONE", "TZID:Europe/Paris", "BEGIN:DAYLIGHT", "TZOFFSETFROM:+0100", "TZOFFSETTO:+0200", "TZNAME:CEST", "DTSTART:19700329T020000", "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU", "END:DAYLIGHT", "BEGIN:STANDARD", "TZOFFSETFROM:+0200", "TZOFFSETTO:+0100", "TZNAME:CET", "DTSTART:19701025T030000", "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU", "END:STANDARD", "END:VTIMEZONE", ]; const VTIMEZONE_LAGOS = [ "BEGIN:VTIMEZONE", "TZID:Africa/Lagos", "BEGIN:STANDARD", "TZOFFSETFROM:+0100", "TZOFFSETTO:+0100", "TZNAME:WAT", "DTSTART:19700101T000000", "END:STANDARD", "END:VTIMEZONE", ]; describe("parserEvenements", () => { test("VEVENT complet → tous les champs mappés", () => { const flux = vcalendar( ...VTIMEZONE_PARIS, vevent( "UID:complet@8", "SUMMARY:Atelier lecture", "DESCRIPTION:Venez nombreux", "LOCATION:Café du Huit", "CATEGORIES:Culture", "DTSTART;TZID=Europe/Paris:20260710T180000", "DTEND;TZID=Europe/Paris:20260710T200000", ), ); const [event] = parserEvenements(flux, AUJOURDHUI); expect(event.uid).toBe("complet@8"); expect(event.titre).toBe("Atelier lecture"); expect(event.description).toBe("Venez nombreux"); expect(event.lieu).toBe("Café du Huit"); expect(event.categories).toEqual(["Culture"]); expect(event.journeeEntiere).toBe(false); expect(event.debut.toISOString()).toBe("2026-07-10T16:00:00.000Z"); expect(event.fin.toISOString()).toBe("2026-07-10T18:00:00.000Z"); }); test("SUMMARY / DESCRIPTION / LOCATION absents → chaînes vides", () => { const flux = vcalendar( vevent("UID:vide@8", "DTSTART:20260710T180000"), ); const [event] = parserEvenements(flux, AUJOURDHUI); expect(event.titre).toBe(""); expect(event.description).toBe(""); expect(event.lieu).toBe(""); }); describe("CATEGORIES", () => { test("une valeur → tableau à un élément", () => { const flux = vcalendar( vevent("UID:cat1@8", "DTSTART:20260710T180000", "CATEGORIES:Culture"), ); expect(parserEvenements(flux, AUJOURDHUI)[0].categories).toEqual([ "Culture", ]); }); test("plusieurs valeurs séparées par virgule → tableau", () => { const flux = vcalendar( vevent("UID:cat2@8", "DTSTART:20260710T180000", "CATEGORIES:A,B"), ); expect(parserEvenements(flux, AUJOURDHUI)[0].categories).toEqual([ "A", "B", ]); }); test("absent → tableau vide", () => { const flux = vcalendar( vevent("UID:cat0@8", "DTSTART:20260710T180000"), ); expect(parserEvenements(flux, AUJOURDHUI)[0].categories).toEqual([]); }); }); test("dédoublonnage : maître + RECURRENCE-ID même UID → 1 Event (le maître)", () => { const flux = vcalendar( vevent( "UID:recur@8", "SUMMARY:Maître", "DTSTART:20260710T180000", "RRULE:FREQ=WEEKLY", ), vevent( "UID:recur@8", "SUMMARY:Exception", "RECURRENCE-ID:20260717T180000", "DTSTART:20260717T190000", ), ); const events = parserEvenements(flux, AUJOURDHUI); expect(events).toHaveLength(1); expect(events[0].titre).toBe("Maître"); }); describe("filtre futur (aujourd'hui inclus)", () => { test("hier → exclu", () => { const flux = vcalendar( vevent("UID:hier@8", "DTSTART:20260629T180000"), ); expect(parserEvenements(flux, AUJOURDHUI)).toHaveLength(0); }); test("aujourd'hui → inclus", () => { const flux = vcalendar( vevent("UID:auj@8", "DTSTART:20260630T080000"), ); expect(parserEvenements(flux, AUJOURDHUI)).toHaveLength(1); }); test("demain → inclus", () => { const flux = vcalendar( vevent("UID:demain@8", "DTSTART:20260701T180000"), ); expect(parserEvenements(flux, AUJOURDHUI)).toHaveLength(1); }); }); describe("journée entière", () => { test("DTSTART;VALUE=DATE → journeeEntiere true", () => { const flux = vcalendar( vevent("UID:jour@8", "DTSTART;VALUE=DATE:20260701"), ); expect(parserEvenements(flux, AUJOURDHUI)[0].journeeEntiere).toBe(true); }); test("événement horodaté → journeeEntiere false", () => { const flux = vcalendar( vevent("UID:heure@8", "DTSTART:20260701T180000"), ); expect(parserEvenements(flux, AUJOURDHUI)[0].journeeEntiere).toBe(false); }); }); test("fuseau Europe/Paris (VTIMEZONE) → instant absolu correct", () => { const flux = vcalendar( ...VTIMEZONE_PARIS, vevent("UID:paris@8", "DTSTART;TZID=Europe/Paris:20260710T120000"), ); // Été : Paris = UTC+2 → 12:00 local = 10:00 UTC. expect(parserEvenements(flux, AUJOURDHUI)[0].debut.toISOString()).toBe( "2026-07-10T10:00:00.000Z", ); }); test("fuseau Africa/Lagos (VTIMEZONE) → offset distinct de Paris", () => { const flux = vcalendar( ...VTIMEZONE_LAGOS, vevent("UID:lagos@8", "DTSTART;TZID=Africa/Lagos:20260710T120000"), ); // Lagos = UTC+1 toute l'année → 12:00 local = 11:00 UTC (≠ Paris). expect(parserEvenements(flux, AUJOURDHUI)[0].debut.toISOString()).toBe( "2026-07-10T11:00:00.000Z", ); }); test("line-folding + échappements (\\, \\n) → décodés", () => { const flux = vcalendar( vevent( "UID:fold@8", "DTSTART:20260710T180000", // Ligne repliée (continuation par CRLF + espace) + échappements. "SUMMARY:Café\\, lecture et tr\r\n ès longue suite\\nsur deux lignes", ), ); expect(parserEvenements(flux, AUJOURDHUI)[0].titre).toBe( "Café, lecture et très longue suite\nsur deux lignes", ); }); test("DTEND absent → fin dérivée sans planter (fin >= debut)", () => { const flux = vcalendar( vevent("UID:nodtend@8", "DTSTART:20260710T180000"), ); const [event] = parserEvenements(flux, AUJOURDHUI); expect(event.fin.getTime()).toBeGreaterThanOrEqual(event.debut.getTime()); }); describe("récurrent (RRULE) — comportement T1 sans expansion", () => { test("maître futur → présent une seule fois", () => { const flux = vcalendar( vevent( "UID:rfutur@8", "DTSTART:20260710T180000", "RRULE:FREQ=WEEKLY", ), ); expect(parserEvenements(flux, AUJOURDHUI)).toHaveLength(1); }); test("maître passé → absent (trou T1 assumé)", () => { const flux = vcalendar( vevent( "UID:rpasse@8", "DTSTART:20260101T180000", "RRULE:FREQ=WEEKLY", ), ); expect(parserEvenements(flux, AUJOURDHUI)).toHaveLength(0); }); }); test("flux sans VEVENT / VCALENDAR vide → []", () => { expect(parserEvenements(vcalendar(), AUJOURDHUI)).toEqual([]); }); });