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

---

<a id="risque-jest-rn-config-node"></a>
## 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

---

<a id="risque-faux-test-negatif"></a>
## 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

---

<a id="risque-helpers-copies-tests"></a>
## 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

---

<a id="risque-test-ecran-indirect"></a>
## 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

---

<a id="risque-test-facade-flux-reel"></a>
## 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

---

<a id="risque-tests-presence-textuelle-faux-gardefou"></a>
## 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.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>
## 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

---

<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

---

<a id="risque-dupliquer-styles-pour-tester-hook"></a>
## 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

```typescript
// ✅ 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

---

<a id="risque-fix-visuel-plumbing-sans-test"></a>
## 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

---

<a id="risque-test-fanout-sans-compte"></a>
## 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

---

<a id="risque-stub-enfant-props-calculees"></a>
## 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

---

<a id="risque-module-garde-par-office-trous-test"></a>
## 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
