12k
All articles

Mocker les navigations de page et les formulaires avec MSW 3

Simulez les navigations et les formulaires natifs avec MSW 3 et ses gestionnaires http. Découvrez les réponses HTML, formData et les changements de migration.

OpenReplay Team
OpenReplay Team
Mocker les navigations de page et les formulaires avec MSW 3

Mock Service Worker (MSW) 3.0.0 est sorti le 28 septembre 2026 et ajoute la prise en charge de l’interception des navigations de page et des soumissions de formulaires (#2721). Un clic sur un lien ou l’envoi d’un simple formulaire HTML peut désormais recevoir une réponse mockée avec les mêmes handlers http que vous écrivez déjà.

Jusqu’à cette version, la soumission d’un formulaire natif dans un environnement Storybook ou Playwright échappait totalement à MSW. Il fallait soit faire tourner un vrai backend pour ce seul parcours, soit renoncer au test. Dans une discussion de 2023, le mainteneur expliquait que MSW ignorait volontairement les requêtes de navigation, afin d’éviter qu’un mock soit servi par erreur à la place d’une véritable page HTML.

Cet article présente ce que ce changement rend testable, les breaking changes de la v3 que vous rencontrerez en premier, ainsi que des patterns de handlers pour les pages et les envois de formulaires. La version corrective 3.0.3 a suivi le 10 octobre 2026. Pour installer MSW et créer vos premiers handlers, consultez mocking API servers with Mock Service Worker.

Points clés à retenir

  • MSW 3.0.0 (28 septembre 2026) ajoute l’interception des navigations de page et des soumissions de formulaires. Aucune API dédiée n’est documentée, vous utilisez donc les handlers standard http.get et http.post.
  • Un simple formulaire HTML envoie ses champs encodés en URL, et non en JSON. Lisez donc le corps avec request.formData() plutôt qu’avec request.json().
  • MSW 3 est exclusivement ESM et nécessite Node.js 22 ou une version ultérieure, ainsi que TypeScript 5.9 ou une version ultérieure.
  • onUnhandledRequest est renommé onUnhandledFrame, et request.headers.get('cookie') renvoie désormais null dans les handlers.

Que permet le mocking des navigations et des formulaires avec MSW 3 ?

Avec MSW 3, vous pouvez tester un formulaire rendu côté serveur ou un lien vers une autre page sans faire tourner de backend. Le handler renvoie le document HTML de la page suivante, et le navigateur l’affiche comme il le ferait avec une véritable réponse serveur.

Avant la version 3, MSW ne couvrait que les requêtes émises par votre JavaScript via fetch ou XHR. Restaient donc hors de portée un formulaire d’inscription en amélioration progressive, un formulaire de recherche qui recharge la page ou encore un lien « voir le détail » vers une page rendue côté serveur. Vous pouvez désormais décrire chacun de ces cas sous forme de handler et faire vos assertions sur ce que l’utilisateur voit au final.

C’est le chemin d’erreur qui en profite le plus. Les replays de session des parcours de formulaire montrent souvent des utilisateurs qui atterrissent sur une page blanche ou une page d’erreur brute après avoir cliqué sur Envoyer. Un POST mocké qui renvoie le HTML d’une erreur de validation ou une erreur 500 vous permet de reproduire cet écran dans un test avant la mise en production.

Migrer vers MSW 3 : les breaking changes qui frappent en premier

La plupart des suites de tests cassent d’abord à cause des versions minimales des runtimes et d’une seule option renommée. Voici les changements des notes de version de la v3.0.0 à vérifier avant de vous intéresser aux nouvelles fonctionnalités :

  • MSW est désormais exclusivement ESM (#2765).
  • La version minimale de Node.js prise en charge est la v22 (#2729).
  • TypeScript 5.9 est désormais la version minimale.
  • graphql est désormais exporté sous la forme msw/graphql (#2691), et GraphQL devient une véritable peer dependency optionnelle. Les handlers GraphQL se définissent par ailleurs désormais sur un graphql.link(), car graphql.query() et graphql.mutation() ont été retirés du namespace.
  • L’option onUnhandledRequest a été renommée onUnhandledFrame.
  • worker.stop() renvoie une Promise (#2724). Vous n’avez besoin de l’attendre (await) que dans les scénarios critiques où des requêtes sont encore en cours.
  • msw/native est supprimé au profit de @msw/react-native. Une issue GitHub signale que @msw/react-native n’est pas encore publié sur npm et que son code source cible toujours MSW 2 : les projets React Native n’ont donc aucun chemin de migration fonctionnel tant que ce package n’est pas publié.

Les notes de version et le guide de migration 2.x → 3.x couvrent tout le reste. Les extraits de code de cet article utilisent le nouveau point d’entrée msw/http. D’après l’annonce de MSW 3.0, les exports à la racine de msw sont conservés pour la rétrocompatibilité, mais la prochaine version majeure supprimera tout ce qui peut être importé depuis un point d’entrée dédié.

Comment mocker une navigation de page avec http.get ?

Pour mocker une navigation de page, écrivez un handler http.get pour le chemin de la page et renvoyez une Response dotée d’un corps HTML et d’un content type text/html. Les notes de version de la v3 annoncent l’interception des navigations et des formulaires, mais ni elles ni le guide de migration ne documentent d’API ou d’étape de configuration dédiée. Les exemples ci-dessous s’appuient donc uniquement sur les handlers standard.

Chaque request handler MSW associe un prédicat, qui filtre les requêtes entrantes, à un response resolver, qui traite celles qui passent ce filtre. La documentation sur l’interception des requêtes précise que les prédicats relatifs tels que /signup sont résolus par rapport à location.href. Lorsqu’un resolver renvoie une instance de Response, c’est cette instance qui devient la réponse mockée, comme l’indique la documentation sur le traitement des requêtes. C’est pourquoi un simple new Response fonctionne ici sans helper spécifique au HTML.

import { http } from 'msw/http'

export const html = (body: string, status = 200) =>
  new Response(`<!doctype html><html><body>${body}</body></html>`, {
    status,
    headers: { 'Content-Type': 'text/html' },
  })

export const pageHandlers = [
  http.get('/signup', () =>
    html(`<form method="post" action="/signup">
            <input name="email" type="email">
            <button>Sign up</button>
          </form>`),
  ),
]

Les formulaires de recherche qui utilisent method="get" se comportent différemment. Un formulaire soumis avec method="get" place ses champs dans la query string. Les prédicats MSW ne doivent pas contenir de paramètres de requête : faites donc correspondre uniquement le chemin et lisez les champs depuis new URL(request.url).searchParams :

http.get('/search', ({ request }) => {
  const q = new URL(request.url).searchParams.get('q') ?? ''
  const safe = q.replace(/&/g, '&amp;').replace(/</g, '&lt;')
  return html(`<h1>Results for ${safe}</h1>`)
})

Comment mocker l’envoi d’un formulaire avec http.post ?

L’envoi d’un formulaire natif est traité par un handler http.post associé à l’URL de l’attribut action du formulaire. Le resolver reçoit request sous la forme d’une Request standard de la Fetch API, accompagnée de params et de cookies, et la Response qu’il renvoie devient la page que le navigateur affiche ensuite.

La seule différence avec le pattern MSW 2 que vous connaissez déjà réside dans le format du corps. Lorsque votre propre JavaScript soumet un formulaire, il envoie généralement du JSON :

import { http, HttpResponse } from 'msw/http'

http.post<never, { email: string }>('/api/signup', async ({ request }) => {
  const body = await request.clone().json()
  return HttpResponse.json({ ok: true, email: body.email }, { status: 201 })
})

Un simple formulaire HTML envoie ses champs sous forme de données de formulaire encodées en URL plutôt qu’en JSON, car application/x-www-form-urlencoded est l’encodage de formulaire par défaut. Appeler .json() sur ce corps échoue. Lisez-le plutôt avec Request.formData(). Le guide MSW sur l’upload de fichiers montre l’utilisation de formData() sur des requêtes interceptées.

import { http } from 'msw/http'
import { html } from './pages'

export const formHandlers = [
  http.post('/signup', async ({ request }) => {
    const form = await request.clone().formData()
    const email = form.get('email')

    if (!email) {
      return html('<p role="alert">Email is required.</p>', 422)
    }

    return html('<h1>Check your inbox</h1>')
  }),
]

Prévoyez une branche distincte pour chaque issue côté serveur que vous devez tester : succès, échec de validation et page d’erreur 500. Vos assertions portent alors sur le document que l’utilisateur voit réellement.

Les parcours de formulaire dépendent souvent de cookies de session. En v3, request.headers.get('cookie') ne renvoie plus de chaîne de cookies mockée dans les handlers. Cet appel renvoie null dans tous les environnements : utilisez donc l’argument cookies du resolver pour toute logique basée sur les cookies.

Où fonctionne l’interception des navigations et des formulaires de MSW 3 ?

Les notes de version de la v3 ne précisent pas quels environnements prennent en charge l’interception des navigations et des formulaires. Vérifiez donc la PR #2721 et la documentation au regard de votre propre configuration avant de vous y fier. Une navigation de page est une opération effectuée par le navigateur, ce qui fait des runners basés sur un navigateur l’endroit naturel pour essayer ces handlers.

L’historique de MSW explique pourquoi. Avant la version 3, le script du worker ignorait les requêtes dont le mode était navigate, comme l’expliquait le mainteneur dans la discussion #1578. Si votre suite Vitest ou Jest exécute des tests de composants dans Node, comme dans mocking API calls in Vue tests with Vitest, les mocks fetch continuent de fonctionner comme avant. Ne partez pas du principe qu’une soumission de formulaire natif s’y comporte de la même manière. Cette version ajoute également un plugin Vite, msw/vite (#2781), qui sert le script du worker à votre place : vous n’avez donc plus à copier mockServiceWorker.js dans votre répertoire public.

Conclusion

MSW 3 n’exige aucun nouveau vocabulaire pour mocker les pages et les formulaires. Vous utilisez http.get pour la page, http.post pour la soumission, formData() pour le corps et une Response en text/html pour le résultat. Effectuez la migration dans cet ordre : passez à Node.js 22, TypeScript 5.9 et ESM, renommez onUnhandledRequest en onUnhandledFrame, puis basculez vos imports vers msw/http. Ajoutez ensuite un handler pour chaque issue de votre formulaire le plus important, en commençant par la page d’erreur.

FAQ

request.formData() fonctionne-t-il pour les formulaires qui utilisent enctype multipart/form-data ?

Oui. Request.formData() analyse aussi bien les corps application/x-www-form-urlencoded que multipart/form-data pour produire un objet FormData : le même code de handler fonctionne donc, que le formulaire contienne ou non un champ de fichier. Les champs texte sont renvoyés sous forme de chaînes par form.get(), et les champs de fichier sous forme d'objets File dont vous pouvez inspecter le nom, la taille et le type.

Comment lire des groupes de cases à cocher ou des champs à sélection multiple dans un handler de formulaire ?

Utilisez form.getAll('name') au lieu de form.get('name'). Un groupe de cases à cocher ou un select doté de l'attribut multiple envoie une entrée par valeur sélectionnée sous le même nom de champ, et get() ne renvoie que la première. getAll() renvoie un tableau contenant toutes les valeurs, ou un tableau vide si rien n'est sélectionné, car le navigateur n'envoie pas du tout les cases non cochées.

Quelle est la différence entre passthrough() et le fait de ne rien renvoyer depuis un resolver MSW ?

Renvoyer passthrough(), que MSW 3 importe depuis msw/utils/passthrough, transmet la requête au réseau réel sans la modifier et la marque comme traitée : aucun handler suivant ne peut alors l'affecter. Ne rien renvoyer laisse la requête intacte, et MSW essaie le handler correspondant suivant. Utilisez passthrough() lorsque vous voulez délibérément qu'une page ou une soumission de formulaire précise atteigne un vrai serveur.

Faut-il régénérer mockServiceWorker.js après la migration vers MSW 3 ?

Oui, sauf si vous utilisez le nouveau plugin msw/vite, qui sert le script du worker à votre place. Sinon, après avoir installé MSW 3, exécutez npx msw init suivi de votre répertoire public, puis committez le nouveau fichier, afin que le script du worker corresponde au package 3.x et non plus à l'ancienne copie 2.x.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.