docs(architecture): add the tests section and the hook harness
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m57s

Resolves #409

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-10-12 19:44:43 -04:00
parent 1ba3b8bae9
commit 79ba1ed2f3

View file

@ -587,6 +587,36 @@ Page spéciale : `ProfileSelectionPage` (affichée quand aucun profil n'est acti
- **Clés organisées** hiérarchiquement par domaine (`nav.*`, `dashboard.*`, `import.*`, etc.)
- **Formateur `lowercase`** (`src/i18n/config.ts`) : `{{valeur, lowercase}}` met en minuscules, selon la langue, un libellé interpolé dans une phrase depuis sa propre clé (le filtre « Non catégorisées » dans la carte de démarrage de la page Transactions, #399). La phrase décide de la casse, pas le code.
## Tests (vitest sans jsdom)
Le dépôt n'a pas de jsdom : les tests rendent les composants par `renderToStaticMarkup`, ou appellent hooks et composants comme des fonctions pour lire l'arbre rendu et les props. Un rendu statique ne lance aucun effet et aucun clic ; ce qu'un composant fait avec React entre deux rendus passe par un faux module `react`.
### Harnais de hooks — `src/test/hookHarness.ts` (#409)
Un seul harnais pour tous les tests qui simulent React. Motif d'import, unique, en tête du fichier de test :
```ts
vi.mock("react", async (orig) => (await import("../test/hookHarness")).fakeReact(await orig()));
import { hookHarness } from "../test/hookHarness";
const harness = hookHarness();
```
Puis `harness.clear()` dans `beforeEach` (un montage neuf), `harness.render(() => Composant(props))` pour un rendu du composant monté, `harness.commit()` pour jouer ses effets. Les valeurs que le test change entre deux rendus (route, valeur de contexte) vivent dans un objet `vi.hoisted` du test. `hookHarness.ts` n'importe React qu'en `import type` : un import de valeur, depuis la fabrique qui construit le faux `react`, chargerait ce faux.
Sémantique simulée, chaque point prouvé par `src/test/hookHarness.test.ts` :
- emplacements par ordre d'appel, curseur remis à zéro à chaque `render()` ; un test peut prérégler l'état d'un `useState` ou d'un `useReducer` avant le premier rendu ;
- `useState` : initialiseur appelé une fois, setter stable, mises à jour fonctionnelles ; **aucun nouveau rendu automatique**, le test rend de nouveau quand il veut lire l'état ;
- `useReducer` : le reducer est appliqué, `dispatch` est stable, et chaque action est aussi enregistrée par l'espion `harness.dispatch` ;
- `useRef` : un objet par emplacement, gardé d'un rendu à l'autre ;
- `useMemo` / `useCallback` : recalculés seulement quand une dépendance change (`Object.is`) ;
- `useEffect` : capturé pendant le rendu, joué seulement par `commit()` s'il a une dépendance changée, nettoyages d'abord ; `replayEffects()` joue le double passage de StrictMode au montage, `unmount()` les nettoyages ;
- tout autre hook (`useContext`, `useLayoutEffect`, `useSyncExternalStore`, `useId`, `use`…) **lève** : un test rougit sur son assertion, jamais par « Invalid hook call ». Mocker le module qui l'appelle (`useOnboarding`, `useLocation`, `useTranslation`…).
Non simulé : le nouveau rendu après un setter ou un dispatch, les enfants (un composant dans l'arbre rendu reste un élément), le DOM, le concurrent, la boucle de mises à jour pendant le rendu (le `render()` suivant la lit).
Utilisateurs : `useOnboardingChecklist.wiring.test.ts`, `UsersSettingsPage.test.tsx`, `BudgetTable.wiring.test.ts`, `BudgetPage.wiring.test.ts`, `AppShell.slot.test.tsx`, `OnboardingStepCard.test.tsx`. Hors périmètre de #409, `useImportWizard.mount.test.ts` et `useImportWizard.executeImport.test.ts` gardent leurs faux non persistants.
## CI/CD
Quatre workflows Forgejo Actions dans `.forgejo/workflows/`. Le runner est à **capacité 1** : les jobs se suivent, ils ne tournent pas en parallèle.