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.
259 lines
9.0 KiB
TypeScript
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"]);
|
|
});
|
|
});
|