Fondations

21 — Tests et Storybook

Statut : fait · Phase : transverse · Mis à jour : 30/10/2026.

Décidé le 29/10/2026. Chaque composant et chaque runtime de Récit reçoit une story qui le spécifie état par état, une fonction play qui prouve son comportement, un contrôle d'accessibilité et une capture en clair et en sombre. Storybook devient la spécification exécutable de l'interface : ce que la migration de style (19-style.md) transforme, ces tests le vérifient ; ce qui vient après s'écrit d'abord dans une story. Le plan inventorie l'existant (§1), fixe la pyramide et l'outil (§2), structure Storybook (§3), dresse le catalogue des stories (§4), les conventions (§5), la mesure (§6), le câblage CI (§7), puis les lots et leur imbrication dans les vagues du plan 19 telles que 20-multi-agents.md §4 les redécoupe (§8). Ce plan ne s'implémente pas avant la demande de l'utilisateur.

0. Décision, portée et lecture de la demande

0.1 La demande, telle qu'elle est lue

  • « Élaborer des tests » : des tests fonctionnels, qui prouvent ce que fait un composant (un clic ouvre, une touche navigue, un état s'affiche), et pas seulement qu'il se rend sans exception.
  • « Structurer Storybook » : une arborescence par surface, des décorateurs communs, une story par composant avec ses états, et Storybook comme point d'entrée des tests de composants, pas comme une vitrine à part.
  • « Le plus de tests fonctionnels sur le sujet » : le sujet est l'interface de Récit telle que le plan 19 la réécrit — les primitives ui/ du framework, ses composants et sa navigation, les composants de diapositive, le runtime du deck, les blocs motion, site-kit, les composants des sites et des exemples. Le code non visuel (compilation, config, CLI) garde ses tests Vitest, ce plan ne les refait pas.

0.2 Objectifs

  • Une story par composant exporté, avec un export par état (défaut, variantes, vide, erreur, chargement, long contenu), les args typés, l'autodoc.
  • Une fonction play par comportement : ouverture et fermeture, clavier, focus, sélection, copie, navigation ; écrite contre les rôles et les noms accessibles, jamais contre une classe.
  • L'accessibilité par story (addon-a11y en mode test, déjà a11y.test: "error"), et le test axe des pages e2e conservé.
  • Le visuel par story : une capture en clair et en sombre de chaque story, dans le conteneur Linux, par la spec e2e/storybook.spec.ts que STY1 a posée ; les stories sont la galerie avant/après du plan 19 au grain du composant.
  • Un seul outil de plus : l'addon Vitest de Storybook, qui exécute les stories dans Chromium par Playwright, déjà présent. Pas de Jest, pas de second runner.
  • Une mesure : un compteur des composants sans story et sans play, en avertissement puis en erreur, comme le compteur de style du plan 19 ; un cliquet de couverture Vitest par paquet.

0.3 Non-objectifs

  • Pas de refonte des tests unitaires existants (1 591 tests, 154 fichiers) : ils restent, on n'ajoute que ce qui manque.
  • Pas de tests de rendu Remotion image par image hors des goldens existants (16 clichés).
  • Pas de tests de charge, pas de tests de contrat réseau : Récit n'a pas de serveur.
  • Pas de couverture à 100 % comme objectif chiffré : la cible est « chaque composant a sa story, chaque comportement son play », mesurée par le compteur de §6, pas par un pourcentage de lignes.

0.4 La pyramide cible

NiveauQuoiOutilOù ça tourne
Unitairelogique pure : parseurs, config, navigation du deck, horloge motion, scriptsVitest (node, happy-dom au besoin)pnpm test
Composantchaque story se rend, ses args tiennent, aucune erreur consoleStorybook + addon Vitest, navigateur Chromiumpnpm test (projet storybook)
Interactionfonctions play : clavier, souris, focus, étatsstorybook/test (userEvent, within, expect)idem
Accessibilitéaxe sur chaque story ; axe sur trois pages e2eaddon-a11y en mode test ; @axe-core/playwrightidem ; conteneur e2e
Visuelune capture par story, clair et sombre ; les pages de site ; les diapositivesPlaywright sur storybook-static et sur les sitesconteneur e2e (-linux.png)
Fluxce qu'une story ne peut pas prouver : recherche puis navigation, thème persistant, tiroir mobile, deckPlaywright, specs e2e/*.spec.ts sans captureconteneur e2e
Filmsun cliché par bloc, deux films, deux thèmesgoldens de @recit/motion-render (pixelmatch, 0,2 %)build.yaml « Motion goldens »
ExportPDF d'un deck : nombre de pages, dimensionsdeck-features.spec.ts (existant)conteneur e2e

1. Inventaire avant

1.1 L'outillage Vitest

  • Racine vitest.config.ts : trois globs de projets (packages/!(formats|integrations|tools), packages/{formats,integrations,tools}/*, scripts), couverture v8 avec quatre rapporteurs dans ./coverage, sans seuil. Scripts : pnpm test = vitest run --typecheck, test:coverage ; e2e est un projet nx à part (recit-e2e, scope:test).
  • Configs de projet : packages/core et site-stories (include des *.test-d.ts pour le typecheck), packages/recit, packages/tools/site-kit, packages/site (globals, setupFiles: vitest.setup.ts qui étend expect avec @testing-library/jest-dom et appelle cleanup() après chaque test, typecheck par tsgo sur tsconfig.app.json).
  • Environnements : node par défaut ; happy-dom 21.0.4 déclaré par fichier (@vitest-environment happy-dom) dans 21 fichiers de packages/site ; aucun jsdom.
  • Dépendances de test (catalogue) : @testing-library/dom 10.4.2, jest-dom 6.10.0, react 16.3.3, user-event 14.6.7, utilisées par packages/site seulement ; @vitest/coverage-v8 4.1.11 ; Vitest 4.1.11 ; @axe-core/playwright à la racine ; Playwright 1.64.1.
  • Volume (29/10) : 154 fichiers, 1 591 tests, en 2 min 30 environ dans pnpm verify.
ProjetFichiers de specCe qu'ils couvrent
core23parseur, schémas, config (app: inclus), conversion source, JSON Schema
site5624 .test.tsx de composants (ci-dessous), config, routes, SSR, plugins, hooks
motion16schéma, timeline, adaptateurs
site-stories11plugin, config de site, i18n, composants de récit, motion
deck10navigation, first-render (chaîne SSR), presentation-a11y (role="application"), mdx-slide, transitions, peer-messenger
app8config Tauri, coquille, serveur, Android, iOS, deck
motion-blocks6player, render, horloge, thèmes
cli, mcp5 + 5commandes, bundling, serveur MCP
components4code-block, mermaid-render, presets, slides (rendu HTML des composants de diapositive)
site-kit4config, énumérations, sites, workspace ; aucun composant
scripts6frontières, version plans, e2e-linux, paquets publiés, workflows, galerie
theme, recit1 + 1thèmes, point d'entrée
motion-render0les goldens (§1.3) tiennent lieu de test

1.2 Les tests de composants existants

Vingt-quatre .test.tsx dans packages/site/src : app/entry.server, processRoutes, createRedirectRoutes, lib/components/BrowserWindow, Footer, Header, LandingPage, Main, Markdown, MobileTopNavigation, NotFoundPage, PluginHeads, ScrollableTable, Search, Slot, TopNavigation, navigation/Navigation, core/transform-config, hooks/useEvent, plugins/markdown/MdxPage, search-inkeep, search-pagefind, ui/CodeBlock, ui/Stepper. Chacun monte son harnais à la main : MemoryRouter ou createMemoryRouter + RouterProvider, QueryClientProvider avec un QueryClient neuf, le contexte de config moqué (Header.test.tsx:30-45, Footer.test.tsx:15-42). Aucun helper de rendu partagé, aucune story réutilisée. Dans deck, first-render.spec.tsx et presentation-a11y.spec.tsx rendent par renderToString et lisent le HTML ; components/src/__tests__/slides.spec.ts vérifie le HTML des composants de diapositive. Rien ne teste un comportement au clavier ou à la souris hors e2e.

1.3 Les goldens motion

packages/formats/motion-render/scripts/goldens.mjs : un cliché par bloc au milieu de chaque scène, pour deux films de démonstration (warm-editorial 16:9, saas-product 9:16), rendus depuis le film.json et depuis le jumeau Markdown ; 16 PNG dans fixtures/goldens, diff pixelmatch à 0,2 % de pixels, les diffs écrits dans fixtures/goldens-diff ; étape « Motion goldens » de build.yaml, diffs envoyés en artefact. C'est le seul test visuel hors Playwright.

1.4 La suite e2e après STY1

  • 15 specs dans e2e/, 443 références -linux.png (213 avant STY1), trois projets Playwright : chromium (1280×720), chromium-dark (tous les site-*.spec.ts, colorScheme: "dark" et le choix mémorisé theme/recit-theme, suffixe -dark), mobile (390×844, tests @mobile de site-docs, suffixe -mobile) ; maxDiffPixelRatio 0,02, animations désactivées ; rapporteur json dans test-results/linux/report.json.
  • storybook.spec.ts : les 16 stories de index.json, en clair et en sombre par iframe.html?id=…&globals=theme:…, servies depuis sites/storybook/storybook-static sur le port 4380 ; 32 références ; la liste des ids est codée en dur dans la spec (STORIES), à maintenir à chaque story.
  • accessibility.spec.ts : axe WCAG 2 A/AA et 2.1 A/AA sur trois URL.
  • Un passage complet en conteneur : 7 à 8 min (copie 1 s, installation 25 à 45 s, construction 2 à 2 min 30, tests 4 min 20 à 5 min 20 pour 444 tests) ; galerie node scripts/screenshot-gallery.js --serve <port> ; règle du skill e2e-baselines : deux passages, régénération par test:e2e:update:linux <spec> seulement.

1.5 Storybook

  • sites/storybook (scope:site, privé) : Storybook 10.6.3 (10.6.5 publié), react-vite, addons docs, a11y (a11y.test: "error"), themes (withThemeByClassName, light: "", dark: "dark"), typescript.reactDocgen: "react-docgen", tags: ["autodocs"], controls.expanded. Scripts dev (port 6006, --no-open), build (storybook-static), typecheck. Construit par pnpm build (nx) : la spec e2e le sert.
  • Stories : 9 fichiers, 16 stories, toutes dans packages/components/src/slides/ (Callout, CodeBlock, CodeGroup, Cover, Grid, Media, Mermaid, Points, Quote), titres Slides/<Composant>, satisfies Meta<typeof X>, args et argTypes renseignés (Callout.stories.tsx:5-16). Le glob de main.ts ne lit que packages/components.
  • Décorateurs : withSlide (cadre 16:9, SlideMotion, parameters.slide.fit) avec deux style inline et deux classes (preview.tsx:14-32, le plan 19 STY6.1 le réécrit en module) ; withThemeByClassName. preview.css importe Tailwind, les polices, les tokens, le pont Tailwind, page.css, deck.css, et déclare @source sur packages/components/src.
  • Absents : addon Vitest, test-runner, fonctions play, storybook/test, composeStories, presets de viewport, décorateur de fournisseurs de site (routeur, query, config), stories hors packages/components.

1.6 Sans test et sans story

SurfaceComposantsTest aujourd'hui
packages/site/src/lib/ui54 composants (57 fichiers), dont 27 sans importeur que STY3 supprime ; 27 restent (§4.1)CodeBlock.test.tsx, Stepper.test.tsx
packages/site/src/lib/components (+ navigation)50 fichiers : Header, Footer, Banner, Search, Pagination, ThemeSwitch, Mermaid, Markdown, Typography, Heading, 12 de navigation…14 .test.tsx de rendu, aucun comportement
packages/site/src/lib/pluginsMdxPage, PagefindSearch, ResultList, IndexingDialog, search-inkeep3 .test.tsx
packages/formats/deck10 composants : Presentation, SlideStage, SlideFrame, PresenterView, MobileRemoteView, RemoteQrCodeModal, AnnotationCanvas, DevHud, HtmlSlide, SceneContent10 specs sans interaction, e2e deck-features
packages/formats/motion-blocks8 blocs (chapter, compareBars, list, stat, statement, title, uiMock, uiScene), playerplayer.test.tsx, render.test.tsx, goldens
packages/components9 avec story, 6 sans : Card, Columns/Col, SlideImage, Badge, Kbd, Step (mdx/builtins.tsx), SlideMotionslides.spec.ts (HTML)
packages/tools/site-kitCards, ExampleHeader, SourceFile, Tutorialaucun
Sites et exempleswww (Hero, FormatGrid, OneSource), docs/StatusBadge, motion/VideoDownloads, components/FormatSupport, deck-mdx (Counter, Timeline), deck-showcase/Metriccaptures de pages seulement

1.7 Le budget CI

build.yaml tourne sur ubuntu-latest dans l'image mcr.microsoft.com/playwright:v1.64.1-noble : pnpm check, pnpm test, publint et taille des bundles, goldens motion, pnpm test:e2e, rapport Playwright en artefact ; un second job vérifie les liens. Pas de durée mesurée dans le dépôt ; sur la machine de travail, pnpm verify prend 3 min 20 et un passage e2e 7 à 8 min : l'ordre de grandeur du job est 12 à 15 min. Chromium est dans l'image : les tests de stories en navigateur n'y ajoutent aucune installation.

2. Cible : la pyramide et l'outil

2.1 Les trois façons de tester une story, évaluées

OptionCe que ça donneDépendancesVerdict
@storybook/addon-vitest : chaque story devient un test Vitest, exécuté dans Chromium (mode navigateur de Vitest, fournisseur Playwright) ; play et a11y comprisrendu réel (mise en page, portails, focus), un seul runner avec les tests unitaires, projet Vitest à part, rapport unifié, storybook/test fourni par storybook@storybook/addon-vitest 10.6.x, @vitest/browser-playwright 4.1.x (Vitest 4.1 du dépôt) ; Chromium déjà làRetenu. Deux dépendances de dev, aucun runner nouveau, le navigateur est celui des e2e
@storybook/test-runner 0.26 : Jest + Playwright qui visite chaque story de storybook-staticrendu réel aussi, mais un second runner (Jest) et un second rapport, Storybook construit avant@storybook/test-runner, jest, jest-playwrightÉcarté : double la pile de test (règle « une seule stack »), plus lent (build + visite)
Stories portables : composeStories de @storybook/react-vite dans un test Vitest happy-domréutilise une story comme fixture d'un test unitaire, sans navigateuraucuneComplément, pas la base : happy-dom n'a ni mise en page ni focus fiable ; réservé à la logique qui a besoin d'une story comme entrée (deck, tableaux)

Décision : addon Vitest, les stories portables en complément. Le mode navigateur tourne headless ; en local il faut une fois pnpm exec playwright install chromium (déjà requis par pnpm test:e2e, documenté dans CONTRIBUTING par TST5).

2.2 Le projet Vitest storybook

  • sites/storybook/vitest.config.ts (nouveau) : defineProject avec le plugin storybookTest({ configDir: ".storybook" }), test.browser = { enabled: true, headless: true, provider: playwright(), instances: [{ browser: "chromium" }] }, setupFiles avec le vitest.setup.ts de Storybook (setProjectAnnotations des annotations de preview.tsx).
  • vitest.config.ts racine : sites/storybook ajouté aux projects ; pnpm test l'exécute avec les autres, pnpm test --project storybook l'isole. Les stories n'entrent pas dans la couverture v8 des paquets (fichiers *.stories.tsx exclus), mais le code des composants qu'elles rendent, si : c'est ce qui fait monter la couverture de packages/site sans écrire un test par composant.
  • Tags : parameters.a11y.test = "error" (déjà) fait échouer une story sur une violation axe ; une story qui n'a pas de sens en test porte tags: ["!test"] (jamais pour cacher un échec : la raison dans la story).
  • storybook/test : expect, within, userEvent, fn, waitFor ; fn() pour chaque rappel (onClick, onChange) afin de l'asserter.

2.3 Le visuel

e2e/storybook.spec.ts passe de la liste codée en dur à la lecture de storybook-static/index.json : chaque story reçoit ses deux captures sans toucher à la spec ; les stories qui se stabilisent tard (Mermaid attend mermaidReady, le Quote sa transition) portent un tag (wait-mermaid, wait-motion) que la spec lit dans index.json, qui n'expose pas les parameters. Les captures suivent la règle du skill e2e-baselines (conteneur, deux passages, galerie, régénération par spec). Une story qui change d'aspect pendant le plan 19 se régénère à la porte de la vague, après revue de galerie : c'est le « comparatif avant/après » au grain du composant que l'utilisateur demandait au plan 19.

2.4 Les flux

Les comportements qu'une story ne peut pas prouver seule (plusieurs pages, stockage, chargement d'index) gardent une spec Playwright sans capture, dans e2e/ : recherche ouverte au clavier, résultat choisi, page atteinte ; thème changé puis conservé au rechargement ; tiroir mobile ouvert, lien suivi, tiroir fermé ; sommaire qui suit le défilement ; deck : flèches, ?slide=N, console présentateur ouverte en seconde fenêtre, impression. Ce « filet comportemental » (TST-A) s'écrit avant que le plan 19 ne touche ces composants (vague 2 de 20-multi-agents.md §4), contre les rôles et les noms : il survit à la migration et la prouve.

3. La structure de Storybook

3.1 Arborescence des titres

TitreSourceDécorateurs
Site/UI/<Composant>packages/site/src/lib/ui/*.stories.tsxwithMantine, withTheme
Site/Components/<Composant>packages/site/src/lib/components/*.stories.tsxwithMantine, withTheme, withSite
Site/Navigation/<Composant>…/lib/components/navigation/*.stories.tsxidem
Site/Plugins/<Plugin>…/lib/plugins/**/*.stories.tsxidem
Slides/<Composant>packages/components/src/**/*.stories.tsx (existant)withSlide, withTheme
Deck/<Composant>packages/formats/deck/src/**/*.stories.tsxwithDeck (un Story compilé en fixture), withTheme
Motion/Blocks/<Bloc>packages/formats/motion-blocks/src/blocks/*.stories.tsxwithFilm (StaticAnim, thème motion)
Kit/<Composant>packages/tools/site-kit/src/components/*.stories.tsxwithMantine, withTheme, withSite
Sites/<site>/<Composant>sites/*/src/*.stories.tsx, examples/*/components/*.stories.tsxselon la surface

Les globs correspondants entrent dans main.ts dès TST0, même vides : un fichier de story posé plus tard apparaît sans configuration.

3.2 Décorateurs communs (sites/storybook/.storybook/decorators/)

  • withMantine : MantineProvider avec createRecitTheme (plan 19 STY2) et la feuille Mantine en couche ; avant STY2, un createTheme({}).
  • withTheme : le mode de couleur par globals.theme, withThemeByClassName jusqu'à STY9 puis withThemeByDataAttribute (data-mantine-color-scheme) ; la spec e2e lit le même globals.
  • withSite : ce que SiteRoot fournit, en fixtures — createMemoryRouter sur une route paramétrable (parameters.site.path), QueryClientProvider neuf, le contexte de config avec sites/storybook/src/harness/config.ts (un recit.config de fixture, Lorem Ipsum), les slots, MDXProvider ; les modules virtuels du framework (virtual:recit-config, virtual:recit-theme.css, navigation, routes) sont fournis par un plugin Vite de main.ts (viteFinal) qui les résout vers src/harness/virtual/* — c'est le point dur de TST0, prouvé par la story pilote Site/Components/Header.
  • withSlide : l'existant, réécrit en module par STY6.1 ; parameters.slide.fit.
  • withDeck : un Story compilé depuis une fixture Markdown (examples/deck-starter) et le DeckApp ou la Presentation montée à la diapositive parameters.deck.slide.
  • withFilm : AnimContext en StaticAnim, ThemeVars du thème motion choisi par globals.motionTheme (warm-editorial, saas-product), cadre 16:9 ou 9:16.
  • withReducedMotion : MotionConfig reducedMotion="always" et prefers-reduced-motion émulé pour les captures et les play (les transitions de motion/react ne sont pas ce qu'on teste).
  • Viewports : presets mobile 390×844, tablet 768×1024, desktop 1280×720, déclarés une fois ; une story mobile porte parameters.viewport.defaultViewport.

3.3 args, argTypes, autodoc

satisfies Meta<typeof X> et StoryObj<typeof meta> partout (le modèle est Callout.stories.tsx) ; args complets sur meta, chaque export ne pose que ce qui change ; argTypes avec control pour les énumérations et action pour les rappels (fn()), options depuis les constantes du code (CALLOUT_TYPES de @recit/core/browser, jamais recopiées) ; tags: ["autodocs"] global ; parameters.docs.description en une phrase quand le nom ne suffit pas. Contenu Lorem Ipsum, jamais de nom réel (règle des exemples).

3.4 Storybook et le paquet privé @recit/site

sites/storybook porte scope:site et peut dépendre de tout (scripts/check-package-boundaries.js, ALLOWED.site) ; l'interdiction d'importer @recit/site ne vaut que sous packages/** (biome.jsonc, noRestrictedImports). Les stories du framework vivent à côté des composants dans packages/site/src avec des imports relatifs, et sites/storybook ajoute @recit/site en devDependencies pour que le typecheck et le glob les voient ; nx reçoit l'arête (règle ST-20). De même pour chaque site et chaque exemple dont Storybook lit une story (sites/www, examples/deck-mdx…) : une devDependency par source de stories, sinon un build affecté peut sauter Storybook.

4. Le catalogue des stories

Chaque ligne : les états (un export par état), les interactions (play), l'accessibilité (ce qu'axe doit voir), le visuel (ce qui mérite une capture au-delà du défaut). Les composants que STY3 supprime (27 ui/ sans importeur) n'y sont pas.

4.1 Site/UI — les 27 primitives conservées (TST2)

ComposantÉtatsInteractions (play)A11yVisuel
Button, ButtonGroupvariantes, tailles, icône, désactivé, chargement, asChild/lienclic (fn), Entrée et Espace, désactivé ne déclenche pas, focus visiblenom accessible, aria-disabledfocus ring, groupe
Badge, Kbdvariantes—contrastedéfaut
Togglepressé / non, désactivéclic bascule aria-pressed, Espacearia-pressedles deux états
Input, InputGroup, Textarea, Labelvide, valeur, invalide, désactivé, avec addonsaisie (userEvent.type), onChange appelé, label clique le champassociation label/champ, aria-invalidinvalide
Separatorhorizontal, vertical, décoratif—role="separator" ou none—
Carden-tête, contenu, pied, cliquable—titredéfaut
Alert, Calloutchaque type/couleur, avec et sans titre, long contenu—role="alert" ou notetous les types
Dialog, Drawerfermé, ouvert, avec formulaire, non fermable par clic extérieurouvrir, Échap ferme, clic extérieur, focus piégé puis rendu au déclencheuraria-modal, titre, focusouvert (clair et sombre)
Popover, Tooltipfermé, ouvert, positionssurvol et focus ouvrent le tooltip, clic ouvre le popover, Échap fermearia-describedby, role="tooltip"ouvert
DropdownMenuitems, séparateurs, raccourcis, sous-menu, désactivéouvrir au clic, flèches, Entrée sélectionne (fn), Échap fermerole="menu", menuitemouvert
Command (→ Spotlight)vide, résultats, aucun résultat, groupessaisie filtre, flèches, Entrée choisit, Échap ferme, Ctrl+Krole="dialog", listbox/optionouvert avec résultats
Collapsiblefermé, ouvertclic bascule, aria-expandedaria-expanded, aria-controlsles deux
NavigationMenuitems, actif, déroulantsurvol et clavierrole="navigation"déroulé
CodeBlock, EmbeddedCodeBlocklangage, titre, lignes surlignées, long, bouton copiercopier écrit dans le presse-papiers (navigator.clipboard moqué), retour visuelbouton nommésurlignage, sombre
CodeTabs, CodeTabPanel2 et 5 onglets, onglet mémoriséclic et flèches changent d'onglet, aria-selectedrole="tablist", tab, tabpanelonglet actif
SyntaxHighlightchaque langue configurée, thème clair et sombre—contraste des tokensles deux thèmes
Stepper1, 3 et 10 étapes, étape courante—liste ordonnéedéfaut

4.2 Site/Components, Navigation, Plugins (TST3)

ComposantÉtatsInteractionsA11y
Header, TopNavigation, HeaderNavigationlogo, liens, actif, avec et sans recherche, bannière, mode sombrelien actif marqué, ThemeSwitch bascule le mode (globals non requis), recherche ouvrebanner, navigation nommée
MobileTopNavigation, MobileTopNavigationDrawerfermé, ouvert, arborescence profondebouton ouvre, lien ferme, Échap ferme, focus piégéaria-expanded, dialog
Footer, Banner1 à 3 colonnes, logo, variables, bannière fermablefermer la bannière, mémorisationcontentinfo
Search, PagefindSearch, ResultList, IndexingDialogfermé, ouvert, vide, résultats (index Pagefind de fixture), indexation en coursCtrl+K, saisie, flèches, Entrée navigue (fn sur le routeur), Échapcombobox, listbox
Sidebar, NavigationItem, SidebarToggle, Tocplié, déplié, item actif, catégories imbriquées, sommaire à 3 niveauxplier/déplier, clavier, le Toc suit IntersectionObserver (moqué)tree ou list, aria-current
Pagination, Anchor, CategoryHeadingpremière, milieu, dernière page ; ancre survoléelien d'ancre copie l'URLnav nommée
PageProgress, Spinner0, 50, 100 %—progressbar
BrowserWindow, Framedavec URL, sans, contenu long——
ScrollableTable, InlineCode, Markdown, Mermaid, LanguageIcontableau large, code inline, Markdown riche, diagramme, erreur Mermaiddéfilement horizontal au claviertable avec en-têtes
NotFoundPage, ErrorPage, DeveloperHint404, erreur avec et sans détail—h1
MdxPagepage avec sommaire, sans, avec métadonnées, boutons « copier en Markdown »menu « ouvrir dans ChatGPT/Claude » (fn), copiemain, article
Typography, Heading, MdxComponents (TST5, après STY5)chaque niveau, prose complète (titres, listes, tableaux, code, citations)ancres des titreshiérarchie des titres

4.3 Slides (TST1), Deck et Motion (TST4a), Kit et Sites (TST4b)

SurfaceComposantÉtatsInteractions
Slidesles 9 existants, réécrits ; Card, Columns/Col, SlideImage, Badge, Kbd, Step, SlideMotionchaque variante, fit, contenu long, image manquanteCodeGroup : onglets ; CodeBlock : copier ; Mermaid : rendu puis erreur
DeckPresentation (dans withDeck)diapositive 1, milieu, dernière, mode ?print=1, notesflèches, Espace, Home/End, Maj+D et c (stylo, effacer), s ouvre la console (ouverture moquée), ?slide=N
DeckSlideStage, SlideFrame, HtmlSlide, SceneContent16:9 et 4:3, à l'échelle dans trois tailles de cadre, fond, progressionredimensionnement du cadre (ResizeObserver moqué) recalcule l'échelle
DeckPresenterViewdiapositive courante et suivante, notes, minuteur à 0 et après 10 mintouches de navigation, minuteur démarre et se remet à zéro
DeckMobileRemoteView, RemoteQrCodeModal, AnnotationCanvas, DevHudconnecté, déconnecté, QR affiché, tracé, HUD avec diagnosticsboutons précédent/suivant (fn), tracé par pointeur, effacer
Motionles 8 blocs dans withFilmchaque thème, 16:9 et 9:16, contenu minimal et maximal— (état final StaticAnim ; le mouvement reste aux goldens)
Motionplayeren pause, en lecture, finlecture/pause, barre de progression
KitCards, ExampleHeader, SourceFile, Tutorial1 à 6 cartes, en-tête avec et sans badge, fichier long, tutoriel à 5 étapes—
Siteswww/Hero, FormatGrid, OneSource, docs/StatusBadge, motion/VideoDownloads, components/FormatSupport, deck-mdx/Counter, Timeline, deck-showcase/Metricdéfaut, variantesCounter : incrémente ; Timeline : étape active

5. Conventions

5.1 Fichiers

  • X.stories.tsx à côté de X.tsx ; un fichier par composant, un export par état, les interactions dans la story qui les concerne.
  • X.test.tsx seulement pour ce qu'une story ne couvre pas (logique sans DOM, SSR par renderToString) ; un test qui a besoin d'un composant monté réutilise la story (composeStories) au lieu de remonter un harnais.
  • Les 24 .test.tsx de packages/site qui ne font que rendre migrent vers des stories (TST3) ; ceux qui testent la logique (transform-config, useEvent, processRoutes, SSR) restent.
  • Pas de dossier __stories__ : la story suit le composant, comme le module CSS du plan 19.

5.2 Ce qu'une story ou un test affirme

  • Des rôles et des noms (getByRole("button", { name: /copy/i })), jamais une classe, jamais un sélecteur CSS, jamais un texte de classe de module.
  • Aucun snapshot de DOM (toMatchSnapshot sur du HTML interdit) : un snapshot ne dit pas ce qui compte ; le visuel est la capture, le comportement est le play.
  • data-testid interdit sauf sur un élément sans sémantique et sans texte (un cadre de diapositive, un canevas), listé dans scripts/check-stories.js ; un data-testid ajouté sans cette entrée fait échouer le compteur.
  • Une classe de module s'affirme en important classes dans le test et en vérifiant toHaveClass(classes.open) : le nom haché ne s'écrit jamais en dur ; cela ne sert qu'aux états sans rôle (open/closed d'un conteneur), et le play préfère aria-expanded.
  • Les rappels sont des fn() d'args et s'affirment par toHaveBeenCalledWith.
  • Le presse-papiers, IntersectionObserver, ResizeObserver, matchMedia, localStorage et BroadcastChannel sont moqués dans vitest.setup.ts de Storybook, une seule fois.
  • Aucune erreur console pendant une story : le harnais échoue sur console.error (le React #418 intermittent connu s'observe en e2e, pas ici : le rendu Storybook n'hydrate pas).

5.3 La loi du plan 19 s'applique aux stories

Une story est un .tsx de packages/ ou de sites/storybook : le compteur scripts/check-style-rule.js la lit. Pas de style={{ display: "grid" }} pour disposer les variantes (Callout.stories.tsx:26 aujourd'hui) : un décorateur withGrid adossé à decorators/grid.module.css, ou parameters.layout. Les décorateurs sont des composants comme les autres : un module chacun.

5.4 Nommage et langue

Titres de stories en anglais (Site/UI/Button, exports Default, Disabled, Loading, WithIcon), texte des stories en Lorem Ipsum, commentaires en anglais comme le code. Les play s'appellent par le comportement (opens on click and closes on Escape), pas par l'implémentation.

6. Mesure

6.1 scripts/check-stories.js (TST0, en avertissement ; TST5, en erreur)

Entré dans check:other. Pour chaque fichier de composant des surfaces de §3.1 (un .tsx qui exporte un composant React, hors *.test.tsx, *.stories.tsx, index.ts, hooks, utilitaires), il signale : (1) pas de X.stories.tsx à côté ; (2) une story sans play pour un composant interactif (il reçoit onClick, onChange, onOpenChange, ou rend un button, input, a, dialog, menu) ; (3) data-testid hors liste ; (4) toMatchSnapshot sur du HTML ; (5) tags: ["!test"] sans commentaire la ligne au-dessus. Compte par surface, total, baseline scripts/stories-baseline.json qui refuse toute hausse ; --strict à TST5. Test scripts/__tests__/check-stories.spec.ts.

6.2 Le cliquet de couverture

test:coverage mesure déjà ; TST0 note la couverture par paquet dans le journal (lignes, branches) ; TST5 pose dans chaque vitest.config.ts de projet coverage.thresholds au niveau mesuré (arrondi à l'unité inférieure), avec autoUpdate: true : la couverture ne peut que monter. Pas d'objectif chiffré : le compteur de §6.1 dit ce qui manque, la couverture dit que rien ne régresse.

6.3 Ce qui est compté comme « fait »

IndicateurAvant (29/10)Après TST5 (cible)
Fichiers de story9environ 110 (27 ui, 45 framework, 16 slides, 10 deck, 9 motion, 4 kit, 9 sites)
Stories (exports)16400 à 500
Stories avec play0une par composant interactif (environ 60)
Captures Storybook -linux.png32800 à 1 000 (clair et sombre)
Specs e2e de flux sans capture2 (deck-features, accessibility)4 (+ site-behaviour, deck-behaviour)
Composants sans story (compteur)environ 1200

7. Câblage CI et durées

ÉtapeCommandeOùDurée estimée
Unitaires et storiespnpm test (projets Vitest, storybook inclus)pnpm verify, build.yaml « test »+ 1 à 2 min pour 400 stories dans Chromium
Compteur de storiesnode scripts/check-stories.jspnpm checksecondes
Build Storybooknx run recit-storybook:buildpnpm build (déjà)environ 1 min
Captures Storybookplaywright test e2e/storybook.spec.tsconteneur e2e (-linux.png)+ 4 à 8 min à 1 000 captures ; par surface en vague
Fluxplaywright test e2e/*-behaviour.spec.tsconteneur e2e+ 1 min
Goldensinchangébuild.yamlinchangé

pnpm verify gagne 1 à 2 min ; le job build.yaml passe de 12–15 à 18–25 min. Si les captures Storybook dépassent 5 min, la spec se découpe par surface (storybook-site, storybook-slides, …) et les vagues ne rejouent que la leur, comme 20-multi-agents.md §3.2 le prévoit pour les sites.

8. Lots

Chaque lot nomme ses fichiers possédés, ce dont il dépend, ce qu'il fournit, sa vérification et sa vague dans le découpage de 20-multi-agents.md §4 (les lots TST s'intercalent dans les vagues STY : un lot TST n'entre dans une vague que si tous ses fichiers sont libres de tout lot STY de cette vague). Tailles S, M, L. Règles de 20-multi-agents.md §3 : un lot, un agent, ses fichiers ; relire avant d'écrire ; jamais de git ; note ### TSTn — <titre> — <date> en fin de journal.md ; BROWSER=none pour tout serveur local.

TST-A — Le filet comportemental e2e (M), vague 2

Dépend de : rien (les sites sont construits à chaque passage). Parallèle avec : STY2, STY6.1, STY7a. Agent frais.

Fichiers : e2e/site-behaviour.spec.ts (nouveau), e2e/deck-behaviour.spec.ts (nouveau), e2e/support.ts (helpers ajoutés seulement), la note de journal.

  • A.1 — Site (sites/docs en preview) : Ctrl+K ouvre la recherche, saisie, flèche, Entrée mène à la page ; le ThemeSwitch passe en sombre et le rechargement le garde ; le tiroir mobile (projet mobile) s'ouvre, un lien navigue, le tiroir est fermé ; le sommaire marque la section visible après défilement ; le bouton copier d'un bloc de code remplit le presse-papiers (permission accordée au contexte) ; les onglets de CodeTabs changent au clavier ; la pagination précédent/suivant.
  • A.2 — Deck (examples/deck-starter par recit-site preview d'un site qui le publie, ou le present/* de sites/presentation) : flèches, Espace, Home/End, ?slide=N, s ouvre la console présentateur (context.waitForEvent("page")), la console suit la diapositive, mode ?print=1 : autant de .print-slide que de diapositives.
  • A.3 — Aucune capture : assertions de rôle, d'URL, de localStorage et de presse-papiers ; deux passages en conteneur verts.

Fournit : la preuve comportementale qui traverse le plan 19 ; chaque vague STY rejoue ces deux specs à sa porte.

Vérification : pnpm test:e2e:linux e2e/site-behaviour.spec.ts e2e/deck-behaviour.spec.ts deux fois ; pnpm -F recit-e2e typecheck ; pnpm check.

TST0 — Le socle Storybook test (M), vague 3

Dépend de : STY6.1 (qui réécrit .storybook/* sans Tailwind et pose le module de withSlide), STY0a (Mantine dans le framework). Parallèle avec : STY3a-c, STY6.2, STY7b — STY6.2 possède packages/components/src/**, TST0 n'y touche pas. Agent frais avec un brief long : la vague 3 a déjà ses trois agents lourds (STY3a-c), et §2, §3 et §1 de ce plan suffisent à décrire le harnais.

Fichiers : sites/storybook/.storybook/main.ts (addon, globs de §3.1, plugin des modules virtuels), preview.tsx (décorateurs de §3.2 importés), .storybook/decorators/* (nouveau, un module CSS chacun), .storybook/vitest.setup.ts (nouveau), sites/storybook/vitest.config.ts (nouveau), sites/storybook/package.json (addon, @recit/site en dev, test script), pnpm-workspace.yaml (catalogue : @storybook/addon-vitest, @vitest/browser-playwright ; Storybook aligné sur 10.6.5 si l'addon l'exige), pnpm-lock.yaml, vitest.config.ts racine (projet), package.json racine (check:other), sites/storybook/src/harness/** (config de fixture, modules virtuels, index Pagefind de fixture), packages/site/src/lib/components/Header.stories.tsx (la story pilote du site, fichier nouveau que STY4a ne possède pas en vague 3), e2e/storybook.spec.ts (lecture de index.json), scripts/check-stories.js, scripts/stories-baseline.json, scripts/__tests__/check-stories.spec.ts, .github/workflows/build.yaml (rien si pnpm test suffit ; sinon une étape), la note de journal.

  • 0.1 — L'addon et le projet Vitest (§2.2) : pnpm test --project storybook exécute les 16 stories existantes dans Chromium, a11y en erreur.
  • 0.2 — Les décorateurs de §3.2 et les presets de viewport ; withMantine branché sur le thème de STY2.
  • 0.3 — Le harnais du site : withSite, le plugin des modules virtuels, la story pilote Site/Components/Header avec un play (le lien actif, l'ouverture de la recherche) qui passe dans Chromium ; c'est la preuve que les stories du framework sont possibles.
  • 0.4 — storybook.spec.ts lit index.json : les 16 + 1 stories ont leurs captures, baseline régénérée à la porte de la vague.
  • 0.5 — check-stories.js en avertissement, baseline écrite, test ; couverture par paquet notée au journal (§6.2).
  • 0.6 — Durée de pnpm test avant et après, notée.

Fournit : le runner, les décorateurs, le harnais, le compteur ; les lots TST1 à TST5 n'écrivent plus que des stories.

Vérification : pnpm test --project storybook ; pnpm test complet ; pnpm -F recit-storybook build && pnpm -F recit-storybook typecheck ; pnpm exec vitest run scripts/__tests__ ; pnpm check ; pnpm test:e2e:linux e2e/storybook.spec.ts à la porte.

TST1 — Les stories de @recit/components (M), vague 4

Dépend de : TST0, STY6.2. Parallèle avec : STY4a-b, STY6.3a-b, STY8, TST2a-b. Agent frais.

Fichiers : packages/components/src/**/*.stories.tsx (les 9 réécrits selon §3.3 et §5.3, les 6 nouveaux de §1.6), sites/storybook/.storybook/decorators/grid.module.css si withGrid n'existe pas encore (demander à TST0 sinon), la note de journal.

  • 1.1 — Chaque composant : ses états ; play pour CodeGroup (onglets), CodeBlock (copier), Mermaid (rendu, erreur), Step.
  • 1.2 — slides.spec.ts réduit à ce que les stories ne disent pas (HTML des presets) ; code-block.spec.tsx réutilise la story par composeStories.
  • 1.3 — Compteur à zéro sur packages/components.

Vérification : pnpm test --project storybook, pnpm exec vitest run packages/components, node scripts/check-stories.js packages/components, captures à la porte.

TST2 — Les stories de Site/UI (L, deux agents), vague 4

Dépend de : TST0, STY3a-c. Parallèle avec : STY4a-b (qui possèdent lib/components, pas ui/), TST1. Agents frais.

  • TST2a : Button, ButtonGroup, Badge, Kbd, Toggle, Input, InputGroup, Label, Textarea, Separator, Card, Alert, Callout, Stepper, CodeBlock, EmbeddedCodeBlock, CodeTabs, CodeTabPanel, SyntaxHighlight — fichiers packages/site/src/lib/ui/<X>.stories.tsx.

  • TST2b : Dialog, Drawer, Popover, Tooltip, DropdownMenu, Command/Spotlight, Collapsible, NavigationMenu — idem ; les surcouches sont les play les plus riches (§4.1).

  • 2.1 — États et play de §4.1 ; CodeBlock.test.tsx et Stepper.test.tsx migrés vers composeStories ou supprimés s'ils ne disent rien de plus.

  • 2.2 — Compteur à zéro sur packages/site/src/lib/ui.

Vérification : pnpm test --project storybook, pnpm exec vitest run packages/site, node scripts/check-stories.js packages/site/src/lib/ui, captures à la porte.

TST3 — Les stories du framework (L, deux agents), vague 5

Dépend de : TST0, STY4a-b. Parallèle avec : STY5a-b (qui possèdent Typography, Heading, MdxComponents, main.css : TST3 ne les touche pas), TST4a-b. Agents frais.

  • TST3a, pages et mise en page : Header, TopNavigation, HeaderNavigation, MobileTopNavigation(+Drawer), Footer, Banner, PageProgress, Spinner, BrowserWindow, Framed, Anchor, Pagination, Search, DeveloperHint, CategoryHeading, NotFoundPage, ErrorPage, LanguageIcon, ScrollableTable, InlineCode, Markdown, Mermaid, ThemeSwitch, Slot — packages/site/src/lib/components/<X>.stories.tsx ; les .test.tsx de rendu correspondants migrés ou supprimés (§5.1).
  • TST3b, navigation et plugins : lib/components/navigation/*.stories.tsx, lib/plugins/markdown/MdxPage.stories.tsx, search-pagefind/*.stories.tsx (index de fixture du harnais), search-inkeep (story !test : widget tiers, raison écrite).

Vérification : comme TST2 sur packages/site/src/lib/components et lib/plugins ; pnpm test complet ; captures à la porte.

TST4 — Deck, motion, kit, sites (M, deux agents), vague 5

Dépend de : TST0, STY6.3a-b, STY7b, STY8. Parallèle avec : STY5a-b, TST3a-b. Agents frais.

  • TST4a, deck et motion : packages/formats/deck/src/**/*.stories.tsx (withDeck), packages/formats/motion-blocks/src/blocks/*.stories.tsx et player.stories.tsx (withFilm), sites/storybook/.storybook/decorators/deck.tsx et film.tsx s'ils ne sont que des squelettes après TST0, la note de journal. first-render.spec.tsx et presentation-a11y.spec.tsx restent (SSR) ; player.test.tsx réutilise la story.
  • TST4b, kit, sites, exemples : packages/tools/site-kit/src/components/*.stories.tsx (les six composants des sites y ont été déplacés : un site, c'est du Markdown et de la config), examples/{deck-mdx,deck-showcase}/components/*.stories.tsx (les globs sont dans main.ts depuis TST0 ; les exemples sont des fixtures e2e : Lorem Ipsum, rien d'autre ne bouge).

Vérification : pnpm test --project storybook, pnpm exec vitest run packages/formats/deck packages/formats/motion-blocks packages/tools/site-kit, pnpm -F @recit/motion-render goldens:check (rien ne doit bouger), compteur sur ces surfaces, captures à la porte.

TST5 — Clôture (M), vague 6

Dépend de : STY5a (typographie), TST1 à TST4. Parallèle avec : STY9, STY10.2 (qui possède CONTRIBUTING.md, NOTICE.md, sites/docs/sidebar.ts : TST5 lui demande une ligne de sidebar et une phrase de CONTRIBUTING). Agent frais.

Fichiers : packages/site/src/lib/components/Typography.stories.tsx, Heading.stories.tsx, packages/site/src/lib/util/MdxComponents.stories.tsx, scripts/check-stories.js (mode erreur, baseline supprimée), les vitest.config.ts de projet (seuils de couverture, §6.2), sites/docs/pages/documentation/testing.mdx (nouveau : la pyramide, écrire une story, un play, lancer les tests, lire la galerie), .claude/skills/testing/SKILL.md (nouveau : la recette d'une story et d'un play, les moqueries disponibles, les interdits de §5.2, la liste de contrôle), la note de journal.

  • 5.1 — Les trois stories de typographie après STY5a ; compteur à zéro partout, en erreur.
  • 5.2 — Seuils de couverture posés au niveau mesuré, autoUpdate (dans le vitest.config.ts racine, un glob par paquet : Vitest ne lit les seuils que là).
  • 5.3 — Docs (documentation/reference/testing.mdx), skill ; AGENTS.md reçoit une ligne « Tests » dans la section « Styling » ou à côté (via STY10.2, même vague : demander).
  • 5.4 — Durées finales de pnpm test, du build Storybook et des captures notées ; découpage de storybook.spec.ts par surface si > 5 min (§7) : six specs storybook-<surface>.spec.ts.

Vérification : pnpm verify ; pnpm test:e2e:linux complet deux fois ; pnpm check en mode strict.

8.1 Les vagues, imbriquées dans 20-multi-agents.md §4

Vague (plan 20)Lots STY de la vagueLots TST ajoutésAgents en plusCondition d'entrée des TST
2STY2, STY6.1, STY7aTST-A1 fraisaucune : e2e sur les sites construits
3STY3a-c, STY6.2, STY7bTST01 fraisSTY6.1 et STY0a vérifiés
4STY4a-b, STY6.3a-b, STY8TST1, TST2a, TST2b3 fraisTST0 vérifié, STY3a-c et STY6.2 vérifiés
5STY5a-bTST3a, TST3b, TST4a, TST4b4 fraisSTY4a-b, STY6.3a-b, STY7b, STY8 vérifiés
5'STY5c——(TST3-4 peuvent déborder ici : aucun fichier commun avec STY5c)
6STY9, STY10.2TST51 fraisSTY5a, TST1 à TST4 vérifiés

Au plus six agents dans une vague (vague 5 : STY5a-b et TST3a-b, TST4a-b), ce qui dépasse le plafond de cinq de 20-multi-agents.md §4 : la vague 5 se joue en deux temps (TST3a-b avec STY5a-b, puis TST4a-b) si la limite d'usage le demande, sans changer les dépendances. Les portes restent celles du plan 20 §3.2 : e2e en conteneur sur un arbre stable, galerie, revue ; les captures Storybook s'y ajoutent. Neuf lots TST, douze agents, aucun nouveau jalon humain.

8.2 Version plan

Les stories et les specs ne changent aucun contrat publié ; sites/storybook est privé. Seuls packages/components/package.json (script test, devDependency Storybook déjà là) et les vitest.config.ts des paquets (seuils) bougent : pas de version plan pour ce plan, sauf si TST4a exporte une fixture de @recit/deck pour withDeck (alors prerelease, dans le plan du plan 19).

8.3 Effort

LotTailleStories (ordre de grandeur)play
TST-AM—12 flux e2e
TST0M1 (pilote)1
TST1M405
TST2a-bL12025
TST3a-bL16020
TST4a-bM + M9010
TST5M202

Environ 430 stories et 75 play, dix agents, répartis sur les vagues 2 à 6 du plan 19 : le plan 21 n'allonge pas le calendrier du plan 19, il remplit ses vagues.

9. Risques

RisqueParade
Le harnais du site (modules virtuels, config, routeur) est trop lourd à maintenirTST0 le prouve sur Header avant tout ; les fixtures sont un recit.config ordinaire ; repli : stories du framework limitées à ui/ et aux composants sans config
Mode navigateur de Vitest instable (portails, focus, timing des play)waitFor et findBy* plutôt que getBy* après une interaction ; withReducedMotion ; retry: 1 sur le projet storybook seulement
Durée de pnpm test et du job CImesure à TST0.6 et TST5.4 ; projet storybook isolable (--project) ; captures découpées par surface
Chromium absent sur un postemessage clair de Vitest ; pnpm exec playwright install chromium dans CONTRIBUTING ; le conteneur e2e l'a
Versions : addon 10.6.5 contre Storybook 10.6.3 du catalogue, @vitest/browser-playwright contre Vitest 4.1TST0 aligne le catalogue (Storybook 10.6.5) et vérifie la matrice de versions ; une incompatibilité arrête TST0 et se note au journal
Les stories deviennent la seule doc des composants et vieillissentle compteur, l'autodoc, la capture : une story fausse échoue
play écrits contre l'implémentation (classes, structure)§5.2 ; revue de diff ; le skill testing
Le React #418 intermittent brouille les portesil ne concerne que l'hydratation e2e ; les stories n'hydratent pas
Doublon entre une story et un .test.tsx de rendu§5.1 : le test de rendu migre ou meurt ; composeStories pour le reste

10. Questions ouvertes

  1. pnpm test inclut-il le projet storybook (proposé : oui, Chromium requis en local), ou un script test:stories séparé pour garder pnpm verify sans navigateur ?
  2. Les 24 .test.tsx de rendu de packages/site : migrer vers des stories (proposé) ou les garder en double le temps du plan 19 ?
  3. Storybook pour les sites (Sites/www/Hero…) : utile, ou leurs captures de page suffisent ? Proposé : oui pour les composants réutilisés par plusieurs sites (site-kit), au cas par cas pour les autres.
  4. Captures Storybook : une par story en clair et en sombre (proposé, 800 à 1 000 images), ou seulement les stories marquées parameters.screenshot ?
  5. Seuils de couverture : cliquet au niveau mesuré (proposé) ou un plancher commun (70 % de lignes) ?
  6. withDeck : exporter une fixture depuis @recit/deck (version plan) ou compiler la fixture dans sites/storybook avec @recit/core (proposé, aucun contrat publié ne bouge) ?
  7. Storybook publié : déployer storybook-static comme un site (storybook.recit.dev) avec les autres ? Hors de ce plan, à noter dans 07-domaines.