Files
echo-du-huit/docs/solutions/2026-07-17-formater-les-donnees-pour-le-formulaire-mairi-oebs-solution.md

162 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<input type="number">`. `poserSelect`
faisait `[...champ.options]` sur un `<input>` → `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]