Add a pure parser parserEvenements(flux, aujourdhui) backed by vendored
ical.js v2.2.1 (ESM, no build). Returns events at the frozen Event contract
{ uid, titre, description, lieu, debut, fin, categories, journeeEntiere }:
- register embedded VTIMEZONE before toJSDate (correct offset, e.g. Africa/Lagos)
- dedupe by UID, keep the master (RECURRENCE-ID exceptions ignored)
- filter to today and future (local start of day)
- coerce missing text fields to ""
Edge cases deferred to T3 (RRULE expansion, all-day hour) are documented.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
244 lines
7.4 KiB
TypeScript
244 lines
7.4 KiB
TypeScript
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([]);
|
|
});
|
|
});
|