From ee79ca07572198720e13783278bc105f2b7f9cdb Mon Sep 17 00:00:00 2001 From: Pierre Martin Date: Fri, 17 Jul 2026 17:36:56 +0200 Subject: [PATCH] docs: capitalize the mairie form prefill and config solution (T1) Co-Authored-By: Claude Opus 4.8 --- ...-pour-le-formulaire-mairi-oebs-solution.md | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 docs/solutions/2026-07-17-formater-les-donnees-pour-le-formulaire-mairi-oebs-solution.md diff --git a/docs/solutions/2026-07-17-formater-les-donnees-pour-le-formulaire-mairi-oebs-solution.md b/docs/solutions/2026-07-17-formater-les-donnees-pour-le-formulaire-mairi-oebs-solution.md new file mode 100644 index 0000000..67a23a4 --- /dev/null +++ b/docs/solutions/2026-07-17-formater-les-donnees-pour-le-formulaire-mairi-oebs-solution.md @@ -0,0 +1,161 @@ +# Solution : [T1] Content script : pré-remplir le formulaire mairie (+ config) + +## Problème résolu + +Au clic sur « Créer sur le site mairie », l'extension déposait l'événement dans +`storage.local` puis ouvrait un **formulaire vide** : le bénévole devait recopier +à la main titre, dates, heures, adresse, organisateur, email — exactement le +geste que l'extension existe pour supprimer. La boucle n'était pas fermée. + +Objectif tenu : le formulaire s'ouvre **déjà rempli** de ce que la machine sait, +**laisse vide** ce qu'elle ignore (jamais de valeur inventée), et **dit** dans un +bandeau ce qui reste à compléter. Le bénévole relit, complète (image, thème, +enrichissement) et envoie lui-même — **pas d'auto-submit**. + +Ce ticket a aussi **introduit la config** de l'extension. + +## Approche choisie + +**Deux content scripts classiques partageant un scope + namespace `globalThis`** +(approche 3 du brainstorm), **+ config à défauts committés et surcharge locale +optionnelle** (option (a)). + +Le vrai risque du ticket n'était pas « poser des valeurs dans un DOM » : c'était +le **calcul des dates** (DTEND exclusif, dates flottantes de journée entière, +franchissement de minuit) — invisible sous `TZ=Europe/Paris`, la famille de bugs +qui avait déjà coûté deux passes au ticket précédent. Il fallait donc que ce +calcul reste **sous `bun test`**. + +Trois alternatives écartées, et pourquoi : +- **Content script monolithique** : logique de dates intestable (rien à importer + en `bun test`) → on coderait à l'aveugle les deux bugs les plus chers. +- **Module ESM en import dynamique** (`import(runtime.getURL(...))` + + `web_accessible_resources`) : s'appuie sur un **bug ouvert MV3 de Firefox** + ([bugzilla 1803950](https://bugzilla.mozilla.org/show_bug.cgi?id=1803950)) — + parier le risque sur le point le plus fragile de la plateforme. +- **Formater en amont, content script bête** : réviserait une **couture figée + trois jours plus tôt** et coupleraient la page liste au formulaire mairie. + +L'approche retenue garde le calcul testable **sans** parier sur un bug plateforme +**ni** rouvrir une couture fraîche. + +## Décisions clés + +- **`config.js` sans `import` ni `export`, communique par `globalThis`.** Un + fichier sans `import`/`export` est valide **à la fois** comme script classique + (content script, background) **et** comme module ESM (`import "./config.js"` + depuis `presentation.js` et `bun test`). C'est ce qui permet d'absorber + `LIEU_PAR_DEFAUT` (fin d'une duplication) sans build. ⚠️ Le jour où quelqu'un + ajoute un `export`, tous les content scripts cassent → acté dans `DECISIONS.md`, + avec un commentaire en tête de fichier. + +- **Le background lit `config.local.json`, pas le content script.** Un content + script ne peut `fetch(runtime.getURL(...))` que si la ressource est en + `web_accessible_resources` — ce qui **exposerait l'email à la page de la + mairie**. Le background lit sans WAR et transmet via `storage.local`. + +- **Config optionnelle, dégradation propre.** Fichier absent (clone frais), HTTP + non-ok ou JSON invalide → `{}` → organisateur/email vides **signalés dans le + bandeau**, tout le reste rempli. Un `config.local.json` **requis** (énoncé + littéral) aurait cassé tout clone frais : rejeté. Le fichier n'étant pas + garanti présent, il **ne peut pas** être listé dans le manifest (Firefox refuse + d'installer si un fichier `js` manque) : `fetch` + repli est la seule voie. + +- **`CLE_EVENEMENT_EN_ATTENTE` dupliquée + test de dérive.** La constante vit dans + `transfert.js` (ESM), inatteignable depuis un content script classique. + Dupliquée volontairement, avec un test qui garantit l'égalité avec l'originale + (plutôt que rouvrir la couture pour convertir `transfert.js`). + +- **Honnêteté machine/humain.** Jamais de « 00:00 » sur une journée entière + (heures laissées vides), jamais de `lat|lng` inventé sur un lieu hors café, + `input_6_35` (accessibilité) laissé intact, honeypots intacts. Tout manque + remonte dans `aCompleter` (bandeau). + +- **`config.local.json.example` dans `extension/`, pas à la racine.** Le + background résout `runtime.getURL("config.local.json")` **dans `extension/`** : + l'exemple doit être à côté de sa cible de copie. + +## Patterns à réutiliser + +- **Fichier « bilingue » ESM/classique** : ne poser `import`/`export` nulle part, + publier l'API sur `globalThis.MonNamespace`. Chargeable partout dans une + extension MV3 sans build. Le prix : une convention à documenter pour qu'un futur + agent ne la « corrige » pas en `export`. + +- **Séparer calcul pur (testé) / effet de bord (fin, non testé).** + `calculerValeurs(payload, config)` est pur et renvoie un objet **plat, prêt à + poser** (`dateDebut: "18/07/2026"`, `heureDebut: ["19","30"] | null`, …) ; + `formulaire-mairie.js` ne fait que lire le storage et écrire le DOM. Le bug des + heures (voir Pièges) a **validé** ce découpage : confiné au poseur, diagnostiqué + en une pile d'appels, corrigé en quelques lignes, cœur testé jamais touché. + +- **Merge tolérant pour config éditée à la main** : `fusionner(defauts, locale)` + traite `null` / non-objet / tableau comme « pas de surcharge » et ne mute pas + les défauts. Une config optionnelle ne doit **jamais** lever. + +- **Dates de calendrier sans fuseau.** Journée entière = date flottante + (RFC 5545) → lire **uniquement** les composantes locales + (`getFullYear/getMonth/getDate`), jamais l'heure, jamais un fuseau. DTEND est + **exclusif** → reculer la date de fin d'un jour (en composantes locales, jamais + « −24h »). Événement horodaté → tout à `Europe/Paris` via `Intl.DateTimeFormat`, + heures zéro-paddées. **Épingler `TZ=Europe/Paris` dans les tests ET rejouer sous + plusieurs fuseaux** (le bug de fuseau est invisible sous le fuseau de dev). + +- **Poser une valeur dans un champ piloté par un framework** : après `.value`, + dispatcher `new Event("input", {bubbles:true})` **puis** + `new Event("change", {bubbles:true})` (Gravity Forms écoute `change`). Le + dispatch est gratuit — ne pas supposer que `.value` suffit. + +- **Isoler chaque effet de bord dans son propre `try/catch`.** Un `try` global + autour du remplissage laissait la première exception emporter **tous** les + champs suivants. Un `try` **par champ** → le champ fautif part dans `manquants`, + les autres sont posés. Ferme la classe entière de panne, pas le cas vu. + +- **Rendre tout geste automatique visible.** Un bandeau injecté dit ce qui a été + fait et ce qui reste ; storage vide (visite directe) → **rien**. Un + préremplissage silencieusement partiel soumis à une mairie qui modère est le + pire échec possible. + +## Pièges à éviter + +- 🔴 **Ne jamais déduire le type d'un champ de son intitulé — le HTML est la seule + source de vérité.** L'énoncé du ticket affirmait « Heures = 2 **selects** + input_6_38_1/_2 ». Le HTML réel dit ``. `poserSelect` + faisait `[...champ.options]` sur un `` → `champ.options` undefined → + `TypeError`, qui (avant l'isolation par champ) vidait aussi Tarifs, adresse et + coordonnées. Corrigé : heures via `poserTexte` (le zéro-padding `"09"`/`"00"` + reste un *valid floating-point number* au sens HTML). `RECHERCHE.md` §2 a été + corrigée à la racine (colonne « balise réelle » + avertissement) pour qu'un + futur agent ne repaie pas ce bug. C'est la cause racine du seul bug du ticket, + et il venait d'un **doc/énoncé faux cru sur parole**. + +- **Toucher au `manifest.json` impose Décharger + Charger**, pas « Recharger » ni + F5. Piège déjà payé plusieurs fois. Fermer aussi les anciens onglets de la liste + (page orpheline = clics inertes). + +- **DTEND exclusif** : sans recul d'un jour, la date de fin d'un événement + multi-jours est **fausse** — et soumise à une mairie qui modère (personne ne + relit une date qui *a l'air* juste). Le bug le plus cher du ticket. + +- **Course background ↔ content script** (P3 ouvert, non corrigé) : + `chargerConfigLocale()` est async ; au tout premier chargement post-install, si + le formulaire s'ouvrait avant la fin du `fetch`, organisateur/email seraient + vides à tort. Improbable (la valeur persiste ensuite), noté pour mémoire. + +- **`chargerConfigLocale()` s'exécute à chaque réveil de l'event page** et écrit + `{}` si le fichier est absent. Toute future page d'options qui écrirait dans + `storage.local` verrait ses valeurs **écrasées par `{}`** au prochain réveil. + Trancher « qui fait autorité, fichier ou UI » fait partie du futur ticket — ce + n'est pas « juste ajouter un écran ». + +- **Le poseur DOM n'est pas testé unitairement** (aucun harnais DOM dans le repo ; + happy-dom n'émule pas le JS de Gravity Forms / Leaflet). C'est le **test humain** + qui tranche le rendu réel (datepicker, marqueur carte, bandeau non écrasé par le + CSS de la mairie). Le bug des heures l'a prouvé : seul le DOM réel tranche. + +- **Config `cafe` partielle interdite** : `fusionner` est shallow → une locale + avec un `cafe` incomplet produirait `"undefined|adresse"`. Le README précise que + seuls `organisateur` et `email` sont surchargeables. + +## Tags +tags: [firefox-extension, manifest-v3, content-script, globalthis, esm, gravity-forms, dates, timezone, rfc5545, config, bun-test, tdd, vanilla-js]