mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-05-18 08:18:15 +02:00
capitalisation: intégration ~60 entrées RL799_V2 (triage 2026-05-02)
Triage du 95_a_capitaliser.md (~75 propositions) : - 60 entrées intégrées dans knowledge/ (backend, frontend, workflow) - 4 nouveaux fichiers : backend/patterns/tests.md, backend/risques/tests.md, frontend/patterns/general.md, workflow/patterns/general.md - 6 doublons rejetés - Mise à jour des READMEs index pour refléter les nouvelles entrées - 95_a_capitaliser.md restauré à sa structure initiale - 40_decisions_et_archi.md : décision mono-tenant déployable vs SaaS multi-tenant - 90_debug_et_postmortem.md : sub-agents Write indisponible, effet iceberg CI, prisma migrate diffs cosmétiques Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -319,3 +319,474 @@ Poser `role="menu"` / `role="menuitem"` implique obligatoirement :
|
||||
- Contexte technique : PWA / install prompt — RL799_V2 18-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-cache-pwa-soft-delete-fuite"></a>
|
||||
## Cache offline PWA + soft-delete — invalidation diff-based scopée
|
||||
|
||||
### Risques
|
||||
|
||||
- Une PWA qui cache des blobs en IndexedDB pour offline reading **ne reçoit aucun signal automatique** quand un document est soft-deleted côté serveur
|
||||
- Le contenu reste lisible hors-ligne indéfiniment. Pour des données réglementaires ou sensibles, c'est un gap de sécurité non négligeable
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Document soft-deleted en base, encore consultable offline par les utilisateurs qui l'ont mis en cache
|
||||
- Aucun mécanisme automatique de purge
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
**Mitigation V1 (best-effort, diff-based)** au prochain `loadEntries` online :
|
||||
|
||||
1. Lire la liste serveur courante (filtrée `deletedAt IS NULL`)
|
||||
2. Lire les IDs cachés localement **scopés au même périmètre** (type+grade) — sinon on supprime à tort un doc d'un autre onglet
|
||||
3. Diff : `cached - server = soft-deleted` → `removeCachedDocument(id)` pour chaque
|
||||
|
||||
```typescript
|
||||
const bustCachedIfMissing = async (candidateIds: string[], serverIds: Set<string>) => {
|
||||
for (const id of candidateIds) {
|
||||
if (!serverIds.has(id)) await removeCachedDocument(id);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Mitigation V2 (push server-initiated)** : Service Worker abonné à un canal (postMessage / WebSocket / SSE), serveur publie `{ type: 'document-soft-deleted', id }` sur soft-delete, SW intercepte et fait `caches.delete()` immédiat. Coût : infra push + gestion connectivité partielle. À garder en backlog si le risque devient critique (audit GDPR).
|
||||
|
||||
**Tests recommandés** :
|
||||
- doc caché + recharge avec serveur qui omet ce doc → assert `removeCachedDocument` appelé
|
||||
- doc caché + serveur qui retourne le doc → assert pas d'effet (non-régression)
|
||||
- doc caché pour scope `rituels` + recharge sur scope `mementos` qui omet ce doc → assert pas d'effet (scope isolation)
|
||||
|
||||
- Contexte technique : PWA / IndexedDB — RL799_V2 20-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-duplication-focus-parent-modal"></a>
|
||||
## Duplication parent ↔ modal de la capture de focus
|
||||
|
||||
### Risques
|
||||
|
||||
- Quand un composant modal implémente correctement le pattern a11y `previousActiveElement` (capture à `onMounted`, restitution à `close`/`submit`), le composant parent **ne doit PAS** stocker un `lastTrigger` en parallèle
|
||||
- Code mort avec commentaire trompeur : un lecteur qui cherche à comprendre le flux focus va s'y perdre
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Le parent a une ref `lastFabTrigger` / `lastTrigger` écrite au clic du trigger
|
||||
- Elle n'est **jamais lue** — le commentaire prétend "pour restitution du focus par la modal", mais la modal a son propre mécanisme
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- **Une seule responsabilité** pour la restitution du focus : la modal
|
||||
- Le parent se contente d'ouvrir la modal (`uploadModalOpen.value = true`)
|
||||
- Le parent ne capture le focus QUE si la modal ne le fait pas (cas d'un overlay maison sans pattern `previousActiveElement`)
|
||||
- **Repérage en code review** : `grep -n "lastTrigger\|previousActive" components/ pages/` → s'il y a des occurrences dans BOTH un parent et une modal du même flux, c'est le signal
|
||||
|
||||
- Contexte technique : Vue 3 / a11y modales — RL799_V2 20-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-touch-action-none-card-mobile"></a>
|
||||
## `touch-action: none` sur card mobile bloque scroll vertical
|
||||
|
||||
### Risques
|
||||
|
||||
- Une card mobile gérant un swipe horizontal avec `touch-action: none` capture **tout** le toucher, laissant le JS gérer le scroll vertical
|
||||
- Le JS détecte scroll vs swipe via un seuil mais doit **libérer** l'événement après l'avoir analysé → un délai imperceptible s'installe, le scroll vertical devient saccadé et souvent ignoré
|
||||
- L'utilisateur trouve la liste "inscrollable" quand son pouce touche directement les cards
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Poser le pouce sur une card puis scroller ne marche qu'une fois sur 20
|
||||
- Scroll OK dans les marges vides à côté
|
||||
- Aucune erreur console
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```css
|
||||
/* AVANT — bug : scroll vertical capturé */
|
||||
.member-card {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* APRÈS — scroll natif OK, swipe horizontal toujours fonctionnel */
|
||||
.member-card {
|
||||
touch-action: pan-y;
|
||||
}
|
||||
```
|
||||
|
||||
Combiné avec un handler JS qui `preventDefault` uniquement sur mouvement horizontal significatif (> 10 px) :
|
||||
|
||||
```typescript
|
||||
if (Math.abs(deltaX) > 10 && Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
```
|
||||
|
||||
**Règle** :
|
||||
- Card qui gère swipe horizontal ET scroll vertical : `touch-action: pan-y` (le navigateur gère nativement le scroll vertical, seul l'axe horizontal est laissé au JS)
|
||||
- Card qui ne gère QUE du pan/zoom custom (rare) : `touch-action: none` peut se justifier
|
||||
- Tous les autres cas : laisser la valeur par défaut (`auto`)
|
||||
|
||||
**Repérage en code review** : `grep -rn "touch-action: none" components/` → chaque occurrence est suspecte.
|
||||
|
||||
- Contexte technique : CSS / mobile — RL799_V2 21-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-button-wrapper-card-color-inherit"></a>
|
||||
## `<button>` wrapper card — toujours `color: inherit` (reset user-agent)
|
||||
|
||||
### Risques
|
||||
|
||||
- Quand on utilise un `<button>` comme wrapper cliquable d'une card (pattern idiomatique pour l'a11y clavier), il hérite du `color` user-agent par défaut
|
||||
- Sur certains setups (Safari/dark mode notamment), ce `color` peut être bleu — le texte enfant hérite et contamine titres et paragraphes qui devraient prendre la couleur du thème
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Titre de card en bleu sur fond sombre alors que le thème prévoit de l'or
|
||||
- Bug non visible en dev light mode sur Chrome — apparaît uniquement sur certains setups → difficile à reproduire
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```css
|
||||
.my-card-button {
|
||||
/* reset user-agent */
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
/* … */
|
||||
}
|
||||
```
|
||||
|
||||
**Règle** : tout `<button>` qui wrappe du contenu stylé par le thème (card, liste, ligne de tableau) doit reset `color`, `font-family`, `font-size`, `border`, `background`. C'est un bootstrap user-agent minimal à prévoir dans tout design system.
|
||||
|
||||
**Alternative** : `<div role="button" tabindex="0">` avec `@keydown.enter/space`. Plus verbeux, mais évite les resets. Pattern valide si l'équipe est à l'aise avec les implications a11y.
|
||||
|
||||
- Contexte technique : CSS / a11y — RL799_V2 21-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-erreur-silencieuse-4-etats"></a>
|
||||
## Erreur silencieuse = blanc indistinguable de "aucune donnée" — 4 états distincts (loading / empty / error / forbidden)
|
||||
|
||||
### Risques
|
||||
|
||||
- Un composant qui affiche le résultat d'un fetch sans distinguer ses 4 états produit du **blanc** dans 3 cas sur 4 — l'utilisateur ne peut pas savoir si la donnée est en chargement, légitimement vide, en erreur réseau, ou refusée par RBAC
|
||||
- Sur les flows critiques (rituel, opérations sensibles), un blanc silencieux est inacceptable : l'utilisateur prend des décisions sur la base de l'affichage
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Plusieurs cards d'une vue qui auto-fetchent et tombent toutes en `[]` côté front quand l'API renvoie 403 — affichage indistinguable de "vide légitime"
|
||||
- Toast générique "Une erreur est survenue" sans corrélation avec un retry actionnable
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Tout composant qui fetch une ressource doit avoir **4 états distincts** dans son rendu :
|
||||
|
||||
1. **Loading** : skeleton, spinner, ou texte explicite (« Chargement… »)
|
||||
2. **Empty (donnée légitimement vide)** : message explicite *« Aucune donnée enregistrée pour … »*
|
||||
3. **Error (réseau / serveur)** : message + bouton retry. Ne jamais se contenter d'un blanc
|
||||
4. **Forbidden (403)** : message explicite *« Vous n'avez pas accès à cette donnée »* + suggestion d'action (recharger / contacter admin)
|
||||
|
||||
Le frontend doit savoir **distinguer 403** des autres erreurs au niveau de son service HTTP, et propager l'info au composant. Ne pas traiter `!response.ok` en bloc avec un message générique.
|
||||
|
||||
```typescript
|
||||
export const getXxx = async (id: string) => {
|
||||
const response = await apiFetch(`/api/xxx/${id}`);
|
||||
if (response.status === 403) throw new ForbiddenError(...);
|
||||
if (response.status === 404) throw new NotFoundError(...);
|
||||
if (!response.ok) throw new Error(await parseError(response));
|
||||
return (await response.json()).data;
|
||||
};
|
||||
```
|
||||
|
||||
Le composant catche les types d'erreur et choisit le rendu approprié.
|
||||
|
||||
- Contexte technique : Vue 3 / fetch — RL799_V2 27-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-service-worker-non-secure-context"></a>
|
||||
## Service Worker invisible en accès non-secure (HTTP via IP réseau)
|
||||
|
||||
### Risques
|
||||
|
||||
- Les Service Workers exigent un [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) — HTTPS strict, OU URL `http://localhost:*` ou `http://127.0.0.1:*`
|
||||
- Un accès via IP réseau en HTTP (Tailscale `100.x.x.x`, LAN `192.168.x.x`) est non-secure → `navigator.serviceWorker` est `undefined` → la PWA fonctionne en mode "navigateur classique" mais sans cache offline, sans push, sans badge, sans installation
|
||||
- Les tests E2E en Tailscale loupent silencieusement les régressions SW
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `navigator.serviceWorker.register('/sw.js')` lève `Cannot read properties of undefined (reading 'register')`
|
||||
- DevTools > Application > Service Workers ne montre rien
|
||||
- Tests "ça marche en LAN" qui ne reflètent pas la prod HTTPS
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```javascript
|
||||
// Détection
|
||||
console.log(window.isSecureContext, location.protocol, location.hostname);
|
||||
// true "https:" "..." → OK
|
||||
// true "http:" "localhost" → OK
|
||||
// false "http:" "192.168.1.42" → SW désactivé
|
||||
```
|
||||
|
||||
Stratégies par contexte :
|
||||
|
||||
1. **Test local** : utiliser `http://localhost:<port>` strict (jamais l'IP, même en LAN)
|
||||
2. **Test réseau / mobile** : reverse proxy HTTPS (Caddy/Traefik avec Let's Encrypt, ou `tailscale cert` pour le magicDNS Tailscale)
|
||||
3. **Préview Vite** : `vite preview --https` avec un certificat auto-signé (acceptable en dev test)
|
||||
|
||||
**Préventif** :
|
||||
- documenter dans le README projet que le SW exige HTTPS/localhost
|
||||
- en CI E2E, toujours utiliser `localhost` (le webServer Playwright tourne sur `localhost` par défaut)
|
||||
- ne PAS supposer qu'un test "ça marche en LAN" reflète la prod HTTPS
|
||||
|
||||
- Contexte technique : PWA / Service Worker — RL799_V2 28-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-vite-pwa-bascule-strategies-runtime-caching"></a>
|
||||
## Vite-plugin-pwa : bascule `generateSW` → `injectManifest` rend `runtimeCaching` inerte
|
||||
|
||||
### Risques
|
||||
|
||||
- En mode `injectManifest`, Vite PWA n'injecte PAS de runtime workbox — il bundle le `src/sw.ts` fourni tel quel
|
||||
- Toute la config `workbox.*` du `vite.config.ts` (sauf `globPatterns`/`globIgnores` déplacés sous `injectManifest.*`) est ignorée silencieusement, sans warning
|
||||
- Régression directe : leak de cookies API en cache, contenu sensible en cache, 404 transformés en `index.html`
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Après bascule, les routes runtime configurées dans `workbox.runtimeCaching` n'ont plus aucun effet
|
||||
- Le build passe, aucune erreur visible, mais en DevTools > Application > Service Worker on ne voit pas les routes attendues
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Détection : examiner `dist/sw.js` après build — chercher des strings clés (`/api/`, `uploads`, `manifest.webmanifest`, `addEventListener`). Si elles sont absentes du SW custom, c'est qu'on a perdu la protection.
|
||||
|
||||
**Mitigation** : RÉIMPLÉMENTER À LA MAIN dans `src/sw.ts` toutes les routes runtime, denylist, et cleanup :
|
||||
|
||||
```typescript
|
||||
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
|
||||
import { registerRoute, NavigationRoute } from 'workbox-routing';
|
||||
import { NetworkOnly, NetworkFirst } from 'workbox-strategies';
|
||||
import { ExpirationPlugin } from 'workbox-expiration';
|
||||
|
||||
cleanupOutdatedCaches();
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
registerRoute(({ url }) => url.pathname.startsWith('/api/'), new NetworkOnly());
|
||||
registerRoute(({ url }) => url.pathname.startsWith('/uploads/'), new NetworkOnly());
|
||||
registerRoute(
|
||||
({ url }) => url.pathname === '/manifest.webmanifest',
|
||||
new NetworkFirst({
|
||||
cacheName: 'manifest-cache',
|
||||
plugins: [new ExpirationPlugin({ maxAgeSeconds: 300, maxEntries: 1 })],
|
||||
}),
|
||||
);
|
||||
|
||||
registerRoute(
|
||||
new NavigationRoute(async () => { /* fallback handler */ }, {
|
||||
denylist: [/^\/api\//, /^\/uploads\//, /^\/manifest\.webmanifest$/],
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
**Vérifications obligatoires post-bascule** (DevTools sur build/preview) :
|
||||
|
||||
1. Application > Service Worker : `sw.js` activé
|
||||
2. Network : `POST /api/auth/login` → pas de "(from ServiceWorker)", pas de cache
|
||||
3. Network : GET `/uploads/foo.pdf` → réseau direct
|
||||
4. Network mode Offline : navigation `/page` → fallback `index.html` ; `/api/foo` → erreur réseau (PAS index.html)
|
||||
5. Déploiement v2 : ancien cache purgé après activation
|
||||
|
||||
`setCatchHandler` ne suffit PAS à remplacer `navigateFallback` — il ne se déclenche que si une route enregistrée throw. Pour le navigation fallback, utiliser `NavigationRoute` explicitement.
|
||||
|
||||
- Contexte technique : Vite / vite-plugin-pwa / Workbox — RL799_V2 28-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-ts-strict-uint8array-buffersource"></a>
|
||||
## TS strict — `Uint8Array<ArrayBufferLike>` non assignable à `BufferSource`
|
||||
|
||||
### Risques
|
||||
|
||||
- TS 5.7+ avec lib DOM récente paramètre `Uint8Array` par défaut sur `ArrayBufferLike` (qui inclut `SharedArrayBuffer`)
|
||||
- Beaucoup d'APIs DOM (Push API, WebCrypto certaines surfaces) attendent un `BufferSource` strict avec `buffer: ArrayBuffer` — d'où une erreur TS au build
|
||||
|
||||
### Symptômes
|
||||
|
||||
```
|
||||
Type 'Uint8Array<ArrayBufferLike>' is not assignable to type 'BufferSource'.
|
||||
Types of property 'buffer' are incompatible.
|
||||
Type 'ArrayBufferLike' is not assignable to type 'ArrayBuffer'.
|
||||
Type 'SharedArrayBuffer' is missing the following properties from type 'ArrayBuffer'…
|
||||
```
|
||||
|
||||
L'erreur est au build, pas au runtime (le code marche en JS).
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Créer explicitement un `ArrayBuffer` strict, puis remplir via une vue `Uint8Array` :
|
||||
|
||||
```typescript
|
||||
// ❌ Ne compile pas en TS strict
|
||||
const urlBase64ToUint8Array = (s: string): Uint8Array => {
|
||||
const raw = atob(s.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
const out = new Uint8Array(raw.length);
|
||||
for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
|
||||
return out;
|
||||
};
|
||||
|
||||
// ✅ Compile et fonctionne identiquement
|
||||
const urlBase64ToArrayBuffer = (s: string): ArrayBuffer => {
|
||||
const raw = atob(s.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
const buf = new ArrayBuffer(raw.length);
|
||||
const view = new Uint8Array(buf);
|
||||
for (let i = 0; i < raw.length; i++) view[i] = raw.charCodeAt(i);
|
||||
return buf;
|
||||
};
|
||||
```
|
||||
|
||||
**Alternative déconseillée** : `as ArrayBuffer` cast — masque le problème, peut rater une vraie incompatibilité si la lib DOM évolue.
|
||||
|
||||
- Contexte technique : TypeScript 5.x / lib DOM — RL799_V2 28-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-safe-areas-ios-viewport-fit-cover"></a>
|
||||
## Safe-areas iOS — `viewport-fit=cover` indispensable
|
||||
|
||||
### Risques
|
||||
|
||||
- Sur iPhone Pro/Max (notch + home indicator + Dynamic Island), `padding-top: env(safe-area-inset-top)` ou `padding-bottom: env(safe-area-inset-bottom)` retourne **0** sans `<meta viewport content="… viewport-fit=cover">`
|
||||
- Le développeur conclut à tort que le pattern ne fonctionne pas, alors qu'il manque juste l'opt-in
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Header fixed rogné par le notch
|
||||
- Nav bottom rognée par le home indicator
|
||||
- `env(safe-area-inset-*)` retourne 0 sur iPhone Pro+
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
**Pattern complet (3 endroits) à appliquer ensemble** :
|
||||
|
||||
```html
|
||||
<!-- 1) index.html — opt-in safe-areas -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
```
|
||||
|
||||
```css
|
||||
/* 2) Header sticky : top + safe-area-inset-top */
|
||||
.app-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding-top: env(safe-area-inset-top);
|
||||
height: calc(var(--size-header-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
/* 3) Nav bottom : bottom + safe-area-inset-bottom + safe-area-inset-left/right */
|
||||
.app-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
height: calc(var(--size-nav-height) + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* FAB / menu flottant : bottom au-dessus de la nav + safe-area + offset */
|
||||
.fab {
|
||||
bottom: calc(var(--size-nav-height) + env(safe-area-inset-bottom) + 16px);
|
||||
/* `max()` empêche les boutons d'être collés au bord rond du device */
|
||||
right: max(16px, env(safe-area-inset-right));
|
||||
}
|
||||
```
|
||||
|
||||
**Règle de débogage** : si `env(safe-area-inset-bottom)` semble retourner 0 sur iPhone Pro+, **vérifier `<meta viewport>` AVANT de chercher ailleurs**. C'est presque toujours la cause.
|
||||
|
||||
`safe-area-inset-left` et `safe-area-inset-right` ne sont non-nuls qu'en mode paysage (notch latéral). Garder le `padding-left/right` quand même → no-op en portrait, fix en paysage.
|
||||
|
||||
- Contexte technique : CSS / iOS — RL799_V2 02-05-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-input-date-safari-ios-min-width"></a>
|
||||
## `<input type="date">` Safari iOS — `appearance: none` + `min-width: 0` + `min-height` obligatoires
|
||||
|
||||
### Risques
|
||||
|
||||
- Sur Safari iOS (et Chrome iOS car webkit sous-jacent), un `<input type="date">` ou `datetime-local` :
|
||||
- déborde de son conteneur sur la droite (largeur intrinsèque > 100 %)
|
||||
- apparaît plus mince que les autres inputs (hauteur intrinsèque différente)
|
||||
- affiche un styling natif iOS qui casse le design system
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `width: 100%` ne suffit pas — la largeur intrinsèque écrase la contrainte
|
||||
- Bug non reproductible sur Chrome desktop, visible uniquement sur iPhone réel ou Safari Responsive Design Mode
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```css
|
||||
input[type="date"],
|
||||
input[type="datetime-local"],
|
||||
input[type="time"] {
|
||||
appearance: none; /* neutralise le styling natif */
|
||||
-webkit-appearance: none; /* Safari iOS — ne PAS oublier */
|
||||
min-width: 0; /* permet à width: 100% de gagner */
|
||||
min-height: 48px; /* aligne avec les autres inputs */
|
||||
}
|
||||
```
|
||||
|
||||
**Les 4 propriétés sont nécessaires** :
|
||||
|
||||
- `appearance: none` seul : le styling natif disparaît mais la largeur intrinsèque reste → débordement
|
||||
- `min-width: 0` seul : le styling natif reste, on a juste cassé sa hauteur
|
||||
- `min-height: 48px` : nécessaire pour homogénéiser avec les inputs text classiques
|
||||
- `-webkit-appearance: none` : redondant en théorie avec `appearance: none` mais nécessaire en pratique sur certaines versions Safari iOS
|
||||
|
||||
- Contexte technique : CSS / Safari iOS — RL799_V2 01-05-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-fieldset-legend-flex-grid"></a>
|
||||
## `<fieldset>` / `<legend>` cassent un layout flex inline
|
||||
|
||||
### Risques
|
||||
|
||||
- Pour un champ "label + valeur inline" (ex : `Grade [GradeBadge]` sur la même ligne), le réflexe sémantique est `<fieldset>` + `<legend>`
|
||||
- `<legend>` a un comportement natif particulier : interrompt visuellement le `border` du fieldset, son `display` est traité spécialement, il ne se comporte pas comme un enfant flex/grid normal
|
||||
- Le legend prend toute la largeur, l'input passe en dessous, impossible de les aligner sans hacks `position: absolute`
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `<fieldset style="display: flex">` avec `<legend>` + `<input>` qui ne s'alignent pas sur la même ligne
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Pour un groupe de champs **avec layout custom** (flex/grid inline), utiliser `<div>` + `<span class="label">` + le champ :
|
||||
|
||||
```html
|
||||
<!-- ❌ Layout cassé : legend ne se comporte pas comme un enfant flex -->
|
||||
<fieldset class="field-inline">
|
||||
<legend>Grade</legend>
|
||||
<GradeBadge :grade="grade" />
|
||||
</fieldset>
|
||||
|
||||
<!-- ✅ Layout custom OK : div + span — perte sémantique mineure
|
||||
compensée par aria-labelledby si besoin -->
|
||||
<div class="field-inline">
|
||||
<span class="field-inline__label" id="grade-label">Grade</span>
|
||||
<GradeBadge :grade="grade" aria-labelledby="grade-label" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Garder `<fieldset>/<legend>` uniquement quand on accepte le rendu natif (groupe vertical avec border + legend qui chevauche le border supérieur — pattern formulaire admin classique).
|
||||
|
||||
**Trade-off à assumer** : `<fieldset>` apporte une sémantique a11y (groupe de champs liés). En remplaçant par `<div>`, on peut compenser via `role="group" aria-labelledby="..."` si le besoin d'a11y est fort. Pour un simple label+badge inline, c'est rarement nécessaire.
|
||||
|
||||
- Contexte technique : HTML / a11y / CSS — RL799_V2 02-05-2026
|
||||
|
||||
---
|
||||
|
||||
@@ -382,3 +382,176 @@ followingsError: string | null; // erreur de fetchFollowings
|
||||
|
||||
- Règle : dans un store qui gère à la fois des mutations et des listes paginées, chaque opération doit avoir sa propre clé d'erreur
|
||||
- Contexte technique : React Native / Zustand — app-alexandrie review 5.3, 28-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-emit-vue-mutation-serveur-sans-listener"></a>
|
||||
## `emit` Vue annonçant une mutation serveur sans listener parent → caches stale
|
||||
|
||||
### Risques
|
||||
|
||||
- Un composant enfant émet un événement (`emit('approved')`) après une mutation côté serveur, mais aucun parent n'écoute
|
||||
- L'enfant met bien à jour son état local, mais les caches parents qui dérivent du même statut (badges accordion, verrous cascade, prop `previousAggregate` consommée par d'autres enfants) restent stale **indéfiniment**, jusqu'au prochain changement d'écran/route
|
||||
- Bug invisible dans les tests structurels (`content.includes`) et passe inaperçu en revue parce que le code enfant est "correct"
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Tout autre affichage du même statut dans le parent ou dans des sous-composants frères reste "Publiée" alors que la planche est "Approuvée" en DB
|
||||
- L'UI ne se rafraîchit qu'au prochain reload manuel ou navigation
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```bash
|
||||
# Repérer les emits qui annoncent une mutation serveur
|
||||
grep -rn "defineEmits" apps/frontend/src/components
|
||||
|
||||
# Pour chaque emit trouvé, chercher s'il a au moins un listener parent
|
||||
grep -rn "@<eventName>=" apps/frontend/src/pages apps/frontend/src/components
|
||||
```
|
||||
|
||||
Zéro listener = bug latent (sauf si l'emit est purement informatif — analytics, debug).
|
||||
|
||||
**Règle** : tout `emit` qui annonce une **mutation persistée serveur** (création, suppression, changement de statut, validation) doit avoir au moins un listener parent qui :
|
||||
|
||||
1. Invalide les caches locaux dérivés de la même donnée (Map de statuts, computed, props transitives)
|
||||
2. Recharge la slice agrégée si le parent passe une prop construite à partir d'un autre fetch (`previousAggregate`, dashboard data)
|
||||
3. **Ne se contente PAS de l'optimistic update** de l'enfant — la source de vérité reste serveur, le cache parent doit refléter l'état serveur post-mutation
|
||||
|
||||
```vue
|
||||
<!-- ❌ Enfant émet, parent ignore. Le badge de l'accordion reste stale -->
|
||||
<PlancheTraceeCard :tenue-id="planche.tenueId" mode="previous" />
|
||||
|
||||
<!-- ✅ Listener explicite qui invalide les caches dérivés -->
|
||||
<PlancheTraceeCard
|
||||
:tenue-id="planche.tenueId"
|
||||
mode="previous"
|
||||
@approved="onPlancheApproved(planche.tenueId)"
|
||||
/>
|
||||
```
|
||||
|
||||
**Couverture** : test de mount complet via `@vue/test-utils` qui simule l'événement et vérifie que le rendu parent change. Les tests `readFileSync + content.includes('emit')` valident que le code enfant émet, pas que le parent écoute.
|
||||
|
||||
- Contexte technique : Vue 3 — RL799_V2 29-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-templates-vue-references-orphelines"></a>
|
||||
## Templates Vue — références orphelines invisibles à `tsc --noEmit`
|
||||
|
||||
### Risques
|
||||
|
||||
- Une variable supprimée du `<script setup>` mais encore référencée dans le `<template>` ne génère pas d'erreur compile avec `tsc --noEmit` seul (Volar moins strict que `tsc` pur sur les expressions template)
|
||||
- Le composant crashe **uniquement au runtime** : `Cannot read properties of undefined` ou `[Vue warn] Property "X" was accessed during render but is not defined`
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Refactor où on extrait un composable et oublie de destructurer une variable, ou on retire un import devenu (apparemment) inutilisé
|
||||
- Typecheck passe, tests structurels passent, page charge mais une section ne s'affiche pas / un bouton ne fait rien
|
||||
- `[Vue warn]` dans la console au mount
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
**Recommandation outillage** : migrer le `typecheck` du projet de `tsc --noEmit` vers `vue-tsc -p tsconfig.typecheck.json --noEmit`. Avec `vue-tsc`, les expressions template sont strictement typées contre le `<script setup>` exposé. Une ref orpheline → erreur de compile, pas warning runtime.
|
||||
|
||||
**Checklist étendue avant de marquer une extraction "done"** :
|
||||
|
||||
```bash
|
||||
# Pour chaque symbole supprimé/non destructuré, grep dans le template
|
||||
for symbol in normalizedQuery directoryOfficeLabel; do
|
||||
echo "=== $symbol ==="
|
||||
# Patterns template critiques
|
||||
grep -nE "v-(if|else-if|show)=\"[^\"]*\\b$symbol\\b" apps/frontend/src/pages/<Page>.vue
|
||||
grep -nE "\\{\\{[^}]*\\b$symbol\\b" apps/frontend/src/pages/<Page>.vue
|
||||
grep -nE "(:|@)[a-z-]+=\"[^\"]*\\b$symbol\\b" apps/frontend/src/pages/<Page>.vue
|
||||
done
|
||||
```
|
||||
|
||||
**QA visuel obligatoire post-refactor** : pour tout refactor qui touche une page importante, ouvrir la page en browser dev avant de pousser :
|
||||
- naviguer aux états critiques (search, modals, toggle accordion)
|
||||
- vérifier la console : zéro `[Vue warn]` ou `ReferenceError`
|
||||
- tester au moins un workflow complet par section refactorée
|
||||
|
||||
- Contexte technique : Vue 3 / Volar / `vue-tsc` — RL799_V2 29-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-symboles-orphelins-suppression-bloc"></a>
|
||||
## Symboles orphelins après suppression d'un bloc — checklist grep
|
||||
|
||||
### Risques
|
||||
|
||||
- Refactor où on supprime un bloc cohérent (state + computeds + handlers) en utilisant un Edit ciblé. Tout compile, tous les tests passent, mais au runtime : `ReferenceError: <symbole> is not defined` au mount
|
||||
- Le symbole supprimé était encore référencé dans un **lifecycle hook** (`onMounted`, `onUnmounted`), un **watcher**, ou un **handler asynchrone** non inclus dans le bloc supprimé
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Page qui ne mount plus après refactor, alors que typecheck OK + tests verts
|
||||
- Erreur visible uniquement à `cmd+R` sur la page
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```bash
|
||||
# Pour chaque symbole déclaré dans le bloc à supprimer
|
||||
for symbol in updatePointerType pointerMql closeInsertMenuOnOutsideClick; do
|
||||
echo "=== $symbol ==="
|
||||
grep -n "\\b$symbol\\b" apps/frontend/src/pages/<Page>.vue
|
||||
done
|
||||
```
|
||||
|
||||
Doit retourner **uniquement les lignes du bloc à supprimer**. Si une référence apparaît hors du bloc → ne pas supprimer sans traiter explicitement (déplacer, adapter, ou laisser).
|
||||
|
||||
**Lieux à vérifier en priorité** :
|
||||
|
||||
| Lieu | Fréquence du piège |
|
||||
|------|--------------------|
|
||||
| `onMounted(() => { ... })` | ⚠️⚠️⚠️ très fréquent |
|
||||
| `onUnmounted(() => { ... })` | ⚠️⚠️⚠️ très fréquent (cleanup) |
|
||||
| `watch(() => x, () => { fn() })` | ⚠️⚠️ fréquent |
|
||||
| Handler async (`.then(() => fn())`) | ⚠️ rare mais existe |
|
||||
| Computed dans une autre section du fichier | ⚠️ rare |
|
||||
| Template (`@click`, `:disabled`) | typecheck attrape via SFC plugin |
|
||||
|
||||
**Garde-fou complémentaire** : QA visuel obligatoire post-refactor (cf. `risque-templates-vue-references-orphelines`).
|
||||
|
||||
- Contexte technique : Vue 3 — RL799_V2 29-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-extraction-vue-ts-bug-typage-latent"></a>
|
||||
## Extraction `.vue` → composable `.ts` révèle des bugs de typage latents
|
||||
|
||||
### Risques
|
||||
|
||||
- Vue 3 + Volar compile les blocs `<script setup>` avec une stratégie d'inférence moins agressive que `tsc` strict pur. Un handler peut compiler dans le `.vue` si le runtime n'utilise pas le champ manquant
|
||||
- Le composable extrait est compilé par `tsc -p tsconfig.typecheck.json` **hors contexte Vue** → toutes les règles strictes s'appliquent → la divergence de type devient une erreur bloquante
|
||||
- C'est un effet de bord **positif** mais qui peut bloquer l'extraction tant qu'on n'a pas diagnostiqué la divergence
|
||||
|
||||
### Symptômes
|
||||
|
||||
```
|
||||
Type 'X' is not assignable to type 'Y'.
|
||||
The types of '<champ>.<sous-champ>' are incompatible…
|
||||
Type 'A' is missing the following properties from type 'B': …
|
||||
```
|
||||
|
||||
Cas typique : un emit Vue annonçait `Detail` (type pour la liste) alors que la fonction service renvoyait `Data` (type pour la mutation). Silencieux dans le `.vue` d'origine, devenu visible dans le `.ts` extrait.
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Trois cas typiques quand l'erreur apparaît après extraction :
|
||||
|
||||
1. **Le type annoncé ne matche pas le type réellement émis** : aligner sur le type réellement émis plutôt que sur le type annoncé dans `defineEmits`. Si possible, corriger aussi la signature de l'émetteur — mais c'est un autre scope
|
||||
2. **Le `.vue` exploitait un cast implicite** : `v-if="x.foo"` réduit le union type. Dans un `.ts` extrait, narrow explicitement avec un `if` ou un type guard
|
||||
3. **Volar n'analysait pas un chemin de type complexe** : type récursif, génériques imbriqués, `Pick<...>` dans une union → extraire un alias intermédiaire propre dans `@<module>/types.ts`
|
||||
|
||||
**Quoi faire face à l'erreur** :
|
||||
|
||||
1. **NE PAS** mettre `as Foo` pour faire taire le compilateur — c'est probablement masquer le même bug sous un autre nom
|
||||
2. Identifier lequel des deux types est correct (généralement celui que la fonction service / l'API renvoie réellement)
|
||||
3. Aligner la signature du handler/composable sur ce type-là
|
||||
4. Documenter dans un commentaire au-dessus du handler que le type émis diverge du type annoncé dans `defineEmits` (si on ne corrige pas l'émetteur dans le même refactor)
|
||||
5. Ouvrir un TODO si la correction de l'émetteur est hors scope
|
||||
|
||||
**Recommandation outillage** : `vue-tsc` plutôt que `tsc` pur en typecheck (cf. `risque-templates-vue-references-orphelines`). Ce genre de divergence aurait été détecté **avant** le refactor.
|
||||
|
||||
- Contexte technique : Vue 3 / Volar / `vue-tsc` — RL799_V2 29-04-2026
|
||||
|
||||
@@ -149,6 +149,22 @@ source_projects: [app-alexandrie, app-template-resto, RL799_V2]
|
||||
|
||||
- Contexte technique : Vue 3 / node:test — RL799_V2 02-04-2026
|
||||
|
||||
### Cas additionnel : obsolescence silencieuse après refacto structurel
|
||||
|
||||
Au-delà du faux garde-fou de non-régression, un test en `readFileSync(path) + content.includes(...)` devient obsolète sans alarme dès qu'une réorganisation structurelle déplace le code visé. Trois variantes vécues :
|
||||
|
||||
1. **Fichier déplacé par scoping** (ex: `pages/X.vue` → `pages/<module>/X.vue`) → `ENOENT` au runtime, le test crashe au lieu de signaler une régression métier
|
||||
2. **Logique extraite dans un composable / sous-composant** → la chaîne attendue ne vit plus dans le `.vue` mais dans `composables/use<X>.ts` ; le `.vue` existe encore mais ne contient plus le pattern, donc le test échoue sur une assertion sans rapport avec la vraie cause
|
||||
3. **Variable supprimée du `<script setup>` mais conservée dans le template** → string-match passe (le template contient toujours la string), crash JS au mount du composant
|
||||
|
||||
**Mitigations spécifiques** :
|
||||
|
||||
- Centraliser le `path` du fichier visé dans une constante en tête de fichier de test (pas `resolve(...)` inline) — facilite le rerouting en cas de refacto
|
||||
- Lors d'une extraction de logique dans un composable / sous-composant, grep les tests structurels qui pointaient le fichier d'origine et les rediriger vers le nouveau chemin
|
||||
- Pour les composants interactifs (formulaires, modales, listes avec actions), compléter le string-match par au moins un test de mount via `@vue/test-utils` qui vérifie le render sans crash — c'est le seul moyen de valider la cohérence script ↔ template
|
||||
|
||||
- Contexte technique : Vue 3 / vitest — RL799_V2 30-04-2026 (3 cas observés sur la même session)
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-catch-false-test-skip-e2e"></a>
|
||||
@@ -171,3 +187,129 @@ source_projects: [app-alexandrie, app-template-resto, RL799_V2]
|
||||
- **Signal review** : `.catch(() => false)` suivi de `test.skip` dans un test E2E
|
||||
|
||||
- Contexte technique : Playwright / E2E — RL799_V2 08-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-tests-e2e-6-causes-racines"></a>
|
||||
## Tests E2E qui rotent — 6 causes-racines récurrentes
|
||||
|
||||
### Risques
|
||||
|
||||
- Sur une suite E2E mature, les fails ne viennent presque jamais d'un bug applicatif : ils viennent d'un désalignement test ↔ code de prod
|
||||
- Conclure à une régression métier alors que c'est du test obsolète fait perdre du temps et masque les vraies régressions
|
||||
|
||||
### Symptômes
|
||||
|
||||
Les 6 patterns observés sur RL799_V2 (Playwright + Vue 3 + refactors UI fréquents) :
|
||||
|
||||
1. **Testid changé sans MAJ tests** : `getByTestId('library-entries')` timeout, mais le composant expose `data-testid="document-list"`. Cause : refactor d'un composant qui fusionne plusieurs vues en un composant générique avec un testid neutre.
|
||||
|
||||
2. **Labels métier qui changent** : `await expect(badge).toHaveText('Publiée')` échoue, le badge affiche désormais 'Convoquée'. Cause : refactor lifecycle qui renomme les labels affichés sans toucher aux testids structurels.
|
||||
|
||||
3. **Menus / dropdowns conditionnels** : `getByTestId('odj-insert-menu').click()` timeout aléatoire — parfois le menu s'ouvre, parfois pas. Cause : UX qui adapte le flow selon l'état (1 seul type → bouton direct, plusieurs → menu).
|
||||
|
||||
4. **Features supprimées** : `await page.goto('/secretaire?soireeId=xxx')` charge la page mais ne sélectionne plus la soirée. Cause : query param retiré au profit d'une navigation par onglets + click sur card.
|
||||
|
||||
5. **Refactor visuel** : `await expect(badge).toHaveText('A∴')` échoue, le badge affiche désormais une icône SVG. Cause : refactor de représentation (texte → icône) sans toucher au testid.
|
||||
|
||||
6. **Cleanup post-test** : test métier passe en 2 s, mais le `finally { await restoreEntry() }` timeout à 30 s. Cause : le PATCH de cleanup tape sur une route lente (audit log, notif, validation).
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
À chaque diagnostic E2E, vérifier d'abord ces 6 hypothèses avant de conclure à une régression métier :
|
||||
|
||||
- **Cause 1** : grep `data-testid` dans le composant cible avant de modifier le test. Ne jamais "deviner" le testid à partir du nom de la page.
|
||||
- **Cause 2** : préférer asserter sur des classes CSS modifier (`.badge--published`) ou des testids d'état (`data-testid="status-published"`) plutôt que sur du texte humain (cf. `pattern-asserter-classe-css-modifier-vs-texte` dans `frontend/patterns/tests.md`).
|
||||
- **Cause 3** : guard conditionnel via `isVisible({ timeout: 1_000 }).catch(() => false)` pour gérer les deux branches.
|
||||
- **Cause 4** : quand un test commence par une URL avec query param, vérifier en premier que ce param est encore consommé par la page (grep `useRoute` / `route.query` dans le composant).
|
||||
- **Cause 5** : asserter la classe CSS modifier (plus stable que innerHTML qui contiendrait le SVG).
|
||||
- **Cause 6** : cleanup best-effort avec timeout court (cf. `pattern-cleanup-e2e-best-effort` dans `frontend/patterns/tests.md`).
|
||||
|
||||
### Méta-leçon
|
||||
|
||||
Quand on découvre N fails E2E après une période de refactor intense :
|
||||
|
||||
1. Lancer la suite complète une fois pour avoir la liste exhaustive
|
||||
2. Trier par cause-racine plutôt que par fichier
|
||||
3. Fixer en lots cohérents (1 commit par cause-racine) plutôt qu'1 commit par fail
|
||||
4. Capitaliser les patterns dès qu'ils se répètent (> 2 occurrences)
|
||||
|
||||
- Contexte technique : Playwright / Vue 3 — RL799_V2 25-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-tests-string-match-repointer-composant"></a>
|
||||
## Tests `string-match .vue` — limites et compléments après extraction
|
||||
|
||||
### Risques
|
||||
|
||||
- Quand on extrait une section/onglet vers un sous-composant, les assertions `readFileSync + content.includes('Ordre du jour')` échouent — la string est maintenant dans le sous-composant, pas dans la page
|
||||
- Mauvaises réactions : supprimer le test (perd la garantie), `.skip()` (dette accumulée), inverser en `toBeFalsy()` (régression masquée), repointer aveuglément (peut camoufler un problème)
|
||||
|
||||
### Symptômes
|
||||
|
||||
```
|
||||
AssertionError: expected false to be truthy
|
||||
expect(tenuesPage.includes('Ordre du jour')).toBeTruthy();
|
||||
^
|
||||
```
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
**Diagnostic** : lire l'assertion et identifier ce qu'elle garantit (présence d'un comportement métier, d'un data-testid critique, d'un ordre visuel).
|
||||
|
||||
**Repointer correctement** :
|
||||
|
||||
```typescript
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(here, '../../../..');
|
||||
|
||||
// Page coquille (ce qui reste : layout, tabs, rendu conditionnel)
|
||||
const tenuesPage = readFileSync(
|
||||
resolve(root, 'src/pages/tenues/TenuesPage.vue'),
|
||||
'utf-8',
|
||||
);
|
||||
// Sous-composant qui incarne désormais le markup d'un onglet
|
||||
const prochaineView = readFileSync(
|
||||
resolve(root, 'src/pages/tenues/components/ProchaineTenueView.vue'),
|
||||
'utf-8',
|
||||
);
|
||||
// Composable qui incarne désormais la logique d'un onglet
|
||||
const useProchaine = readFileSync(
|
||||
resolve(root, 'src/pages/tenues/composables/useProchaineTenue.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
test('TenuesPage utilise le modèle de vue testable pour tab/titre', () => {
|
||||
expect(tenuesPage.includes('resolveTenuesTab')).toBeTruthy();
|
||||
expect(useProchaine.includes('getProchaineTenueTitle')).toBeTruthy();
|
||||
});
|
||||
```
|
||||
|
||||
**Renommer aussi le test si pertinent** :
|
||||
|
||||
```typescript
|
||||
// Avant
|
||||
test('TenuesPage redirige vers une page dédiée en cas de 403...', () => { /* … */ });
|
||||
|
||||
// Après — le nom devient un index sémantique
|
||||
test('usePastTenues redirige vers une page dédiée en cas de 403...', () => { /* … */ });
|
||||
```
|
||||
|
||||
### Anti-pattern : tests structurels qui bougent en cascade
|
||||
|
||||
Si tes tests doivent être systématiquement mis à jour à chaque refactor, c'est que beaucoup de garanties sont vérifiées par string-match plutôt que par comportement. Pour les composants interactifs critiques (formulaires, listes avec actions, modales), **doubler** avec un test de mount `@vue/test-utils` qui survit aux refactors.
|
||||
|
||||
### Trois variantes vécues
|
||||
|
||||
1. **Fichier déplacé par scoping** (`pages/X.vue` → `pages/<module>/X.vue`) → `ENOENT` au runtime, le test crashe au lieu de signaler une régression métier
|
||||
2. **Logique extraite dans un composable / sous-composant** → la chaîne attendue ne vit plus dans le `.vue` ; le test échoue sur une assertion sans rapport avec la vraie cause
|
||||
3. **Variable supprimée du `<script setup>` mais conservée dans le template** → string-match passe (le template contient toujours la string), crash JS au mount du composant
|
||||
|
||||
**Mitigations spécifiques** :
|
||||
|
||||
- Centraliser le `path` du fichier visé dans une constante en tête de fichier de test — facilite le rerouting en cas de refacto
|
||||
- Lors d'une extraction, grep les tests structurels qui pointaient le fichier d'origine et les rediriger vers le nouveau chemin
|
||||
- Pour les composants interactifs, compléter par au moins un test de mount via `@vue/test-utils` qui vérifie le render sans crash
|
||||
|
||||
- Contexte technique : Vue 3 / Vitest — RL799_V2 29-04-2026
|
||||
|
||||
Reference in New Issue
Block a user