import { describe, expect, test } from "bun:test"; import { join } from "node:path"; import { LIEU_PAR_DEFAUT, formaterDate, formaterLieu, trierParDebut, } from "./extension/presentation.js"; // Fixtures = Event littéraux : le parsing est testé dans evenements.test.ts. function evenement(surcharges: Record = {}) { return { uid: "uid-1", titre: "Atelier", description: "", lieu: "", debut: new Date("2026-07-10T16:00:00Z"), fin: new Date("2026-07-10T18:00:00Z"), categories: [], journeeEntiere: false, ...surcharges, }; } describe("trierParDebut", () => { test("ordre mêlé → croissant sur debut", () => { const tardif = evenement({ uid: "tardif", debut: new Date("2026-08-01T10:00:00Z") }); const tot = evenement({ uid: "tot", debut: new Date("2026-07-01T10:00:00Z") }); const median = evenement({ uid: "median", debut: new Date("2026-07-15T10:00:00Z") }); const tries = trierParDebut([tardif, tot, median]); expect(tries.map((e) => e.uid)).toEqual(["tot", "median", "tardif"]); }); test("ne mute pas le tableau d'entrée", () => { const tardif = evenement({ uid: "tardif", debut: new Date("2026-08-01T10:00:00Z") }); const tot = evenement({ uid: "tot", debut: new Date("2026-07-01T10:00:00Z") }); const entree = [tardif, tot]; trierParDebut(entree); expect(entree.map((e) => e.uid)).toEqual(["tardif", "tot"]); }); test("[] → []", () => { expect(trierParDebut([])).toEqual([]); }); test("deux debut identiques → 2 éléments, pas de plantage", () => { const meme = new Date("2026-07-10T16:00:00Z"); const a = evenement({ uid: "a", debut: meme }); const b = evenement({ uid: "b", debut: meme }); expect(trierParDebut([a, b])).toHaveLength(2); }); }); // Fixture UNIQUE de journée entière, partagée par le test en processus et ceux // en sous-processus : ils doivent décrire le même Event, sinon l'un des deux // ment. Composantes LOCALES, jamais un instant absolu ("...T00:00+02:00") : une // date flottante (VALUE=DATE) n'a pas de fuseau et ical.js la matérialise à // minuit local, donc un instant figé ne serait juste qu'à +02:00. const JOURNEE_ENTIERE_DEBUT = [2026, 6, 10] as const; // DTEND est EXCLUSIF (RFC 5545) : le 11 signifie « dernier jour réel = le 10 ». const JOURNEE_ENTIERE_FIN = [2026, 6, 11] as const; const JOUR_ATTENDU = "10 juillet 2026"; function evenementJourneeEntiere() { return evenement({ debut: new Date(...JOURNEE_ENTIERE_DEBUT), fin: new Date(...JOURNEE_ENTIERE_FIN), journeeEntiere: true, }); } describe("formaterDate", () => { test("événement horodaté → date et heure", () => { const rendu = formaterDate(evenement({ debut: new Date("2026-07-10T16:00:00Z") })); expect(rendu).toContain("10 juillet 2026"); expect(rendu).toContain("18:00"); }); test("journée entière → date sans aucune heure", () => { const rendu = formaterDate(evenementJourneeEntiere()); expect(rendu).toContain(JOUR_ATTENDU); expect(rendu).not.toMatch(/\d{1,2}:\d{2}/); }); test("instant à 11:00 UTC → affiché à l'heure de Paris (13:00)", () => { const rendu = formaterDate(evenement({ debut: new Date("2026-07-10T11:00:00Z") })); expect(rendu).toContain("13:00"); }); // Les 6 formes de fin mesurées sur le vrai parseur (cf. rapport de test). // Cas de Pierre : 10:30 → 12:30 le 18/07 (10:30 Paris = 08:30Z). describe("plage horaire", () => { test("début et fin le même jour → « de 10:30 à 12:30 »", () => { const rendu = formaterDate( evenement({ debut: new Date("2026-07-18T08:30:00Z"), fin: new Date("2026-07-18T10:30:00Z"), }), ); expect(rendu).toContain("18 juillet 2026"); expect(rendu).toContain("10:30"); expect(rendu).toContain("12:30"); }); // DTEND absent : le parseur pose fin === debut. « de 10:30 à 10:30 » serait absurde. test("sans fin (fin === debut) → une seule heure affichée", () => { const debut = new Date("2026-07-18T08:30:00Z"); const rendu = formaterDate(evenement({ debut, fin: new Date(debut) })); expect(rendu).toContain("10:30"); expect(rendu.match(/\d{1,2}:\d{2}/g)).toHaveLength(1); }); // 22:00 → 02:00 : « samedi 18 de 22:00 à 02:00 » serait un mensonge. test("franchit minuit → la date de fin est affichée", () => { const rendu = formaterDate( evenement({ debut: new Date("2026-07-18T20:00:00Z"), fin: new Date("2026-07-19T00:00:00Z"), }), ); expect(rendu).toContain("18 juillet 2026"); expect(rendu).toContain("22:00"); expect(rendu).toContain("19 juillet 2026"); expect(rendu).toContain("02:00"); }); test("journée entière d'un jour (DTEND exclusif) → le seul jour réel", () => { const rendu = formaterDate(evenementJourneeEntiere()); expect(rendu).toContain(JOUR_ATTENDU); expect(rendu).not.toContain("11 juillet"); // le DTEND exclusif n'est pas un jour de l'événement expect(rendu).not.toMatch(/\d{1,2}:\d{2}/); }); // Festival 18→21 exclusif : il se termine réellement le 20. test("journée entière sur plusieurs jours → dernier jour réel = DTEND - 1", () => { const rendu = formaterDate( evenement({ debut: new Date(2026, 6, 18), fin: new Date(2026, 6, 21), journeeEntiere: true, }), ); expect(rendu).toContain("18 juillet 2026"); expect(rendu).toContain("20 juillet 2026"); expect(rendu).not.toContain("21 juillet"); expect(rendu).not.toMatch(/\d{1,2}:\d{2}/); }); }); // Le fuseau des tests est épinglé à Europe/Paris : c'est justement le seul où // une journée entière mal formatée reste invisible. On sous-traite donc à des // processus réglés ailleurs. describe.each(["Asia/Tokyo", "Pacific/Auckland", "America/New_York", "Europe/Paris"])( "sous TZ=%s", (fuseau) => { test("journée entière → la date ne glisse pas d'un jour", async () => { expect(await formaterDansLeFuseau(fuseau)).toContain(JOUR_ATTENDU); }); }, ); }); // Le fixture traverse la frontière de processus en code source : c'est la même // constante JOURNEE_ENTIERE, donc les deux tests ne peuvent pas diverger. async function formaterDansLeFuseau(fuseau: string): Promise { const module = JSON.stringify(join(import.meta.dir, "extension/presentation.js")); const processus = Bun.spawn( [ "bun", "-e", `import { formaterDate } from ${module}; console.log(formaterDate({ debut: new Date(${JOURNEE_ENTIERE_DEBUT.join(", ")}), fin: new Date(${JOURNEE_ENTIERE_FIN.join(", ")}), journeeEntiere: true, }));`, ], { env: { ...process.env, TZ: fuseau }, stdout: "pipe", stderr: "pipe" }, ); const [sortie, erreur, code] = await Promise.all([ new Response(processus.stdout).text(), new Response(processus.stderr).text(), processus.exited, ]); if (code !== 0) throw new Error(`bun -e a échoué (${code}) : ${erreur}`); return sortie; } describe("formaterLieu", () => { test("lieu renseigné → rendu tel quel", () => { expect(formaterLieu(evenement({ lieu: "Salle des fêtes" }))).toBe("Salle des fêtes"); }); test("lieu vide → adresse du café", () => { expect(formaterLieu(evenement({ lieu: "" }))).toBe(LIEU_PAR_DEFAUT); }); test("lieu en blancs → adresse du café", () => { expect(formaterLieu(evenement({ lieu: " " }))).toBe(LIEU_PAR_DEFAUT); }); });