feat: compute the mairie form values from a pending event
Pure, testable core of the prefill: payload + config -> flat values ready to lay down. What the machine does not know stays empty and is listed in aCompleter, never invented (no fake 00:00, no made-up coordinates). DTEND is exclusive (RFC 5545): a festival on the 18th-20th carries DTEND=21, so the end date must step back one day, in local components for all-day events. Without it, a wrong date reaches a moderated form. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b103ab78ec
commit
78153557dd
@@ -0,0 +1,283 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import "./extension/config.js";
|
||||
import "./extension/formulaire-valeurs.js";
|
||||
import { CLE_EVENEMENT_EN_ATTENTE, serialiserPourFormulaire } from "./extension/transfert.js";
|
||||
|
||||
const { DEFAUTS } = (globalThis as any).EchoConfig;
|
||||
const { calculerValeurs } = (globalThis as any).EchoFormulaire;
|
||||
|
||||
// Payload = ce que dépose transfert.js (debut/fin en ISO), pas un Event.
|
||||
function payload(surcharges: Record<string, unknown> = {}) {
|
||||
return {
|
||||
uid: "uid-1",
|
||||
titre: "Atelier",
|
||||
description: "Venez nombreux",
|
||||
lieu: "",
|
||||
debut: new Date("2026-07-18T08:30:00Z").toISOString(),
|
||||
fin: new Date("2026-07-18T10:30:00Z").toISOString(),
|
||||
categories: [],
|
||||
journeeEntiere: false,
|
||||
...surcharges,
|
||||
};
|
||||
}
|
||||
|
||||
// Une journée entière est une date FLOTTANTE : fixture en composantes locales,
|
||||
// jamais un instant absolu (cf. DECISIONS.md 2026-07-14).
|
||||
function payloadJourneeEntiere(debut: Date, fin: Date, surcharges = {}) {
|
||||
return payload({
|
||||
debut: debut.toISOString(),
|
||||
fin: fin.toISOString(),
|
||||
journeeEntiere: true,
|
||||
...surcharges,
|
||||
});
|
||||
}
|
||||
|
||||
describe("journée entière", () => {
|
||||
test("un seul jour (DTEND = J+1) → dateFin === dateDebut", () => {
|
||||
const valeurs = calculerValeurs(
|
||||
payloadJourneeEntiere(new Date(2026, 6, 10), new Date(2026, 6, 11)),
|
||||
DEFAUTS,
|
||||
);
|
||||
|
||||
expect(valeurs.dateDebut).toBe("10/07/2026");
|
||||
expect(valeurs.dateFin).toBe("10/07/2026");
|
||||
});
|
||||
|
||||
test("lit les composantes locales, pas un instant absolu", () => {
|
||||
const valeurs = calculerValeurs(
|
||||
payloadJourneeEntiere(new Date(2026, 6, 10), new Date(2026, 6, 11)),
|
||||
DEFAUTS,
|
||||
);
|
||||
|
||||
expect(valeurs.dateDebut).toBe("10/07/2026");
|
||||
});
|
||||
|
||||
// Festival 18→21 exclusif : il se termine réellement le 20. Sans ce recul,
|
||||
// une date FAUSSE part à une mairie qui modère.
|
||||
test("plusieurs jours (18 → DTEND 21) → dateFin = 20/07/2026", () => {
|
||||
const valeurs = calculerValeurs(
|
||||
payloadJourneeEntiere(new Date(2026, 6, 18), new Date(2026, 6, 21)),
|
||||
DEFAUTS,
|
||||
);
|
||||
|
||||
expect(valeurs.dateDebut).toBe("18/07/2026");
|
||||
expect(valeurs.dateFin).toBe("20/07/2026");
|
||||
});
|
||||
|
||||
test("DTEND absent (fin === debut) → dateFin === dateDebut", () => {
|
||||
const valeurs = calculerValeurs(
|
||||
payloadJourneeEntiere(new Date(2026, 6, 10), new Date(2026, 6, 10)),
|
||||
DEFAUTS,
|
||||
);
|
||||
|
||||
expect(valeurs.dateFin).toBe("10/07/2026");
|
||||
});
|
||||
|
||||
// Poser « 00:00 » serait le mensonge que presentation.js refuse déjà.
|
||||
test("aucune heure : poser 00:00 serait un mensonge", () => {
|
||||
const valeurs = calculerValeurs(
|
||||
payloadJourneeEntiere(new Date(2026, 6, 10), new Date(2026, 6, 11)),
|
||||
DEFAUTS,
|
||||
);
|
||||
|
||||
expect(valeurs.heureDebut).toBeNull();
|
||||
expect(valeurs.heureFin).toBeNull();
|
||||
});
|
||||
|
||||
test("l'heure manquante est signalée", () => {
|
||||
const valeurs = calculerValeurs(
|
||||
payloadJourneeEntiere(new Date(2026, 6, 10), new Date(2026, 6, 11)),
|
||||
DEFAUTS,
|
||||
);
|
||||
|
||||
expect(valeurs.aCompleter).toContain("l'heure");
|
||||
});
|
||||
});
|
||||
|
||||
describe("événement horodaté", () => {
|
||||
test("heures à Paris, zéro-paddées", () => {
|
||||
const valeurs = calculerValeurs(
|
||||
payload({
|
||||
debut: new Date("2026-07-18T07:05:00Z").toISOString(), // 09:05 à Paris
|
||||
fin: new Date("2026-07-18T19:00:00Z").toISOString(), // 21:00 à Paris
|
||||
}),
|
||||
DEFAUTS,
|
||||
);
|
||||
|
||||
expect(valeurs.heureDebut).toEqual(["09", "05"]);
|
||||
expect(valeurs.heureFin).toEqual(["21", "00"]);
|
||||
expect(valeurs.dateDebut).toBe("18/07/2026");
|
||||
expect(valeurs.dateFin).toBe("18/07/2026");
|
||||
});
|
||||
|
||||
test("DTEND absent (fin === debut) → pas d'heure de fin, mais le jour reste vrai", () => {
|
||||
const debut = new Date("2026-07-18T08:30:00Z").toISOString();
|
||||
const valeurs = calculerValeurs(payload({ debut, fin: debut }), DEFAUTS);
|
||||
|
||||
expect(valeurs.heureDebut).toEqual(["10", "30"]);
|
||||
expect(valeurs.heureFin).toBeNull();
|
||||
expect(valeurs.dateFin).toBe("18/07/2026");
|
||||
expect(valeurs.aCompleter).toContain("l'heure");
|
||||
});
|
||||
|
||||
// 22:00 → 02:00 : annoncer la fin le même jour serait un mensonge.
|
||||
test("franchit minuit → dateFin est le lendemain", () => {
|
||||
const valeurs = calculerValeurs(
|
||||
payload({
|
||||
debut: new Date("2026-07-18T20:00:00Z").toISOString(), // 22:00 à Paris
|
||||
fin: new Date("2026-07-19T00:00:00Z").toISOString(), // 02:00 le 19 à Paris
|
||||
}),
|
||||
DEFAUTS,
|
||||
);
|
||||
|
||||
expect(valeurs.dateDebut).toBe("18/07/2026");
|
||||
expect(valeurs.heureDebut).toEqual(["22", "00"]);
|
||||
expect(valeurs.dateFin).toBe("19/07/2026");
|
||||
expect(valeurs.heureFin).toEqual(["02", "00"]);
|
||||
});
|
||||
|
||||
test("heures complètes → l'heure n'est pas dans aCompleter", () => {
|
||||
const valeurs = calculerValeurs(payload(), DEFAUTS);
|
||||
|
||||
expect(valeurs.aCompleter).not.toContain("l'heure");
|
||||
});
|
||||
});
|
||||
|
||||
describe("lieu", () => {
|
||||
test("lieu vide (cas majoritaire) → adresse et coordonnées du café", () => {
|
||||
const valeurs = calculerValeurs(payload({ lieu: "" }), DEFAUTS);
|
||||
|
||||
expect(valeurs.adresse).toBe("8 Rue du Pré Vicinal 31270 Cugnaux");
|
||||
expect(valeurs.coordonnees).toBe(
|
||||
"43.53753132806989|1.342981890597508|8 Rue du Pré Vicinal 31270 Cugnaux",
|
||||
);
|
||||
expect(valeurs.aCompleter).not.toContain("le lieu exact sur la carte");
|
||||
});
|
||||
|
||||
test("lieu en blancs → café", () => {
|
||||
const valeurs = calculerValeurs(payload({ lieu: " " }), DEFAUTS);
|
||||
|
||||
expect(valeurs.adresse).toBe("8 Rue du Pré Vicinal 31270 Cugnaux");
|
||||
});
|
||||
|
||||
// On ne sait pas géocoder : inventer un lat|lng serait pire que le laisser vide.
|
||||
test("lieu renseigné → pas de coordonnées inventées, manque signalé", () => {
|
||||
const valeurs = calculerValeurs(payload({ lieu: "Salle des fêtes" }), DEFAUTS);
|
||||
|
||||
expect(valeurs.adresse).toBe("Salle des fêtes");
|
||||
expect(valeurs.coordonnees).toBe("");
|
||||
expect(valeurs.aCompleter).toContain("le lieu exact sur la carte");
|
||||
});
|
||||
});
|
||||
|
||||
describe("champs de config", () => {
|
||||
test("thème et tarifs viennent de la config", () => {
|
||||
const valeurs = calculerValeurs(payload(), DEFAUTS);
|
||||
|
||||
expect(valeurs.theme).toBe("Culture");
|
||||
expect(valeurs.tarifs).toBe("Gratuit");
|
||||
});
|
||||
|
||||
test("organisateur et email surchargés sont posés et non signalés", () => {
|
||||
const config = { ...DEFAUTS, organisateur: "L'Atelier du Huit", email: "a@b.fr" };
|
||||
|
||||
const valeurs = calculerValeurs(payload(), config);
|
||||
|
||||
expect(valeurs.organisateur).toBe("L'Atelier du Huit");
|
||||
expect(valeurs.email).toBe("a@b.fr");
|
||||
expect(valeurs.aCompleter).not.toContain("l'organisateur et l'email (config.local.json)");
|
||||
});
|
||||
|
||||
test("sans config.local.json → organisateur/email vides et signalés", () => {
|
||||
const valeurs = calculerValeurs(payload(), DEFAUTS);
|
||||
|
||||
expect(valeurs.organisateur).toBe("");
|
||||
expect(valeurs.email).toBe("");
|
||||
expect(valeurs.aCompleter).toContain("l'organisateur et l'email (config.local.json)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("aCompleter", () => {
|
||||
test("signale toujours l'image et le thème", () => {
|
||||
const config = { ...DEFAUTS, organisateur: "X", email: "a@b.fr" };
|
||||
|
||||
const valeurs = calculerValeurs(payload({ lieu: "" }), config);
|
||||
|
||||
expect(valeurs.aCompleter).toContain("l'image (obligatoire)");
|
||||
expect(valeurs.aCompleter).toContain("le thème");
|
||||
});
|
||||
|
||||
test("titre vide → signalé", () => {
|
||||
expect(calculerValeurs(payload({ titre: "" }), DEFAUTS).aCompleter).toContain("le titre");
|
||||
});
|
||||
|
||||
test("titre renseigné → non signalé", () => {
|
||||
expect(calculerValeurs(payload(), DEFAUTS).aCompleter).not.toContain("le titre");
|
||||
});
|
||||
});
|
||||
|
||||
describe("texte", () => {
|
||||
test("titre et description sont repris tels quels", () => {
|
||||
const valeurs = calculerValeurs(payload(), DEFAUTS);
|
||||
|
||||
expect(valeurs.titre).toBe("Atelier");
|
||||
expect(valeurs.description).toBe("Venez nombreux");
|
||||
});
|
||||
|
||||
// Cas majoritaire (142/473 ont une DESCRIPTION) : le poseur n'écrira pas "".
|
||||
test("description vide reste vide", () => {
|
||||
expect(calculerValeurs(payload({ description: "" }), DEFAUTS).description).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
// La constante est dupliquée dans formulaire-valeurs.js (un content script
|
||||
// classique ne peut pas importer transfert.js, qui est en ESM). Ce test est le
|
||||
// garde-fou contre la dérive.
|
||||
describe("couture avec transfert.js", () => {
|
||||
test("la clé dupliquée est identique à l'originale", () => {
|
||||
expect((globalThis as any).EchoFormulaire.CLE_EVENEMENT_EN_ATTENTE).toBe(
|
||||
CLE_EVENEMENT_EN_ATTENTE,
|
||||
);
|
||||
});
|
||||
|
||||
// Le contrat de couture vérifié bout en bout, pas supposé.
|
||||
test("aller-retour réel : serialiserPourFormulaire → calculerValeurs", () => {
|
||||
const evenement = {
|
||||
uid: "uid-1",
|
||||
titre: "Concert",
|
||||
description: "Au café",
|
||||
lieu: "",
|
||||
debut: new Date("2026-07-18T18:00:00Z"), // 20:00 à Paris
|
||||
fin: new Date("2026-07-18T20:00:00Z"), // 22:00 à Paris
|
||||
categories: [],
|
||||
journeeEntiere: false,
|
||||
};
|
||||
|
||||
const valeurs = calculerValeurs(serialiserPourFormulaire(evenement), DEFAUTS);
|
||||
|
||||
expect(valeurs.titre).toBe("Concert");
|
||||
expect(valeurs.dateDebut).toBe("18/07/2026");
|
||||
expect(valeurs.heureDebut).toEqual(["20", "00"]);
|
||||
expect(valeurs.heureFin).toEqual(["22", "00"]);
|
||||
expect(valeurs.adresse).toBe("8 Rue du Pré Vicinal 31270 Cugnaux");
|
||||
});
|
||||
|
||||
test("aller-retour réel d'une journée entière (date flottante)", () => {
|
||||
const evenement = {
|
||||
uid: "uid-2",
|
||||
titre: "Festival",
|
||||
description: "",
|
||||
lieu: "",
|
||||
debut: new Date(2026, 6, 18),
|
||||
fin: new Date(2026, 6, 21),
|
||||
categories: [],
|
||||
journeeEntiere: true,
|
||||
};
|
||||
|
||||
const valeurs = calculerValeurs(serialiserPourFormulaire(evenement), DEFAUTS);
|
||||
|
||||
expect(valeurs.dateDebut).toBe("18/07/2026");
|
||||
expect(valeurs.dateFin).toBe("20/07/2026");
|
||||
expect(valeurs.heureDebut).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user