Мокирование навигации по страницам и отправки форм в MSW 3
Тестируйте навигацию и отправку HTML-форм с MSW 3 и обработчиками http. Разберите HTML-ответы, formData и ключевые изменения при обновлении.
Mock Service Worker (MSW) 3.0.0 вышел 28 сентября 2026 года и получил поддержку перехвата навигации по страницам и отправки форм (#2721). Теперь клик по ссылке или отправка обычной HTML-формы может получить мокированный ответ от тех же http-обработчиков, которые вы уже пишете.
До этого релиза нативная отправка формы в Storybook или Playwright полностью проходила мимо MSW. Для такого сценария приходилось либо поднимать реальный бэкенд, либо пропускать тест. В обсуждении 2023 года мейнтейнер пояснял, что MSW намеренно пропускает навигационные запросы, чтобы мок по ошибке не подменил настоящую HTML-страницу.
В статье разберём, что именно это изменение позволяет тестировать, с какими несовместимыми изменениями v3 вы столкнётесь в первую очередь и какие паттерны обработчиков подходят для страниц и отправки форм. Патч-релиз 3.0.3 вышел 10 октября 2026 года. Об установке MSW и создании первых обработчиков читайте в статье о мокировании API-серверов с помощью Mock Service Worker.
Главное
- В MSW 3.0.0 (28 сентября 2026 года) появился перехват навигации по страницам и отправки форм, но отдельный API для этого не задокументирован, поэтому используются стандартные обработчики
http.getиhttp.post. - Обычная HTML-форма отправляет поля в URL-кодированном виде, а не в JSON, поэтому тело запроса нужно читать через
request.formData(), а неrequest.json(). - MSW 3 поддерживает только ESM и требует Node.js 22+ и TypeScript 5.9+.
- Опция
onUnhandledRequestпереименована вonUnhandledFrame, аrequest.headers.get('cookie')внутри обработчиков теперь возвращаетnull.
Что даёт мокирование навигации и форм в MSW 3?
С MSW 3 можно тестировать серверно-рендерящуюся форму или ссылку на другую страницу без запуска бэкенда. Обработчик возвращает HTML-документ следующей страницы, и браузер отрисовывает его так же, как настоящий ответ сервера.
До третьей версии MSW покрывал только запросы, которые ваш JavaScript отправлял через fetch или XHR. За бортом оставались форма регистрации с прогрессивным улучшением, поисковая форма с перезагрузкой страницы и ссылка «подробнее», ведущая на серверно-рендерящуюся страницу. Теперь каждый из этих сценариев можно описать обработчиком и проверить, что в итоге видит пользователь.
Больше всего выигрывает сценарий ошибок. Записи пользовательских сессий (session replay) с формами нередко показывают, как после нажатия кнопки отправки пользователь попадает на пустую страницу или «сырую» страницу ошибки. Мокированный POST-запрос, возвращающий HTML с ошибками валидации или ответ 500, позволяет воспроизвести этот экран в тесте ещё до релиза.
Переход на MSW 3: несовместимые изменения, с которыми вы столкнётесь первыми
Большинство тестовых наборов в первую очередь ломаются из-за минимальных требований к окружению и одной переименованной опции. Вот изменения из примечаний к релизу v3.0.0, которые стоит проверить, прежде чем переходить к новым возможностям:
- MSW теперь поддерживает только ESM (#2765).
- Минимальная поддерживаемая версия Node.js — v22 (#2729).
- Минимальная версия TypeScript — 5.9.
graphqlтеперь экспортируется какmsw/graphql(#2691), а GraphQL стал полноценной опциональной peer-зависимостью. GraphQL-обработчики теперь определяются черезgraphql.link(), посколькуgraphql.query()иgraphql.mutation()удалены из пространства имён.- Опция
onUnhandledRequestпереименована вonUnhandledFrame. worker.stop()возвращает Promise (#2724). Ожидать его черезawaitнужно только в критичных сценариях, когда ещё остаются незавершённые запросы.msw/nativeудалён в пользу@msw/react-native. Согласно issue на GitHub,@msw/react-nativeпока не опубликован в npm, а его исходный код всё ещё рассчитан на MSW 2, так что у проектов на React Native рабочего пути обновления нет, пока пакет не выйдет.
Всё остальное описано в примечаниях к релизу и руководстве по миграции с 2.x на 3.x. Примеры кода в этой статье используют новую точку входа msw/http. Согласно анонсу MSW 3.0, корневые экспорты msw сохранены для обратной совместимости, но в следующем мажорном релизе будет удалено всё, что можно импортировать из выделенной точки входа.
Как замокировать навигацию по странице с помощью http.get?
Чтобы замокировать навигацию, напишите обработчик http.get для пути страницы и верните Response с HTML-телом и типом содержимого text/html. В примечаниях к релизу v3 заявлен перехват навигации и форм, но ни они, ни руководство по миграции не описывают отдельного API или шага настройки. Поэтому в примерах ниже используются только стандартные обработчики.
Каждый обработчик запросов в MSW сочетает предикат, который фильтрует входящие запросы, и резолвер ответа, который обрабатывает прошедшие фильтр. В документации по перехвату запросов отмечается, что относительные предикаты вроде /signup разрешаются относительно location.href. Если резолвер возвращает экземпляр Response, именно он становится мокированным ответом, как описано в документации по обработке запросов. Поэтому здесь достаточно обычного new Response без каких-либо специальных хелперов для 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>`),
),
]
Поисковые формы с method="get" ведут себя иначе. Форма, отправленная с method="get", передаёт поля в строке запроса. Предикаты MSW не должны содержать параметров запроса, поэтому сопоставляйте только путь, а поля читайте из new URL(request.url).searchParams:
http.get('/search', ({ request }) => {
const q = new URL(request.url).searchParams.get('q') ?? ''
const safe = q.replace(/&/g, '&').replace(/</g, '<')
return html(`<h1>Results for ${safe}</h1>`)
})
Как замокировать отправку формы с помощью http.post?
Нативная отправка формы обрабатывается обработчиком http.post для URL из атрибута action формы. Резолвер получает request как стандартный Request из Fetch API вместе с params и cookies, и любой возвращённый им Response становится следующей страницей, которую покажет браузер.
Единственное отличие от знакомого вам паттерна MSW 2 — формат тела запроса. Когда форму отправляет ваш собственный JavaScript, он обычно передаёт 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 })
})
Обычная HTML-форма отправляет поля не в JSON, а как URL-кодированные данные формы, поскольку application/x-www-form-urlencoded — это кодировка форм по умолчанию. Вызов .json() на таком теле завершится ошибкой. Вместо этого используйте Request.formData(). В руководстве MSW по загрузке файлов показано применение formData() к перехваченным запросам.
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>')
}),
]
Предусмотрите отдельную ветку для каждого варианта ответа сервера, который нужно протестировать: успех, ошибка валидации и страница 500. Тогда ваши проверки будут покрывать именно тот документ, который видит пользователь.
Сценарии с формами часто зависят от сессионных cookie. В v3 request.headers.get('cookie') внутри обработчиков больше не возвращает мокированную строку cookie — во всех окружениях возвращается null, поэтому для любой логики, завязанной на cookie, используйте аргумент резолвера cookies.
Где работает перехват навигации и форм в MSW 3?
В примечаниях к релизу v3 не сказано, какие окружения поддерживают перехват навигации и форм, поэтому, прежде чем на него полагаться, сверьте PR #2721 и документацию со своей конфигурацией. Навигацию по страницам выполняет браузер, так что пробовать эти обработчики логичнее всего в браузерных тест-раннерах.
Это объясняется историей MSW. До третьей версии скрипт воркера пропускал запросы с режимом navigate, как пояснил мейнтейнер в Discussion #1578. Если ваш набор тестов на Vitest или Jest запускает компонентные тесты в Node, как в статье о мокировании API-вызовов в тестах Vue с Vitest, моки fetch продолжат работать как раньше. Но не рассчитывайте, что нативная отправка формы поведёт себя там так же. Кроме того, в релизе появился Vite-плагин msw/vite (#2781), который сам раздаёт скрипт воркера, так что копировать mockServiceWorker.js в публичную директорию больше не нужно.
Итоги
Для мокирования страниц и форм в MSW 3 не нужно осваивать новые понятия. Для страницы используется http.get, для отправки — http.post, для тела запроса — formData(), а для результата — Response с text/html. Обновляйтесь в таком порядке: перейдите на Node.js 22, TypeScript 5.9 и ESM, переименуйте onUnhandledRequest в onUnhandledFrame и переключите импорты на msw/http. После этого добавьте обработчик для каждого исхода вашей самой важной формы, начав со страницы ошибки.
Часто задаваемые вопросы
Работает ли request.formData() для форм с enctype multipart/form-data?
Да. Request.formData() разбирает тела как application/x-www-form-urlencoded, так и multipart/form-data в объект FormData, поэтому один и тот же код обработчика работает независимо от того, есть ли в форме поле для загрузки файла. Текстовые поля возвращаются из form.get() в виде строк, а поля файлов — в виде объектов File, у которых можно проверить name, size и type.
Как прочитать группы чекбоксов или поля множественного выбора в обработчике формы?
Используйте form.getAll('name') вместо form.get('name'). Группа чекбоксов или select с атрибутом multiple отправляет по одной записи на каждое выбранное значение под одним и тем же именем поля, а get() возвращает только первую. getAll() возвращает массив всех значений или пустой массив, если ничего не выбрано, поскольку браузер вообще не отправляет неотмеченные чекбоксы.
Чем возврат passthrough() отличается от того, что резолвер MSW ничего не возвращает?
Возврат passthrough(), который в MSW 3 импортируется из msw/utils/passthrough, отправляет запрос в реальную сеть без изменений и помечает его как обработанный, так что последующие обработчики на него уже не влияют. Если ничего не возвращать, запрос остаётся нетронутым, и MSW пробует следующий подходящий обработчик. Используйте passthrough(), когда нужно намеренно направить конкретную страницу или отправку формы на реальный сервер.
Нужно ли заново генерировать mockServiceWorker.js после перехода на MSW 3?
Да, если только вы не используете новый плагин msw/vite, который сам раздаёт скрипт воркера. В остальных случаях после установки MSW 3 выполните npx msw init с указанием вашей публичной директории и закоммитьте новый файл, чтобы скрипт воркера соответствовал пакету 3.x, а не старой копии из 2.x.
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