Files
_Assistant_Lead_Tech/knowledge/frontend/patterns/tests.md
MaksTinyWorkshop b3417ad77b 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>
2026-05-02 22:12:44 +02:00

21 KiB
Raw Blame History

title: Frontend — Patterns : Tests domain: frontend bucket: patterns tags: [tests, react-native, jest, styles, ui] applies_to: [implementation, review] severity: medium validated_on: 2026-04-07 source_projects: [app-alexandrie, RL799_V2]

Frontend — Patterns : Tests

Extrait de la base de connaissance Lead_tech. Voir knowledge/frontend/patterns/README.md pour l'index complet.


Pattern : Tests de styles React Native sans renderer JSX

Synthèse

  • Objectif : tester les tokens et styles de composants React Native dans un environnement Jest testEnvironment: node sans renderer JSX.
  • Contexte : config Jest avec transform: { '^.+\\.ts$': 'ts-jest' } — les .tsx ne sont pas transformés.
  • Quand l'utiliser : tokens de thème, logique pure, valeurs de style exportées.
  • Quand l'éviter : rendu conditionnel (styles dynamiques inline) — nécessite @testing-library/react-native.

Analyse

  • Avantages :
    • teste que le composant utilise les bons tokens, pas seulement que les tokens ont des valeurs
    • détecte les régressions de style sans renderer
    • rapide, aucune config Jest supplémentaire
  • Limites / vigilance :
    • ne teste pas le style calculé au runtime (style conditionnel dynamique)

Validation

  • Validé le : 19-03-2026
  • Contexte technique : React Native / Jest / ts-jest — app-alexandrie story 0.2

Implémentation

// Button.tsx — exporter le StyleSheet avec un nom préfixé
export const buttonStyles = StyleSheet.create({
  base: { borderRadius: 20, height: 57 },
  primary: { backgroundColor: colors.primary },
});
export function Button(...) { ... }

// ui-components.spec.ts — importer et vérifier les tokens
import { buttonStyles } from './Button';
import { colors } from '@/theme';

it('variante primary utilise colors.primary', () => {
  expect(buttonStyles.primary.backgroundColor).toBe(colors.primary);
});

Deux niveaux de tests UI recommandés

  1. .spec.ts (node) : tokens, valeurs, logique pure
  2. .spec.tsx (config séparée avec renderer) : rendu visuel, interactions

Pattern : Niveaux de test frontend Vue

Synthèse

  • Objectif : clarifier quand utiliser chaque niveau de test frontend Vue (structurel, composant monté, E2E).
  • Contexte : les tests frontend du projet sont du string-matching sur le source .vue (readFileSync + includes). Ce pattern est rapide mais ne valide pas le comportement réel.
  • Quand l'utiliser : à chaque choix de stratégie de test sur un composant Vue.

Niveaux

Niveau Outil Quand l'utiliser
Structurel (string-matching) node:test + readFileSync Smoke tests : vérifier qu'un composant contient les imports, props, slots attendus. Acceptable pour MVP/sprint rapide.
Composant monté @vue/test-utils + vitest Valider le comportement interactif (toggle, emit, slots conditionnels). Obligatoire dès qu'il y a de la logique UI.
E2E Playwright Parcours critiques multi-pages.

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.

Validation

  • Validé le : 03-04-2026
  • Contexte technique : Vue 3 / node:test — RL799_V2 story 6A.8

Pattern : Vérifier l'ordre DOM avec compareDocumentPosition, pas boundingBox

Synthèse

  • Objectif : valider l'ordre réel des éléments dans le DOM, indépendamment du rendu CSS.
  • Contexte : tests E2E Playwright qui doivent vérifier l'ordre d'affichage de sections ou d'éléments.
  • Quand l'utiliser : toute assertion d'ordre dans un test E2E.
  • Quand l'éviter : si on veut explicitement tester la position visuelle CSS (rare).

Analyse

  • Avantages :
    • vérifie l'ordre DOM réel, insensible aux propriétés CSS (flex-order, position, transform)
    • pas de null en retour (contrairement à boundingBox() hors viewport)
    • déterministe
  • Limites / vigilance :
    • ne vérifie pas la position visuelle — si le test doit valider un rendu CSS spécifique, boundingBox reste pertinent

