12 KiB
Solution : [T1] Page liste : afficher les événements + bouton Créer
Problème résolu
Câbler le parseur (evenements.js, figé en T1 précédente mais branché nulle
part) à la page : afficher les événements futurs triés, et poser la couture
page → content script pour le pré-remplissage du formulaire mairie (tâche sœur).
Sous un ticket d'apparence UI se cachaient deux décisions structurantes :
- Où vivent le fetch et le parsing ? L'énoncé disait « passage background (CalDAV+parsing) → page », mais le code réel fetchait déjà dans la page. La question n'était donc pas comment transporter, mais faut-il introduire ce transport.
- Que transmet le bouton « Créer » ? Le remplissage appartient à la tâche sœur ; T1 devait poser la couture, pas le remplissage.
Le coût d'une erreur ici n'était pas dans T1 mais dans T2 (statuts CalDAV) et dans la tâche content script, qui héritent du chemin de données choisi.
Approche choisie
A + H1 : fetch et parsing restent dans la page ; storage.local sous une
clé unique pour la seule frontière irréductible (page → content script).
Pourquoi pas les alternatives :
- Background fetch+parse par message (B) : introduit une frontière de
sérialisation gratuite. Firefox préserve les
Date(structured clone), Chrome les JSON-ise : faire reposer le contratEventfraîchement figé sur un détail du moteur = dette silencieuse. Zéro besoin actuel —background.jsgarde son unique rôle (ouvrir l'onglet) et n'a pas été modifié. - Background →
storage.session(C) : le storage JSON-ise →debut/findeviennent des chaînes, réhydratation partout, contrat entamé. Plus invalidation/fraîcheur à gérer. Sur-ingénierie pour une liste ouverte à la demande. - Clé
evenement:<uid>(couture annoncée par la tâche sœur) : inutilisable en l'état — le content script s'exécute sur le site mairie et n'a aucun canal pour apprendre unuid(le fragment d'URL a été écarté). La couture nommée par la tâche sœur avait un trou ; T1 l'a révisée (voirDECISIONS.md). tabs.sendMessageau chargement : course page/content script à arbitrer, et un F5 sur le formulaire (modéré, on y revient) perd le contexte.
Le principe : n'introduire une frontière que quand elle sert quelqu'un. Le background reste disponible ; quand T2 aura un vrai besoin d'orchestration, il sera introduit pour une raison, pas par anticipation.
Décisions clés
Actées en détail dans docs/DECISIONS.md (2026-07-14) — à lire avant la tâche
sœur ou T2. En résumé :
- Modules purs extraits (
presentation.js,transfert.js) plutôt que tout dansliste.js: ce dernier touchedocumentdès l'import, donc l'importer dans Bun plante — y enterrer tri/formatage/défaut aurait rendu de la logique métier réelle intestable. Chaque module existe parce qu'il rend testable une décision métier (l'ordre, le mensonge « 00:00 », le contrat de couture). - Clé unique
evenement-en-attente, pas indexée par uid ; on n'efface pas à la lecture (le formulaire survit à un F5) ; le dernier clic gagne (assumé : le geste réel est séquentiel). - Sérialisation ISO explicite : on ne parie pas sur le structured clone.
- Défaut café = affichage seulement,
lieustocké brut (""si absent) : écrire l'adresse dans le payload détruirait le signal « pas de lieu » dont le content script a besoin pour décider s'il pose aussi les coordonnées. awaitle dépôt AVANTtabs.create: sinon course — c'est précisément ce que la couture achète (« la donnée attend le content script »).- Permission
storageseule ;"tabs"correctement écartée (tabs.createn'en a pas besoin — confirmé en Firefox réel).
Patterns à réutiliser
Injecter la dépendance impure, garder le module pur (convention du repo :
fetchImpl, aujourdhui, et maintenant storage) — paramètre obligatoire,
testable sans navigateur :
export async function deposerEvenement(storage, evenement) {
await storage.set({ [CLE_EVENEMENT_EN_ATTENTE]: serialiserPourFormulaire(evenement) });
}
// appel réel : deposerEvenement(browser.storage.local, evenement)
Le point d'entrée injecte le « maintenant » — la pureté du parseur est préservée jusqu'au dernier moment :
rendreEvenements(trierParDebut(parserEvenements(res.flux, new Date())));
Tout handler async de clic doit attraper — sinon la promesse rejette en
silence et le bouton paraît mort (voir Pièges) :
creer.addEventListener("click", async () => {
try { await creerSurSiteMairie(evenement); }
catch (erreur) { rendreEchecCreation(erreur); } // console.error + message NOMMANT l'erreur
});
Closure sur l'événement de la carte : ni dataset, ni re-lookup par uid.
Formater une date flottante SANS timeZone, un instant AVEC — les deux
formatteurs coexistent délibérément dans presentation.js :
const FORMAT_JOUR_PARIS = new Intl.DateTimeFormat("fr-FR", { timeZone: "Europe/Paris", dateStyle: "full" });
// Sans timeZone, DÉLIBÉRÉMENT : une journée entière est une date *flottante*
// (RFC 5545). ical.js la matérialise à minuit local ; la relire en local
// restitue le bon jour partout, alors que la convertir vers Paris la ferait
// glisser au jour précédent depuis Tokyo.
const FORMAT_JOUR_FLOTTANT = new Intl.DateTimeFormat("fr-FR", { dateStyle: "full" });
Reculer d'un jour un DTEND de journée entière, en composantes locales :
// DTEND est EXCLUSIF (RFC 5545) : un événement du 18 porte DTEND=19.
// Jamais « -24h » : la date est flottante, un changement d'heure ferait basculer le jour.
const veilleDe = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1);
Tester ce que le fuseau épinglé cache — en sous-processus. TZ=Europe/Paris
est justement le seul fuseau où une journée entière mal formatée reste invisible.
Intl fige le fuseau à l'import : seul un autre processus peut le changer.
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);
});
});
// Bun.spawn(["bun", "-e", …], { env: { ...process.env, TZ: fuseau } })
Le fixture traverse la frontière de processus en code source (une constante
unique interpolée : JOURNEE_ENTIERE_DEBUT.join(", ")) → les deux tests ne
peuvent plus diverger par construction. C'est ce qui a corrigé la cause du
faux vert, pas son symptôme.
Assertions tolérantes sur les sorties Intl (toContain / not.toMatch,
jamais l'égalité stricte) : la ponctuation d'ICU varie entre versions de Bun. On
teste l'intention (bon jour, heure présente/absente, bon fuseau), pas le typographe.
Rendu DOM sûr : textContent exclusivement (jamais innerHTML — les titres
viennent d'une source externe), replaceChildren() pour rester rejouable au
Réessayer, <li class="liste-vide"> dans le <ul> (un état vide est un
contenu, pas une erreur — et un <ul> ne peut contenir que des <li>).
Pièges à éviter
-
🔴 Date flottante vs instant absolu : la famille de bugs de ce ticket. Une
DTSTART;VALUE=DATEn'a pas de fuseau. Cette confusion a produit trois défauts distincts, chacun invisible sousTZ=Europe/Paris:formaterDateforçaitEurope/Parissur les journées entières → la date glissait d'un jour depuis Tokyo (trou du plan, pas de l'exécution) ;- un fixture de test déclarait un instant absolu (
"...T00:00+02:00") pour décrire une date flottante → faux vert ; - (préexistant, hors T1)
evenements.test.ts:141mêle unAUJOURDHUIabsolu à unDTSTARTflottant → rouge sous Auckland.
Règle : un fixture de date flottante se construit en composantes locales (
new Date(2026, 6, 10)), jamais en instant absolu — ce dernier n'est juste que sur une machine à +02:00 et ment partout ailleurs. Côté consommateur : testerjourneeEntiered'abord, ne lire que les composantes locales. -
🔴 Un handler
asyncnon attrapé = un bouton silencieusement mort.() => creerSurSiteMairie(evenement): la promesse n'était ni attendue ni attrapée → toute défaillance devenait un rejet silencieux. Aucun onglet, aucun message, rien à diagnostiquer. Le plan avait différé l'UI d'erreur (« non demandé en T1 ») : le terrain a démontré que ce report rendait l'anomalie invisible ET indiagnosticable, et a coûté deux passes de test. Afficher le détail technique est volontaire : c'est ce qui a fait passer la panne d'« indiagnosticable » à « nommée en 3 secondes ». Leçon générale : la remontée d'erreur d'un geste utilisateur n'est pas du confort différable — c'est l'instrument qui rend le reste diagnosticable. -
🔴 Ajouter une clé au
manifest.jsonimpose Décharger + Charger. Firefox relit les scripts de la page à chaque ouverture, mais le manifest seulement à l'installation. On se retrouve avec le code le plus récent et les permissions figées de l'ancien. Symptôme :browser.<api> is undefinedalors que le manifest déclare la permission. F5 et « Recharger » ne réparent rien. Ce piège a coûté deux passes ; T2 (écriture CalDAV) y retombera en ajoutant sa permission. Documenté dansREADME.md§ Installation (dev). -
bun testnu forceTZ=UTCquandTZest absente (le runner, pasbun -equi lit le fuseau système). Sans le pinTZ=Europe/Parisdepackage.json, les tests ne tournent pas dans le fuseau des bénévoles. Le pin est PORTEUR — ne pas le retirer. Corollaire : un test qui ne passe que grâce au pin cache un bug de fuseau (cf. le sous-processus ci-dessus). -
Un fuseau épinglé masque autant qu'il stabilise.
Europe/Parisest précisément le fuseau où les bugs de date flottante sont invisibles. Épingler pour le déterminisme et tester ailleurs pour la vérité. -
DTENDexclusif : sans le recul d'un jour, on annonce une date FAUSSE (pas juste laide). ⚠️ La tâche sœur doit l'appliquer avant de remplir#input_6_32, sinon elle soumettra une date fausse à la mairie. Autre cas :DTENDabsent → le parseur posefin === debut(pas de plage à afficher : « de 10:30 à 10:30 » serait absurde). -
Ne pas coder «
debut→fin» naïvement : c'était faux 4 fois sur 6 (DTEND absent, franchit minuit, journée entière, festival). Mesurer les formes réelles sur le vrai parseur avant de coder. -
La liste est incomplète par construction : les récurrents au DTSTART maître passé (atelier hebdo, permanence) n'y figurent pas — trou hérité, comblé par [T3]. Aucun avertissement UI : un avertissement permanent que personne ne peut lever devient du bruit.
-
Le bouton ouvre un formulaire VIDE tant que la tâche sœur n'est pas faite. État intermédiaire honnête et démontrable (
about:debugging→ Storage →evenement-en-attente), pas un bug. Ne pas « finir le travail » en douce : cela casserait un découpage déjà tranché.
Tags
tags: [firefox-extension, manifest-v3, webextension, vanilla-js, storage-local, content-script, seam, icalendar, rfc5545, floating-date, dtend-exclusive, timezone, intl, date-formatting, pure-function, dependency-injection, bun-test, cross-timezone-testing, test-fixtures, false-green, async-error-handling, silent-failure, dom-security, textcontent, serialization, contract-design, yagni, low-tech]