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

9.2 KiB
Raw Blame History

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) — 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]