Validation

  • Validé le : 08-04-2026
  • Contexte technique : Playwright / E2E — RL799_V2 story 17-5

Implémentation

const aBeforeB = await page.evaluate(() => {
  const a = document.querySelector('[data-testid="section-a"]');
  const b = document.querySelector('[data-testid="section-b"]');
  if (!a || !b) return false;
  return (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
});
expect(aBeforeB).toBe(true);

Risque associé

boundingBox().y vérifie la position visuelle rendue par CSS, pas l'ordre dans le DOM. De plus boundingBox() retourne null pour les éléments hors viewport → crash non déterministe.


Pattern : Sélecteurs E2E stables orientés intention

Synthèse

Les tests E2E doivent cibler des sélecteurs stables (data-testid, role/name) et non la structure CSS/XPath.

Analyse

Les classes et la structure DOM changent fréquemment sans régression fonctionnelle.

Validation

  • Validé le : 14-04-2026
  • Contexte technique : Playwright / sélecteurs robustes — RL799_V2
  • Ce pattern s'applique à Playwright/Cypress sur toutes les UIs réactives.

Implémentation

  • Préférer data-testid paramétré par identifiant métier stable.
  • Éviter locator.first() si l'ordre peut muter.
  • Isoler les tests mutateurs avec stratégie de remise à l'état (snapshot/restore).

Pattern : Tests statiques readFileSync annotés comme smoke checks

Synthèse

  • Objectif : utiliser les tests readFileSync + includes() pour leur force réelle (présence de signaux structurels) tout en évitant la fausse confiance qu'ils donnent sur le comportement runtime.
  • Contexte : projets Vue où le pattern readFileSync est largement utilisé pour vérifier la présence de testid, rôle ARIA, appel de service.
  • Quand l'utiliser : assertions sur présence d'un pattern (testid, import, regex interdite). À doubler par un mount pour les comportements interactifs.
  • Quand l'éviter : composants interactifs (focus trap, submit delegation, navigation clavier) — utiliser un test mount.

Analyse

  • Avantages :
    • rapide à écrire et lire, résistant aux refactors de structure
    • bon pour vérifier la présence de comportements clés sans monter
  • Limites / vigilance :
    • ne valide PAS que le focus trap cycle correctement, que le composant émet les bons événements, que la navigation clavier Escape ferme la modal
    • ne distingue pas si une référence est dans le <script> ou le <template> (variable supprimée du script mais référencée dans le template → string-match passe, crash runtime)

Validation

  • Validé le : 21-04-2026
  • Contexte technique : Vue 3 / Vitest — RL799_V2

Annotation honnête recommandée

// Note honnête : ces assertions sont des smoke checks structurels, pas une
// vérification runtime du focus trap. Le comportement réel (Tab cycle, Escape,
// focus restauré) se valide manuellement en QA ou via un test happy-dom dédié.
test('AppDialog : pattern previousActiveElement présent', () => {
  expect(content.includes('previousActiveElement')).toBeTruthy();
});

Règle

Si un comportement runtime est critique (focus trap, submit delegation, validation conditionnelle), écrire un test happy-dom séparé avec @vue/test-utils. Le smoke check ne dispense pas du test comportemental — il documente juste la présence d'un signal.


Pattern : Asserter classe CSS modifier vs texte (E2E robuste aux refactors visuels)

Synthèse

  • Objectif : rendre les tests E2E robustes aux refactors visuels (texte → icône SVG, label v1 → label v2, i18n future).
  • Contexte : tests E2E qui valident un état rendu d'un composant.
  • Quand l'utiliser : composant utilisant une convention BEM stricte avec modifier sémantique (.grade-badge--apprenti, .status-badge--published).
  • Quand l'éviter : framework CSS-in-JS qui génère des classes hashées (_grade_x4f3z) — la classe modifier n'est pas accessible.

Analyse

  • Avantages :
    • la classe modifier porte la sémantique et change beaucoup moins souvent que le texte
    • typiquement liée à la prop ou au state, pas au rendu
  • Limites / vigilance :
    • ne remplace pas la validation visuelle (snapshot, screenshot) si le besoin est de protéger le rendu pixel-perfect
    • reste pertinent : validation de contenu utilisateur (notes, prix) où le rendu textuel est la spec

Validation

  • Validé le : 25-04-2026
  • Contexte technique : Playwright / Vue 3 — RL799_V2

Exemple

// ❌ Fragile : casse au refactor texte → SVG ou label v1 → v2
await expect(badge).toHaveText('A∴');

// ✅ Robuste : casse uniquement si le grade change ou si BEM est rompu
await expect(badge).toHaveClass(/grade-badge--apprenti/);

Cas combinable

  • Regex tolérante sur le texte (/convoqu/i) pour les cas où le texte est la seule prise (badges sans classe modifier)
  • Validation visuelle (snapshot) en complément quand le rendu pixel-perfect est protégé

Pattern : Cleanup E2E best-effort (try/catch + timeout court)

Synthèse

  • Objectif : empêcher qu'un cleanup post-test (réinitialisation d'un statut, suppression d'une fixture) fasse échouer un scénario métier qui a déjà passé.
  • Contexte : tests Playwright avec finally { await cleanup() } qui font des PATCH/DELETE/POST sur l'API.
  • Quand l'utiliser : tout cleanup post-test non critique pour les tests suivants (parce qu'on a un seed ou un autre cleanup global).
  • Quand l'éviter : si le cleanup est critique pour l'isolation (UNIQUE constraint au prochain test) — monter le timeout à 60 s plutôt que silencer.

