Files
echo-du-huit/presentation.test.ts
Pierre Martin 69c10abbd7 feat: let the list ignore, submit and undo events from Nextcloud
Each row now shows its shared status and carries the matching gesture. Nothing
is displayed before Nextcloud confirms it: on failure the status stays put and
the reason is spelled out. Ignored events are greyed and pushed to the end,
never hidden.
2026-08-13 16:50:16 +02:00

259 lines
9.0 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { join } from "node:path";
import {
LIEU_PAR_DEFAUT,
formaterDate,
formaterLieu,
formaterStatut,
repousserIgnores,
trierParDebut,
} from "./extension/presentation.js";
// Fixtures = Event littéraux : le parsing est testé dans evenements.test.ts.
function evenement(surcharges: Record<string, unknown> = {}) {
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<string> {
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);
});
});
describe("formaterStatut", () => {
test("soumis avec date → « Soumis le JJ/MM »", () => {
expect(formaterStatut({ type: "soumis", jour: "2026-08-13" })).toBe("Soumis le 13/08");
});
test("soumis sans date exploitable → « Soumis »", () => {
expect(formaterStatut({ type: "soumis", jour: null })).toBe("Soumis");
});
test("ignoré → « Ignoré » ; aucun → rien à afficher", () => {
expect(formaterStatut({ type: "ignoré" })).toBe("Ignoré");
expect(formaterStatut({ type: "aucun" })).toBe("");
});
});
describe("repousserIgnores", () => {
test("les ignorés passent en fin, l'ordre relatif est conservé", () => {
const evenements = [
evenement({ uid: "a" }),
evenement({ uid: "ignore-1", categories: ["mairie:ignoré"] }),
evenement({ uid: "b", categories: ["Concert"] }),
evenement({ uid: "ignore-2", categories: ["Concert", "mairie:ignoré"] }),
evenement({ uid: "c", categories: ["mairie:soumis:2026-08-13"] }),
];
expect(repousserIgnores(evenements).map((e) => e.uid)).toEqual([
"a",
"b",
"c",
"ignore-1",
"ignore-2",
]);
});
test("liste sans ignoré → inchangée, sans mutation de l'entrée", () => {
const evenements = [evenement({ uid: "a" }), evenement({ uid: "b" })];
expect(repousserIgnores(evenements).map((e) => e.uid)).toEqual(["a", "b"]);
expect(evenements.map((e) => e.uid)).toEqual(["a", "b"]);
});
});