Files
_Assistant_Lead_Tech/knowledge/frontend/risques/tests.md
T
MaksTinyWorkshop 5f5c87296e docs(knowledge): capitalisation frontend — intégration du triage local (mai-juin 2026)
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>
2026-06-25 15:31:53 +02:00

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.md pour l'index complet.


Jest React Native — config node bloque les composants .tsx

Risques

  • SyntaxError: Cannot use import statement outside a module lors de l'import d'un barrel .ts qui 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 .ts qui 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.tsx avec une config séparée (@testing-library/react-native + babel-jest)
  • Exporter le StyleSheet de chaque composant pour le tester sans JSX (voir pattern dédié dans 10_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.ts plutô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.ts d'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.ts créé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 .vue et 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 :

  1. Fichier déplacé par scoping (ex: pages/X.vuepages/<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)


Anti-pattern : .catch(() => false) + test.skip dans les tests E2E

Risques

  • Le pattern await locator.isVisible().catch(() => false) suivi de test.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, puis test.skip uniquement si count === 0

  • Réserver .catch() aux cas où une exception est réellement attendue et documentée

  • Signal review : .catch(() => false) suivi de test.skip dans 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) :

  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

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 :

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

  1. Fichier déplacé par scoping (pages/X.vuepages/<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


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 en export const xxxStyles = StyleSheet.create({...}) à côté du makeStyles(themed) interne
  • Toute modif de makeStyles ne 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.create dans 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 tester makeStyles(mock)
  • Garde-fou review : deux StyleSheet.create dans 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 useMemo racine → 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 .length valide 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-tsc valide 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ône OfficerRoleIcon » : 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) renvoie false ou 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 LIVE mandat actif → offices résolus → accès n'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

  1. 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
  2. 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