Analyse

  • Avantages :
    • une lenteur transitoire de l'API de cleanup (audit log, notif fanout, lock DB) ne fait plus passer le test du vert au rouge
    • la dette d'état après échec de cleanup est mineure (le test suivant restaure souvent ou un seed le fera)
  • Limites / vigilance :
    • setup pré-test (beforeEach) : un setup qui échoue doit faire échouer le test, sinon on teste un état inconnu

Validation

  • Validé le : 25-04-2026
  • Contexte technique : Playwright — RL799_V2

Pattern

async function restoreEntry(page: Page, snap: Snapshot): Promise<void> {
  try {
    await page.request.patch(`/api/.../entries/${snap.entryId}/status`, {
      data: { status: snap.status },
      headers: { 'Content-Type': 'application/json' },
      timeout: 10_000,
    });
  } catch (err) {
    // eslint-disable-next-line no-console
    console.warn('[spec-name] restoreEntry échoué (cleanup best-effort):', err);
  }
}

test('mon scénario métier', async ({ page }) => {
  const snap = await snapshotEntry(page, USER_ID);
  try {
    // … scénario métier …
  } finally {
    await restoreEntry(page, snap);  // best-effort
  }
});

Pattern : Helpers Service Worker purs extraits pour tests Vitest

Synthèse

  • Objectif : permettre des tests unitaires Vitest fiables sur la logique du Service Worker, sans monter de stubs élaborés pour self / registration / caches / clients.
  • Contexte : SW custom (mode injectManifest ou similaire) qui contient de la logique non triviale (parsing payload push, validation linkUrl anti open-redirect, sélection client focused).
  • Quand l'utiliser : dès que le SW dépasse 50 lignes ou contient une regex / une condition métier.
  • Quand l'éviter : SW trivial (juste un precacheAndRoute(self.__WB_MANIFEST)).

Analyse

  • Avantages :
    • tests Vitest standards, pas de mock self/registration
    • logique partageable avec d'autres parties du frontend (regex linkUrl partagée serveur ↔ SW ↔ store)
    • le sw.ts final reste lisible : précache + routes + thin event handlers
  • Limites / vigilance :
    • les listeners addEventListener('push'|...) restent non testés unitairement → couverture par E2E + checklist DevTools manuelle
    • bien isoler les imports : pas de dépendance Vue/Pinia dans sw-helpers.ts (le SW n'a pas d'accès au DOM)
    • regex dupliquée serveur ↔ SW : extraire dans @<scope>/shared quand possible (cf. pattern-regex-critique-partagee-anti-divergence dans backend/patterns/contracts.md)

