208 lines
12 KiB
Markdown
208 lines
12 KiB
Markdown
# 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 :
|
|
1. **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**.
|
|
2. **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 contrat `Event` fraîchement figé sur un
|
|
détail du moteur = dette silencieuse. Zéro besoin actuel — `background.js`
|
|
garde son unique rôle (ouvrir l'onglet) et **n'a pas été modifié**.
|
|
- **Background → `storage.session` (C)** : le storage JSON-ise → `debut`/`fin`
|
|
deviennent 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 un `uid` (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 (voir `DECISIONS.md`).
|
|
- **`tabs.sendMessage` au 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
|
|
dans `liste.js` : ce dernier touche `document` dè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**, `lieu` stocké **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.
|
|
- **`await` le dépôt AVANT `tabs.create`** : sinon course — c'est précisément ce
|
|
que la couture achète (« la donnée attend le content script »).
|
|
- **Permission `storage` seule** ; `"tabs"` correctement écartée (`tabs.create`
|
|
n'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 :
|
|
```js
|
|
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 :
|
|
```js
|
|
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) :
|
|
```js
|
|
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` :
|
|
```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** :
|
|
```js
|
|
// 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.
|
|
```ts
|
|
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=DATE` n'a **pas** de fuseau. Cette confusion a produit **trois**
|
|
défauts distincts, chacun invisible sous `TZ=Europe/Paris` :
|
|
1. `formaterDate` forçait `Europe/Paris` sur les journées entières → la date
|
|
**glissait d'un jour** depuis Tokyo (trou du **plan**, pas de l'exécution) ;
|
|
2. un fixture de test déclarait un instant absolu (`"...T00:00+02:00"`) pour
|
|
décrire une date flottante → **faux vert** ;
|
|
3. *(préexistant, hors T1)* `evenements.test.ts:141` mêle un `AUJOURDHUI`
|
|
absolu à un `DTSTART` flottant → 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 :
|
|
tester `journeeEntiere` **d'abord**, ne lire que les composantes locales.
|
|
|
|
- **🔴 Un handler `async` non 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.json` impose 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 undefined`
|
|
alors 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é dans `README.md` § Installation (dev).
|
|
|
|
- **`bun test` nu force `TZ=UTC`** quand `TZ` est absente (le runner, pas `bun -e`
|
|
qui lit le fuseau système). Sans le pin `TZ=Europe/Paris` de `package.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/Paris` est
|
|
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é.
|
|
|
|
- **`DTEND` exclusif** : 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 : **`DTEND` absent → le parseur pose `fin === 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]
|