mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-05-18 08:18:15 +02:00
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>
521 lines
21 KiB
Markdown
521 lines
21 KiB
Markdown
---
|
||
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.
|
||
|
||
---
|
||
|
||
<a id="pattern-tests-styles-sans-renderer"></a>
|
||
## 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
---
|
||
|
||
<a id="pattern-niveaux-test-frontend-vue"></a>
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-ordre-dom-comparedocumentposition"></a>
|
||
## 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
|
||
|
||
```ts
|
||
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.
|
||
|
||
---
|
||
|
||
<a id="pattern-tests-selecteurs-e2e-stables"></a>
|
||
## 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).
|
||
|
||
---
|
||
|
||
<a id="pattern-tests-statiques-vitest-smoke-checks"></a>
|
||
## 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
|
||
|
||
```typescript
|
||
// 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.
|
||
|
||
---
|
||
|
||
<a id="pattern-asserter-classe-css-modifier-vs-texte"></a>
|
||
## 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
|
||
|
||
```typescript
|
||
// ❌ 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é
|
||
|
||
---
|
||
|
||
<a id="pattern-cleanup-e2e-best-effort"></a>
|
||
## 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
|
||
|
||
```typescript
|
||
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
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
<a id="pattern-helpers-sw-purs-extraits"></a>
|
||
## 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
|
||
|
||
```typescript
|
||
// 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: '/' };
|
||
}
|
||
};
|
||
```
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
---
|
||
|
||
<a id="pattern-test-mount-mock-composable-controllable"></a>
|
||
## 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
|
||
|
||
```typescript
|
||
// composable
|
||
export type UsePushNotifications = {
|
||
isSupported: ComputedRef<boolean>;
|
||
isSubscribed: Ref<boolean>;
|
||
subscribe: () => Promise<void>;
|
||
};
|
||
export const usePushNotifications = (): UsePushNotifications => { /* … */ };
|
||
```
|
||
|
||
```typescript
|
||
// 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)
|
||
|
||
---
|
||
|
||
<a id="pattern-assertions-html-react-email"></a>
|
||
## 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 :
|
||
|
||
```html
|
||
<p>V∴M∴ <!-- -->Pierre Vénérable</p>
|
||
```
|
||
|
||
Une regex `/V∴M∴ Pierre/` échoue. Helper utile :
|
||
|
||
```typescript
|
||
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 `'` échoue.
|
||
|
||
```typescript
|
||
assert.match(html, /Dans l(?:'|’|'|')attente/);
|
||
```
|
||
|
||
### Piège 3 — `<link rel="preload">` injecté automatiquement par `<Img>`
|
||
|
||
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 :
|
||
|
||
```typescript
|
||
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 :
|
||
|
||
```typescript
|
||
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)
|
||
```
|