Validation

  • Validé le : 28-04-2026
  • Contexte technique : Vite + vite-plugin-pwa injectManifest / Vitest — RL799_V2

Implémentation

// apps/frontend/src/sw-helpers.ts (testable pure)
const INTERNAL_PATH_REGEX = /^\/(?!\/)[a-zA-Z0-9/_\-?&=%.]*$/;

export const isInternalPath = (url: string): boolean =>
  INTERNAL_PATH_REGEX.test(url);

export const parsePushPayload = (
  eventData: { json: () => unknown } | null | undefined,
): SwPushPayload => {
  try {
    const raw = eventData?.json() as Record<string, unknown> | null;
    if (!raw || typeof raw.title !== 'string') throw new Error('invalid');
    return {
      title: raw.title.slice(0, 80),
      body: typeof raw.body === 'string' ? raw.body.slice(0, 160) : undefined,
      linkUrl: typeof raw.linkUrl === 'string' && isInternalPath(raw.linkUrl) ? raw.linkUrl : '/',
    };
  } catch {
    return { title: 'AppDefault', body: 'Notification', linkUrl: '/' };
  }
};
// apps/frontend/src/sw.ts (thin wrappers)
/// <reference lib="webworker" />
import { isInternalPath, parsePushPayload, selectFocusedClient } from './sw-helpers';

self.addEventListener('push', (event) => {
  event.waitUntil((async () => {
    const payload = parsePushPayload(event.data ?? undefined);
    const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
    const focused = selectFocusedClient(clients);
    if (focused) {
      focused.postMessage({ type: 'push:received', payload });
      return;
    }
    await self.registration.showNotification(payload.title, {
      body: payload.body,
      data: { url: payload.linkUrl },
    });
  })());
});

Checklist

  • Toute logique conditionnelle / regex / parsing extraite dans sw-helpers.ts
  • sw.ts ne contient que precache, routes runtime, thin addEventListener qui appellent les helpers
  • Helpers testés avec Vitest standard (pas de mock self)
  • Listeners SW couverts par E2E + checklist manuelle DevTools post-build

Pattern : Test mount + mock composable contrôlable

Synthèse

  • Objectif : tester un composant Vue qui consomme un composable réactif en contrôlant l'état du composable depuis le test, sans monter de fixtures lourdes.
  • Contexte : composant <script setup> qui appelle const x = useFooBar() et utilise x.isReady.value / x.action() dans le template.
  • Quand l'utiliser : composant interactif avec branches conditionnelles dépendant du composable (5 états du footer push, modes admin vs user).
  • Quand l'éviter : composant trivial sans logique conditionnelle (wrapper de markup).

Analyse

  • Avantages :
    • couvre la cohérence script ↔ template (un attribut renommé dans le composable casse le test)
    • test indépendant de la VRAIE implémentation (pas de duplication des stubs DOM Notification, serviceWorker, etc.)
    • chaque scénario UI testable isolément en mutant l'état du mock entre tests
  • Limites / vigilance :
    • le mock doit retourner exactement la même SHAPE que le composable réel — un test peut passer alors que le composable a évolué et casse en runtime
    • mitigation : déclarer un type ReturnType<typeof useFooBar> ou export type UseFooBar exporté par le composable, et typer le mock dessus
    • vi.mock est hoisté → définir l'état du mock dans beforeEach, pas en module-level

Validation

  • Validé le : 28-04-2026
  • Contexte technique : Vitest + @vue/test-utils + Vue 3 — RL799_V2

Implémentation

// composable
export type UsePushNotifications = {
  isSupported: ComputedRef<boolean>;
  isSubscribed: Ref<boolean>;
  subscribe: () => Promise<void>;
};
export const usePushNotifications = (): UsePushNotifications => { /* … */ };
// Test du composant
import { computed, ref } from 'vue';
import type { UsePushNotifications } from '@/composables/usePushNotifications';

let mockState: { isSupported: boolean; isSubscribed: boolean };
const subscribeSpy = vi.fn();

