Triage et intégration des propositions frontend du buffer 95_a_capitaliser.md (lot local RL799_V2/Vue3 + app-alexandrie/RN-Expo, mai-juin 2026). ~73 entrées intégrées sur knowledge/frontend/ + 1 nouveau fichier, dont : - patterns/state.md : race-token partagé latest-wins (fusion 3 props), capture sync anti-race, event bus timestamp, clé cache composite, état dérivé = computed - risques/state.md : 9 risques Zustand/store (fetchId reset, useRef remount, re-fetch infini sur [], flag optimiste écrasé, cache détail/liste stale, latch sans reset, :key index) - patterns/navigation.md : Expo Router (tab bar, useFocusEffect, Href typé, routing pur fusionné) - patterns/general.md : helpers temps purs, composants génériques + skeleton, fail-fast, touch target - risques/general.md : 24 risques (sweep statique, filtre client liste paginée, hooks avant return, a11y VoiceOver/disabled, redirection allowlist, RangeError toISOString, section i18n...) - design-tokens (cluster theming light/dark MD3), tests, performance, react-native, nextjs - NOUVEAU risques/responsive.md (gating par capacité d'input + checklist régressions mobile) - READMEs patterns/risques mis à jour Doublons inter-fichiers évités (vérifié : aucune ancre dupliquée introduite). Rejets (doublons 91/9/87), reciblages workflow (156/257) et bloc 32 (CLAUDE projet) non intégrés ici. Source 95_a_capitaliser.md non purgée (purge en fin de capitalisation complète). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
21 KiB
title: Frontend — Risques & vigilance : Tests domain: frontend bucket: risques tags: [tests, jest, react-native, ts-jest, coverage, facade] applies_to: [analysis, implementation, review, debug] severity: high validated_on: 2026-04-07 source_projects: [app-alexandrie, app-template-resto, RL799_V2]
Frontend — Risques & vigilance : Tests
Extrait de la base de connaissance Lead_tech. Voir
knowledge/frontend/risques/README.mdpour l'index complet.
Jest React Native — config node bloque les composants .tsx
Risques
SyntaxError: Cannot use import statement outside a modulelors de l'import d'un barrel.tsqui réexporte des.tsx- Impossible d'importer des composants React Native dans les tests — JSX non transformé
Symptômes
- Erreur de syntaxe inattendue au run des tests sur un fichier
.tsqui importe un.tsx - Les tests de tokens passent mais tout test touchant un composant échoue
Bonnes pratiques / mitigations
transform: { '^.+\\.ts$': 'ts-jest' }ne transforme que.ts— pas.tsx- Pattern recommandé : tester la logique pure (tokens, valeurs de style) dans
.spec.ts, le rendu visuel dans.spec.tsxavec une config séparée (@testing-library/react-native+babel-jest) - Exporter le
StyleSheetde chaque composant pour le tester sans JSX (voir pattern dédié dans10_frontend_patterns_valides.md) - Contexte technique : React Native / Jest / ts-jest — app-alexandrie 19-03-2026
Faux test négatif — tester le helper au lieu de tester l'exclusion
Risques
- Un test nommé "X n'utilise pas Y" qui appelle Y en interne est un test normal mal documenté, pas un test d'exclusion
- Donne une fausse confiance sur le comportement par défaut du helper
Symptômes
- Test intitulé "sans fallback, la valeur EN vide n'est pas remplacée" mais qui appelle le helper avec fallback activé
Bonnes pratiques / mitigations
-
Un vrai test négatif vérifie que X n'importe pas Y, ou que le comportement par défaut empêche l'effet indésirable
-
Pour un helper à fallback optionnel : tester explicitement le cas
fallbackToFr=false(défaut) avec une valeur vide -
Contexte technique : TypeScript / Jest — app-template-resto 17-03-2026
Helpers copiés localement dans les tests (faux positif permanent)
Risques
- La logique réellement exécutée en production peut diverger du helper copié dans le test sans casser aucun test.
- Un refactor du module source ne casse pas le test — la couverture est illusoire.
Symptômes
- Fonctions utilitaires redéfinies dans
*.spec.tsplutôt qu'importées depuis le module de production - Tests verts malgré une régression dans le code source
Bonnes pratiques / mitigations
-
Les tests doivent importer le module réellement utilisé par l'écran/composant, jamais dupliquer la logique.
-
Si la logique est partagée entre écran et test, l'extraire dans un utilitaire partagé (single source of truth).
-
Checklist review : aucune fonction de production recopiée dans
*.spec.ts. -
Contexte technique : TypeScript / Jest — 30-03-2026
Test d'écran indirect — logique UI validée via helper adjacent non relié au flux
Risques
- Le test reste vert même si la logique de décision UI dans le screen diverge du helper testé.
- Un changement d'implémentation de l'écran ne casse aucun test.
Symptômes
*.spec.tsd'un écran qui n'importe pas le composant/écran mais seulement un helper utilitaire adjacent- Couverture affichée OK mais comportement réel de l'écran non testé
Bonnes pratiques / mitigations
-
La logique de décision UI doit être soit testée via rendu composant (
@testing-library/react-native), soit extraite dans un module dédié importé par le screen ET par le test (single source of truth). -
Règle : un test qui n'importe pas le composant écran ni son module de logique ne peut pas valider le comportement de l'écran.
-
Contexte technique : React Native / Jest — 30-03-2026
Test de façade — helpers adjacents validés à la place du flux réel
Risques
- Une tâche d'intégration (conversion, persistance, atomicité, contrat UI) est clôturée alors que les tests ne valident que des helpers purs (chemins, labels, sanitization).
- Le pipeline réel, la persistance DB et les transitions UI ne sont jamais exercés.
Symptômes
- Tests
*.spec.tscréés uniquement sur des fonctions utilitaires pures pour une story qui promet un pipeline - La tâche est cochée ✅ mais aucun test n'appelle le module de traitement de production
Bonnes pratiques / mitigations
-
Si une tâche promet conversion, statut DB, atomicité ou contrat UI/public : le test doit appeler le module de production correspondant ou un seam injecté unique.
-
Règle : un test qui ne vérifie que des helpers adjacents ne peut pas clôturer une tâche d'intégration.
-
Contexte technique : TypeScript / Jest — app-template-resto 31-03-2026
Tests de présence textuelle = faux garde-fou de non-régression
Risques
- Des tests basés uniquement sur
content.includes(...)valident du texte statique tout en laissant passer des régressions réelles sur auth/API et comportements UI - Les AC fonctionnels sont déclarés validés alors que le flux réel (autosave, submit, transitions d'état) n'est pas exercé
Symptômes
- Tests qui lisent le fichier
.vueet assertent uniquement des chaînes (includes) sans exécuter le composant ni ses interactions - Story cochée malgré des régressions fonctionnelles invisibles aux tests verts
- Services API et guards d'accès validés par des assertions textuelles au lieu de tests comportementaux
Bonnes pratiques / mitigations
-
Exiger au moins un test comportemental par flux critique (montage composant + interaction + assertion d'effet)
-
Reléguer les tests textuels au rôle de smoke structurel non bloquant
-
Pour les services API et guards d'accès : exiger un test exécutant réellement la fonction (mock fetch/session/router) et validant statuts d'erreur + contrat d'appel
-
Pour les templates/checklists critiques : ne pas se limiter à la présence de mots-clés, valider la structure attendue (sections obligatoires, champs non vides, format minimal)
-
Règle : si un test vérifie un comportement (ex: "le menu se ferme après clic"), il doit monter le composant, pas chercher une string dans le source
-
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 :
- Fichier déplacé par scoping (ex:
pages/X.vue→pages/<module>/X.vue) →ENOENTau runtime, le test crashe au lieu de signaler une régression métier - Logique extraite dans un composable / sous-composant → la chaîne attendue ne vit plus dans le
.vuemais danscomposables/use<X>.ts; le.vueexiste encore mais ne contient plus le pattern, donc le test échoue sur une assertion sans rapport avec la vraie cause - 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
pathdu fichier visé dans une constante en tête de fichier de test (pasresolve(...)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-utilsqui 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)
Anti-pattern : .catch(() => false) + test.skip dans les tests E2E
Risques
- Le pattern
await locator.isVisible().catch(() => false)suivi detest.skip(true, ...)masque les erreurs réelles (sélecteur cassé, timeout, changement de structure) derrière un skip silencieux - Un AC peut rester perpétuellement non testé sans qu'aucun rapport ne le signale comme problème
Symptômes
- Tests skippés en permanence dans les rapports CI
- AC marqués
[x]dans la story mais jamais réellement validés
Bonnes pratiques / mitigations
-
Pour vérifier si un élément optionnel est présent (données dépendantes du seed), utiliser
await locator.count()qui retourne 0 sans lancer d'exception, puistest.skipuniquement si count === 0 -
Réserver
.catch()aux cas où une exception est réellement attendue et documentée -
Signal review :
.catch(() => false)suivi detest.skipdans un test E2E -
Contexte technique : Playwright / E2E — RL799_V2 08-04-2026
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) :
-
Testid changé sans MAJ tests :
getByTestId('library-entries')timeout, mais le composant exposedata-testid="document-list". Cause : refactor d'un composant qui fusionne plusieurs vues en un composant générique avec un testid neutre. -
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. -
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). -
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. -
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. -
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-testiddans 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-textedansfrontend/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.querydans 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-effortdansfrontend/patterns/tests.md).
Méta-leçon
Quand on découvre N fails E2E après une période de refactor intense :
- Lancer la suite complète une fois pour avoir la liste exhaustive
- Trier par cause-racine plutôt que par fichier
- Fixer en lots cohérents (1 commit par cause-racine) plutôt qu'1 commit par fail
- Capitaliser les patterns dès qu'ils se répètent (> 2 occurrences)
- Contexte technique : Playwright / Vue 3 — RL799_V2 25-04-2026
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 entoBeFalsy()(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 :
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 :
// 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
- Fichier déplacé par scoping (
pages/X.vue→pages/<module>/X.vue) →ENOENTau runtime, le test crashe au lieu de signaler une régression métier - 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 - 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
pathdu 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-utilsqui vérifie le render sans crash -
Contexte technique : Vue 3 / Vitest — RL799_V2 29-04-2026
Dupliquer les styles pour tester un composant qui utilise un hook
Risques
- Pour rendre testable un composant qui style via un hook (
useThemedColors), on duplique les styles enexport const xxxStyles = StyleSheet.create({...})à côté dumakeStyles(themed)interne - Toute modif de
makeStylesne propage pas à la copie statique → les tests passent sur du code mort ; la régression n'est vue qu'au smoke device
Symptômes
- Deux
StyleSheet.createdans le même fichier (interne + exporté pour les tests) avec les mêmes définitions
Bonnes pratiques / mitigations
// ✅ passer la palette statique à makeStyles : 1 ligne, 0 duplication, source unique
export const sectionHeaderStyles = makeStyles(colors);
- Si le composant n'expose pas
makeStyles, exposer la fonction (pas le résultat) et testermakeStyles(mock) - Garde-fou review : deux
StyleSheet.createdans le même fichier = suspicion de duplication - Contexte technique : React Native — app-alexandrie (ux-cleanup-5,
SectionHeader.tsx), 29-05-2026
Fix visuel de plumbing sans test = régression silencieuse garantie
Risques
- Un bug "visuel" venant d'un câblage cassé (theme provider, navigation theme, font/locale loader) fixé directement dans le
useMemoracine → non testable - Une PR future qui re-câble le mauvais provider ne casse rien en CI mais ré-introduit le bug en prod
Symptômes
- Fix livré sans test de non-régression alors que la cause root est testable en isolation
Bonnes pratiques / mitigations
- Extraire la dérivation en module pur (
buildXxxFrom(scheme, tokens) → Theme) - Ajouter ≥ 2 tests : un par scheme + un garde-fou prouvant que les valeurs natives OS ne fuient pas (
expect(theme.colors.background).not.toBe('#ffffff')) - Contexte technique : React Native — app-alexandrie (ux-cleanup-8 H2,
navigation-theme.ts), 29-05-2026
Test de ciblage/fan-out qui assert le prédicat mais pas le COMPTE
Risques
assert(recipients.every(r => r.grade === 'Compagnon'))passe AUSSI si on ne notifie qu'1 destinataire sur 9 (ou zéro) —.every()sur une liste partielle/vide est vrai par vacuité- Un bug de complétude (ciblage partiel, exclusion silencieuse d'actifs) reste invisible
Symptômes
- Test "X notifie/cible Y" vert alors que le ciblage est incomplet
Bonnes pratiques / mitigations
- Ajouter
assert.equal(recipients.length, EXPECTED)où EXPECTED est calculé dynamiquement depuis les fixtures (seedUsers.filter(...)), jamais hardcodé - Le
.every()valide la pureté du ciblage, le.lengthvalide la complétude — les deux sont nécessaires - Contexte technique : tests de notification — RL799 (review v2-1-3), 13-06-2026
Stub de composant enfant en mount test : déclarer aussi les props CALCULÉES
Risques
- Un stub d'enfant qui ne déclare que les props affichées laisse non testée la logique de calcul des props non lues (
:variant="sourceVariant(x)") vue-tscvalide que la valeur est une variante acceptée, mais PAS que la bonne branche est prise au runtime → une inversion de logique passe typecheck + mount
Symptômes
- Une fonction de calcul de prop n'est jamais exercée au runtime ; seul le typecheck la couvre
Bonnes pratiques / mitigations
- Le stub expose la prop calculée (
props: ['label','variant']) et la reflète dans un attribut testable (:data-variant="variant") ; le test asserte la valeur - Règle : tout
:prop="fn(...)"dans un template mérite une assertion runtime sur la valeur résultante (surtout si elle prépare une extension future) - Contexte technique : Vue 3 / @vue/test-utils — RL799, 22-06-2026
Module gardé par office (rôle dérivé) — 2 trous de test systématiques
Risques
- Pattern « guard backend
requireOffice(office)+ nav item conditionné + icôneOfficerRoleIcon» : deux assertions manquent quasi toujours sans que la suite verte ne le rattrape - (1) aucun test ne valide le rendu de l'icône d'office en navbar mobile (si
isOfficerRole(office)renvoiefalseou si le wrapper de taille manque, l'icône ne rend rien / déborde, sans erreur) - (2) le test d'accès API mocke les offices via le token (
createTestToken({ offices: ['x'] })), pas via un vrai mandat en DB → la chaîne LIVEmandat actif → offices résolus → accèsn'est jamais couverte bout-en-bout
Symptômes
- Tous les tests passent alors que l'icône d'office ne rend rien et que la chaîne mandat→accès n'est pas testée
Bonnes pratiques / mitigations
- Test mount transverse : pour CHAQUE office routé, vérifier que le nav item rend une icône d'office non vide et bornée en taille
- Au moins un test par domaine créant un mandat actif en DB → accès 200, et un mandat révoqué → 403
- À traiter dans un chantier « tests offices » dédié (raffinement transverse, pas story par story) mais à décider sciemment dès le câblage du 1er office
- Contexte technique : Vue 3 / backend — RL799 (review v2-5-2), 22-06-2026