vi.mock('@/composables/usePushNotifications', () => ({
  usePushNotifications: (): UsePushNotifications => ({
    isSupported: computed(() => mockState.isSupported),
    isSubscribed: ref(mockState.isSubscribed),
    subscribe: subscribeSpy,
  }),
}));

import MyComponent from '@/components/MyComponent.vue';

beforeEach(() => {
  subscribeSpy.mockReset();
  mockState = { isSupported: true, isSubscribed: false };
});

test('CTA visible quand !isSubscribed', () => {
  mount(MyComponent);
  expect(document.querySelector('[data-testid="cta"]')).not.toBeNull();
});

Checklist

  • Composable expose un type Use<Name> réutilisable dans les tests
  • Mock retourne une shape conforme à ce type
  • État du mock défini dans beforeEach (réinitialisé entre tests)
  • Tests utilisent data-testid (pas de couplage CSS class)
  • Couvre TOUS les états observables (pas que le happy path)

Pattern : Assertions sur le HTML rendu par React Email — pièges à éviter

Synthèse

  • Objectif : écrire des tests Vitest fiables sur le HTML produit par un template React Email sans buter sur les commentaires JSX, les glyphes Unicode bruts, et les <link rel="preload"> injectés automatiquement.
  • Contexte : tests qui vérifient la présence ou l'absence de patterns dans le HTML rendu (côté mail Resend ou côté PDF Puppeteer en previewOnly).
  • Quand l'utiliser : assertions sur le HTML rendu par @react-email/components.
  • Quand l'éviter : tests qui valident uniquement la présence d'un composant React (snapshot par exemple).

Validation

  • Validé le : 29-04-2026
  • Contexte technique : Vitest / React Email — RL799_V2

Piège 1 — Commentaires React entre fragments JSX

React Email insère <!-- --> entre 2 fragments {var} adjacents :

<p>V∴M∴ <!-- -->Pierre Vénérable</p>

Une regex /V∴M∴ Pierre/ échoue. Helper utile :

const matchAroundComments = (_source: string, ...parts: string[]): RegExp => {
  const escaped = parts.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
  return new RegExp(escaped.join('\\s*(?:<!--[^>]*-->\\s*)*'), 'i');
};

assert.match(html, matchAroundComments(html, 'V∴M∴ ', 'Pierre Vénérable'));

Piège 2 — Caractères Unicode bruts dans les constantes shared

Une constante avec (U+2019) brut est rendue telle quelle dans le HTML par React Email (pas d'encodage). Une regex avec ' ASCII ou &apos; échoue.

assert.match(html, /Dans l(?:'||&#x27;|&apos;)attente/);

Le composant <Img> de @react-email/components injecte <link rel="preload" as="image" href="…"> dans le <head> en mode mail (pas en mode previewOnly). Une assertion expect(html).not.toMatch(/<link\s+rel/i) échoue sur le mail.

Solution : autoriser les <link rel> qui pointent vers appBaseUrl (URL maîtrisée), bloquer les autres :

const linkMatches = html.match(/<link\s+[^>]*href=["']([^"']+)["']/gi) ?? [];
for (const linkTag of linkMatches) {
  const href = /href=["']([^"']+)["']/i.exec(linkTag)?.[1];
  assert.ok(href.startsWith(appBaseUrl) || href.startsWith('data:'));
}

Piège 4 — Assertions trop génériques sur des termes ambigus

Une regex /V∴M∴/ pour vérifier "ligne signature V∴M∴ vacante absente" matche aussi le footer "Le V∴M∴ et les officiers..." qui contient le même token. Solution : cibler un pattern plus précis du contexte (la classe CSS du style vmSignature : font-weight:600).

Garde-fou Puppeteer mode PDF

En mode previewOnly, le HTML doit être strictement offline-safe :

assert.doesNotMatch(html, /<link\s+rel/i);  // pas de preload (≠ mode mail)
assert.doesNotMatch(html, /<script\s+src/i);
assert.doesNotMatch(html, /<img[^>]+src=["'](?!data:)/i);  // pas de http(s)