Перейти к содержимому
Поиск
Разработка

Vitest: тесты JavaScript и интерфейсов — мастерская и 9 практических задач

Девять задач на Vitest: расчёт, формы, API, таймеры, DOM и фокус. Живой компонент, реальные записи ошибок и исправлений, coverage и исходники Windows/Linux.

Александр Некрасов
Vitest — тесты JavaScript, исправления и проверки интерфейса

Vitest на практике: девять задач, живой компонент, реальные записи failing/passing, mocks, таймеры, DOM, Browser Mode, coverage и исходники Windows/Linux.

Vitest: поймать ошибку до того, как её найдёт пользователь

Форма принимает пустой адрес, поиск отправляет несколько запросов вместо одного, скидка ломает итог заказа, а раскрытая панель теряет фокус. Каждая такая ошибка выглядит небольшой, пока не попадает в рабочий интерфейс. Vitest позволяет записать ожидаемое поведение в виде проверок и повторять их после изменения кода. Вместо ручного перебора всех вариантов вы получаете конкретное сообщение: какое условие нарушено и чем фактический результат отличается от ожидаемого.

В этой статье девять практических задач: расчёт цены, таблица входных значений, асинхронная загрузка, подмена API, debounce, форма, раскрывающийся блок, snapshot с покрытием и запуск в CI. Для каждой важен пользовательский смысл. Мы проверяем, какую сумму увидит покупатель, уйдёт ли неправильная заявка и сможет ли человек продолжить работу с клавиатуры.

На странице можно переключать подготовленную ошибочную и исправленную реализации, пробовать небольшой живой компонент и читать записи настоящего тестового runner. Сам Vitest запускается из исходников в Node.js либо через настроенный Browser Mode. Статическая мастерская не выдаёт переключение сохранённого отчёта за новый запуск тестов на вашем устройстве.

Где тесты приносят практическую пользу

Первое полезное место — вычисление, влияющее на результат операции: цена, округление, комиссия, ограничение количества, преобразование выгрузки. Такой код обычно можно проверить без браузера, базы и внешних сервисов. Несколько понятных случаев показывают договорённости лучше, чем случайное число примеров в комментарии. После изменения формулы тест напомнит, что нулевое количество или отрицательная цена должны обрабатываться определённым образом.

Второе место — граница интерфейса и API. Проверка показывает, какой payload отправляется, как отображается отказ, что происходит после повторного нажатия и не возвращается ли старый ответ поверх нового. Здесь важны не только успешные запросы. В рабочем продукте отсутствие сети, неправильный ввод и отмена встречаются регулярно, поэтому у них должен быть явный сценарий.

Третье место — регрессия после исправления. Сначала воспроизведите замеченную ошибку небольшим падающим тестом, затем исправьте реализацию и убедитесь, что тот же тест проходит. Это особенно полезно в старом проекте, где нет возможности немедленно проверить всё. Начните с дорогостоящих для пользователя сбоев и постепенно расширяйте проверяемую область.

Что Vitest делает, а что остаётся за другими инструментами

Vitest находит тестовые файлы, выполняет проверки, управляет окружением и формирует отчёт. Он предоставляет функции сравнения, mocks, управление тестовым временем, snapshots и подключаемое покрытие кода. Его удобно использовать в проектах, которые уже собираются через Vite, но наличие Vue или React не является обязательным условием. Наши примеры используют обычные JavaScript-функции и HTML-компоненты.

Unit-тест проверяет небольшую операцию в ограниченном окружении. Интеграционный тест соединяет несколько настоящих частей, например функцию загрузки с локальным HTTP-сервером. Проверка интерфейса через jsdom работает с моделью DOM. Browser Mode выполняет тест в настоящем браузерном окружении с выбранным provider. Это разные уровни доказательства: успешный результат на одном уровне не подтверждает автоматически остальные.

Нагрузочное тестирование отвечает на другой вопрос: как сервис ведёт себя при потоке пользователей и запросов. Для него пригодится материал про k6. Полные пользовательские маршруты и автоматизация страниц связаны с браузерными инструментами, включая Puppeteer. Выбирайте уровень проверки по риску операции, а не по желанию использовать одну библиотеку для всего.

Версии и воспроизводимый набор зависимостей

Материал подготовлен для Vitest 5.1.0, jsdom 31.0.0 и Node.js 24 LTS от 24.15. Связанные пакеты Vitest для coverage и Browser Mode закреплены версией 5.1.0. Ограничение Node относится к этому стенду с jsdom, а не ко всем возможным конфигурациям Vitest. Точные зависимости и транзитивные версии находятся в package-lock.json. Чтобы повторить стенд, используйте npm ci: он восстанавливает зафиксированное дерево, вместо того чтобы незаметно выбирать новые версии при каждом запуске.

Не ориентируйтесь только на команды из старого руководства. У основных версий меняются поддерживаемый Node, расположение настроек и browser provider. Сначала проверьте node –version, затем фактические engines пакетов и конфигурации архива. Обновляйте инструменты отдельным изменением, после которого можно повторить тесты и сравнить отчёты. Это сохраняет понятную причину возможной несовместимости.

Общая установка и первый тест описаны в официальном Getting Started. В нашем архиве уже подготовлены конфигурации и команды для конкретного набора задач. Они избавляют от ручной сборки стенда, но не отменяют проверку вашего окружения, путей файлов и платформенных зависимостей браузера.

node --version
npm ci
npm test
npm run capture
npm run verify
npm run build-public

Мини-мастерская: живой компонент и настоящие отчёты

Выберите задачу и сравните две подготовленные реализации. Ошибочный вариант показывает конкретное нарушение договора, исправленный — ожидаемое поведение. Смотрите не только на цвет отчёта: прочитайте имя теста, ожидаемое значение, фактическое значение и код, который был выполнен. Затем попробуйте соответствующее действие в живом компоненте, чтобы связать техническую проверку с тем, что видит человек.

Отчёты получены настоящим Vitest и сохранены вместе со сведениями о запуске. Переключение варианта на странице открывает другую запись. Оно не запускает Node-процесс и не пересчитывает coverage. Живой компонент, напротив, действительно исполняет общий JavaScript-модуль в браузере. Тесты из архива импортируют этот же модуль, поэтому демонстрация поведения связана с проверяемым кодом.

Исходники позволяют повторить обе части у себя: выполнить обычные проходящие тесты, отдельно воспроизвести намеренные отказы и заново сформировать записи. У Browser Mode есть отдельная граница: подготовленный файл конфигурации не равен выполненному браузерному тесту. В отчёте должна сохраняться фактическая отметка выполнения, а не зелёный значок только за наличие соответствующего пункта в меню.

На странице отдельно показаны живой JavaScript-компонент и сохранённые отчёты настоящего Vitest. Новый запуск runner выполняется из архива. Проверки jsdom не подтверждают мобильную вёрстку или Browser Mode.

Хороший тест начинается с ожидаемого поведения

До написания expect сформулируйте правило человеческим языком. Например: «Пустой адрес не отправляется на сервер, рядом с полем появляется объяснение». В этой фразе уже есть вход, запрещённое действие и наблюдаемый результат. Она помогает выбрать проверки, которые сохранят смысл после перестановки HTML-элементов или внутреннего рефакторинга функции.

Удобная структура теста состоит из подготовки, действия и проверки. Подготовьте данные или компонент, выполните одну понятную операцию, сравните результат с договорённостью. Ожидаемое значение желательно получить независимо от реализации: для небольшой суммы посчитать вручную, для статуса выбрать предусмотренное правило. Если ожидаемый результат вычисляется тем же кодом, что и фактический, тест способен повторить ошибку и всё равно пройти.

Не обязательно проверять каждый внутренний вызов. Слишком подробное знание реализации делает тесты хрупкими: полезное изменение архитектуры ломает десятки проверок, хотя пользовательский результат остался правильным. Выбирайте устойчивые наблюдаемые признаки: возвращённые данные, статус, отправленный payload, доступное сообщение и согласованное состояние элементов управления.

import { expect, test } from 'vitest';

test('пример понятного договора', () => {
  const actual = [120, 80].reduce((sum, item) => sum + item, 0);
  expect(actual).toBe(200);
});

Опыт 1. Цена заказа: обычное значение и границы

Расчёт суммы кажется простым до появления скидки, нулевого количества и округления. В первом опыте сравниваются подготовленные ошибочная и исправленная функции. Введите обычные данные, затем попробуйте границу допустимого диапазона. Тест должен объяснять, что именно обещает функция, а не только подтверждать один удобный пример, на котором обе реализации случайно совпали.

Для денежных расчётов важно заранее выбрать представление. Целые копейки позволяют избежать части ошибок двоичной арифметики, но сами по себе не определяют правила скидки и округления. Отдельно договоритесь, на каком шаге округляется результат и какие значения недопустимы. Проверка должна фиксировать эти бизнес-правила явно, иначе две команды могут по-разному понимать «правильную» итоговую сумму.

Неверный ввод не следует молча превращать в разумное на вид число. Если отрицательное количество запрещено, полезен тест на отказ. Для исключения передайте expect функцию, которую нужно вызвать, а не результат уже выполненного вызова. В живом интерфейсе аналогичная ошибка должна стать понятной подсказкой, не оставляя старую успешную сумму рядом с новыми неправильными параметрами.

import { expect, test } from 'vitest';
import { createLab } from '../public/model.js';
const lab = createLab('fixed');

test('3 товара по 199 рублей, скидка 10%', () => {
  expect(lab.total(19900, 3, 10)).toBe(53730); // Все суммы в копейках.
});
test('дробное количество запрещено', () => {
  expect(() => lab.total(100, 1.5)).toThrow(RangeError);
});

Падающий тест должен падать по правильной причине

Красный результат сам по себе ещё не полезен. Тест может упасть из-за отсутствующего импорта, неверного пути, ошибки запуска или неправильного ожидания. В учебном примере нужно видеть расхождение результата конкретной операции: например, неверную сумму или лишний вызов. Именно такое падение доказывает, что проверка чувствительна к интересующей нас ошибке.

После исправления повторите тот же тест без ослабления условия. Если ради зелёного отчёта удалить assertion или заменить ожидаемое значение фактическим, исходная проблема останется без защиты. Иногда договор действительно меняется, но тогда изменение теста должно быть объяснено требованием продукта. Оно не должно прятаться среди механического обновления большого файла.

В архиве ошибочные реализации отделены от обычного набора, который должен завершаться успешно. Скрипт записи запускает их специально и сохраняет ненулевой exit code как ожидаемую часть опыта. Поэтому красная запись в мастерской не означает, что читателю предлагают сломанный основной проект. Она показывает воспроизводимый отказ конкретного теста до исправления.

Опыт 2. test.each: таблица значений для формы

Проверка адреса почты должна рассмотреть пустое поле, пробелы, отсутствие обязательных частей и обычный корректный ввод. Когда логика действия одна, удобно описать набор входов таблицей и выполнить test.each. Каждая строка становится отдельным тестом с собственным именем. В отчёте видно, какой вариант нарушил правило, вместо общего сообщения о неудаче большой функции.

Имя проверки должно включать понятное описание случая. Это полезнее, чем двадцать безымянных строк, где ошибку приходится искать по позиции в массиве. Разделяйте нормализацию и проверку: например, решение об удалении пробелов по краям должно быть частью контракта. Не добавляйте случайную «умную» коррекцию адреса, которую пользователь не ожидает.

Даже хороший клиентский валидатор не доказывает существование почтового ящика и не отменяет серверную проверку. Для сложных структур полезно отдельно изучить Ajv и JSON Schema. Табличные тесты помогут проверить выбранную схему на характерных данных, но полнота набора случаев по-прежнему зависит от требований и наблюдений разработчика.

import { expect, test } from 'vitest';
import { createLab } from '../public/model.js';
const lab = createLab('fixed');

test.each([
  ['пустая строка', '', false],
  ['нет домена', 'anna@', false],
  ['обычный адрес', 'anna@example.test', true],
])('%s', (_name, input, expected) => {
  expect(lab.validateEmail(input)).toBe(expected);
});

Опыт 3. Асинхронная загрузка: дождаться результата

Асинхронный тест должен действительно дождаться операции. Если вызвать Promise и забыть await или return, проверка может закончиться раньше ответа, а ошибка появится вне ожидаемого места. В третьем опыте функция обращается через fetch к локальному тестовому HTTP-серверу. Успешный ответ и отказ сервера проверяются раздельно, чтобы интерфейс не принимал любой JSON за успешную загрузку.

HTTP-статус и факт разрешения Promise — разные вещи. fetch обычно возвращает Response и для ответа 500; приложение само проверяет ok и решает, как представить ошибку. Тест успешного маршрута сравнивает данные, а тест отказа убеждается, что ошибка дошла до вызывающего кода. Иначе форма может показать «Сохранено», хотя сервер сообщил о проблеме.

Локальный сервер делает этот опыт воспроизводимым и не зависит от чужого публичного API. Он запускается только для проверки и закрывается после неё. Это настоящий HTTP-вызов в пределах тестовой машины, но не проверка боевого DNS, TLS, авторизации внешнего поставщика или качества соединения пользователя. На странице живой пример обращается к JSON-файлу самой мастерской; кнопка отказа запрашивает отсутствующий файл. Его 404 может содержать HTML от веб-сервера, поэтому ошибка разбора в ошибочной реализации отличается от контролируемого 503 в тесте.

// В tests/scenarios.test.js base создаёт beforeAll:
// настоящий node:http server на случайном loopback-порту.
// После набора afterAll закрывает его соединения и listener.
test('настоящий HTTP возвращает клиента', async () => {
  await expect(lab.loadCustomer(fetch, base + '/customer'))
    .resolves.toEqual({ id: 7, name: 'Анна' });
});
test('HTTP 503 становится отказом операции', async () => {
  await expect(lab.loadCustomer(fetch, base + '/unavailable'))
    .rejects.toThrow('HTTP 503');
});

Опыт 4. Mock API: проверить данные отправки

Иногда нужно проверить форму независимо от доступности сети. Подмените внешний метод функцией vi.fn и задайте её ответ. После отправки проверьте не только сообщение об успехе, но и параметры вызова: куда и с какими данными обратилась операция. Такой тест ловит забытое поле, неправильное имя параметра или лишнюю отправку по двойному нажатию.

Подмена должна находиться на понятной границе. Если заменить и проверяемую функцию, и её зависимости, можно получить зелёный тест, который ничего не говорит о рабочей логике. В нашем наборе mock-сценарий дополняется отдельным настоящим HTTP-опытом. Первый делает отказ и payload управляемыми, второй проверяет фактическую работу транспорта и преобразования ответа.

После теста состояние подмен нужно очищать или восстанавливать. Иначе следующий случай унаследует число вызовов, заменённый метод или фиксированное значение. Выбирайте действие осознанно: очистка истории вызовов и восстановление исходной функции имеют разный смысл. Официальное руководство Mocking описывает эти инструменты и ограничения разных окружений.

import { expect, test, vi } from 'vitest';
import { createLab } from '../public/model.js';
const lab = createLab('fixed');

test('не теряем копейки и убираем крайние пробелы', async () => {
  const post = vi.fn().mockResolvedValue({ id: 42 });
  await lab.saveOrder({ post }, '  anna@example.com  ', 53730);
  expect(post).toHaveBeenCalledExactlyOnceWith('/orders', {
    email: 'anna@example.com', totalCents: 53730
  });
});

Mock, spy и настоящая интеграция

Mock задаёт контролируемое поведение зависимости. Spy позволяет наблюдать вызовы существующего метода и при необходимости временно заменить реализацию. В обоих случаях важно понимать, что именно осталось настоящим. Проверка вызова подменённого API не доказывает, что реальный сервер принимает такой формат, поэтому контракт полезно подтверждать отдельным интеграционным тестом.

Не старайтесь подменять всё на уровне глобальных объектов. Часто проще передать зависимость параметром или создать небольшой сервис с явным методом отправки. Тогда тест читает намерение и не зависит от порядка загрузки модулей. В более сложных случаях пригодится vi.mock, но его поведение при поднятии объявления и работе с импортами нужно изучить отдельно.

Ограничения Browser Mode также отличаются от Node-окружения. Например, namespace нативного ES-модуля не является обычным свободно изменяемым объектом. Не переносите автоматически каждый приём vi.spyOn из Node в браузер. Проверьте выбранный provider и официальную документацию. Хорошая архитектура границ часто оказывается полезнее, чем сложная подмена внутренних деталей.

import { afterEach, vi } from 'vitest';

afterEach(() => {
  vi.restoreAllMocks();
  vi.useRealTimers();
});

Опыт 5. Debounce и управляемое время

Поисковая строка не должна отправлять запрос после каждой буквы, если продукт предполагает небольшую паузу ввода. Debounce сохраняет только последнее отложенное действие. Ошибочная реализация часто забывает отменить предыдущий таймер, и сервер получает несколько запросов. Тест должен проверить именно это пользовательское следствие: до истечения паузы вызова нет, после неё отправляется один актуальный запрос.

Ждать настоящие секунды в таком тесте неудобно и нестабильно. Fake timers позволяют управлять тестовым временем: подготовить несколько изменений, передвинуть часы и проверить наблюдаемый результат. При этом приложение работает с подменёнными таймерами, а не становится быстрее в production. Это инструмент проверки последовательности, не средство ускорения реального интерфейса.

Возвращайте настоящие таймеры после завершения, даже если assertion упал. Если код смешивает таймеры с Promise, выбирайте асинхронное продвижение времени и ждите его завершения. Подробности механизма доступны в руководстве Timers. Проверку сетевого времени и поведения вкладки в фоне выполняют отдельно.

import { afterEach, expect, test, vi } from 'vitest';
import { createLab } from '../public/model.js';
afterEach(() => vi.useRealTimers());

test('ищем только последнее значение', async () => {
  vi.useFakeTimers();
  const callback = vi.fn();
  const search = createLab('fixed').debounceSearch(callback, 300);
  search('а'); search('ан'); search('анна');
  await vi.advanceTimersByTimeAsync(299);
  expect(callback).not.toHaveBeenCalled();
  await vi.advanceTimersByTimeAsync(1);
  expect(callback).toHaveBeenCalledExactlyOnceWith('анна');
  search.cancel();
});

Таймеры и отмена: проверять отсутствие лишнего действия

Часто самый полезный assertion подтверждает, что действие не произошло. После отмены поиска не должен отправиться старый запрос. После закрытия компонента не должен сработать забытый callback. После нового ввода не должен отображаться результат предыдущей строки. Эти проверки защищают интерфейс от ошибок, которые трудно заметить в спокойном ручном сценарии.

Одна только отмена таймера не решает все гонки. Если запрос уже отправлен, понадобится отмена ожидания или номер актуальной операции. Поздний ответ должен быть проигнорирован, когда состояние изменилось. В тесте можно управляемо разрешить старый Promise после нового и проверить, что экран сохранил свежий результат. Это осмысленнее, чем добавлять длинный sleep в надежде поймать нужный порядок.

В живом компоненте мастерской ограничения нужны и для удобства: сброс должен вернуть понятное исходное состояние, повторный запуск — оставаться доступным, а результат — соответствовать текущему вводу. Проверки отдельных функций помогают построить эту логику, но её соединение с настоящими кнопками и полями всё равно требует отдельной приёмки интерфейса.

Опыт 6. Контактная форма в jsdom

Форма связывает несколько действий: человек вводит адрес, нажимает отправку, видит ошибку либо подтверждение. В jsdom можно создать DOM, смонтировать компонент, изменить поле и вызвать предусмотренное событие. Тест проверяет состояние элементов и текст сообщения. Такой подход позволяет быстро заметить неправильный обработчик, забытое отключение кнопки или отсутствие реакции на ошибку валидации.

Выбирайте элементы по устойчивому пользовательскому смыслу: label, роли и доступному имени. Селектор из цепочки служебных div и классов ломается при косметической перестройке, хотя поведение осталось правильным. Само наличие доступного имени полезно не только тестам: оно помогает людям, которые используют экранный диктор или другие способы взаимодействия.

jsdom моделирует веб-документ внутри Node. Он не выполняет полноценную браузерную раскладку CSS и не подтверждает, что кнопка видна на телефоне или не перекрыта другой панелью. Успешный DOM-тест говорит о проверенном поведении модели. Сравнение окружений приведено в Test Environment. Контактная форма мастерской показывает локальное состояние и не сохраняет введённый адрес в CRM или базе; настоящий HTTP рассматривается в отдельном опыте.

// @vitest-environment jsdom
import { expect, test } from 'vitest';
import { createLab } from '../public/model.js';

test('ошибка пустого поля видна, фокус возвращён к вводу', () => {
  const cleanup = createLab('fixed').mountContactForm(document.body);
  try {
    document.querySelector('form').dispatchEvent(
      new Event('submit', { bubbles: true, cancelable: true })
    );
    const input = document.querySelector('input');
    expect(input.getAttribute('aria-invalid')).toBe('true');
    expect(document.activeElement).toBe(input);
    expect(document.querySelector('[role=status]').textContent)
      .toBe('Укажите корректный email');
  } finally { cleanup(); }
});

Проверить ошибку формы глазами пользователя

Полезный тест формы не заканчивается на том, что функция validate вернула false. Нужно убедиться, что неправильные данные не отправлены, ошибка показана рядом с нужным полем, а пользователь понимает следующий шаг. При повторной корректной попытке старое сообщение должно исчезнуть. Иначе техническая часть может работать, а интерфейс продолжит выглядеть сломанным.

Проверяйте состояние ожидания отдельно от завершения. Пока операция выполняется, повторная отправка обычно ограничена. После успеха, отказа и отмены кнопка должна прийти в предусмотренное состояние. Если finally отсутствует или относится только к успешной ветке, человек может остаться с навсегда выключенной кнопкой. Для воспроизведения удобно использовать контролируемый Promise.

Текст ошибки не обязан быть жёстко зафиксирован целиком во всех тестах. Иногда достаточно стабильного кода, наличия сообщения и связи с полем. Полное совпадение полезно там, где формулировка является частью договора. Выбирайте точность, которая ловит реальную проблему и не заставляет переписывать десятки проверок после улучшения одной человеческой фразы.

Опыт 7. Раскрывающаяся панель, состояние и фокус

Небольшой disclosure-компонент хорошо показывает связь DOM, доступности и реального браузера. У кнопки есть состояние aria-expanded, у управляемой панели — видимость. При открытии и закрытии эти признаки должны согласованно меняться. Если действие предполагает переход фокуса или закрытие по Escape, это тоже нужно описать и проверить явно.

В jsdom можно проверить атрибуты, скрытие и логическое значение document.activeElement. Но наличие такого теста не доказывает, что клавиатурное действие в реальном браузере вызывает те же события или что контент доступен при узкой ширине. Для этого подготовлен отдельный Browser Mode. Его результаты учитываются только после настоящего запуска выбранного браузерного provider.

Не добавляйте сложные ARIA-роли ради теста. Обычная кнопка с понятным названием и корректным состоянием часто лучше самодельного элемента с неполной клавиатурной логикой. Пример помогает увидеть нарушение и исправление, а не обещает автоматически проверить всю доступность приложения. Экранный диктор, порядок чтения и визуальный фокус требуют самостоятельной оценки.

// Фрагмент теста в jsdom; полный setup находится в архиве.
const cleanup = lab.mountDisclosure(document.body);
try {
  const toggle = document.querySelector('#details-toggle');
  toggle.click();
  expect(toggle.getAttribute('aria-expanded')).toBe('true');
  document.querySelector('input').dispatchEvent(
    new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
  );
  expect(document.querySelector('#details-panel').hidden).toBe(true);
  expect(document.activeElement).toBe(toggle);
} finally { cleanup(); }

Browser Mode: реальный браузер и роль provider

Browser Mode выполняет тесты в браузерном окружении. Provider определяет, как запускается браузер и как взаимодействие до него доходит. Для CI обычно нужен автоматизируемый provider, например Playwright. В архиве предусмотрена отдельная конфигурация, чтобы браузерные проверки не смешивались незаметно с быстрыми Node/jsdom-тестами.

Preview provider удобен для ручного открытия страницы тестов, но имеет другую границу: взаимодействия могут симулироваться через DOM-события. Это не то же доказательство, что действия через автоматизационный протокол настоящего браузера. Официальное руководство Browser Mode прямо различает preview и provider для CI.

Для этой публикации подготовка конфигурации и выполнение теста учитываются раздельно. Если браузерный runtime не запускался, такой сценарий остаётся подготовленным к проверке и не получает отметку PASS. Записи Node и jsdom не подставляются вместо него. После запуска у себя проверьте конкретные ожидаемые действия, выбранный браузер, ширину окна и приложите собственный отчёт.

При подготовке материала 12 ноября 2026 года отдельно выполнен preview-прогон: три проверки компонента на ширинах 1280, 390 и 296 пикселей прошли успешно. Отдельный протокол Browser Mode описывает окружение и границы результата. Сохранённый Node/jsdom-отчёт мастерской относится к более раннему запуску, поэтому его browser.status=not-run оставлен без подмены.

В ручной конфигурации архива автоматическое открытие нового окна отключено через BROWSER=none. После команды preview откройте полный URL, который напечатает Vitest; API runner привязан к 127.0.0.1:8526. Это отдельный адрес от статической мастерской на 8525.

В Vitest 5 браузерные page и userEvent импортируются из vitest/browser. В конфигурациях стенда задано publicDir: false: общий модуль и CSS из public должны проходить обработку Vite при импорте в тест. Это не меняет отдельный статический сервер мастерской. Старый импорт @vitest/browser/context для версии этого проекта использовать не нужно.

В закреплённом preview 5.1.0 есть особенность обработки таймеров: callback advanceTimers вызывается даже с обычными таймерами. В тесте архива собственный экземпляр userEvent проверяет vi.isFakeTimers() перед продвижением времени. Эта настройка применяется только к preview; Playwright использует обычный provider. Проверки клика, фокуса, Escape и ширины остаются теми же, а собственный экземпляр и DOM очищаются после каждого теста.

# Ручной запуск подготовленного preview:
npm run browser:preview

# Автоматизированная browser-проверка из архива:
npm run browser:ci

В tests/disclosure.browser.test.js подготовлены три ширины: 1280, 390 и 296 пикселей. Проверяются открытие, фокус поля, Escape, возврат фокуса и отсутствие горизонтального выхода документа. Эти проверки относятся к маленькому disclosure-компоненту; они не заменяют осмотр всей мастерской и опубликованной статьи.

Для автоматизированного Chromium установите браузерный runtime. На Windows выполните npx.cmd playwright install chromium. На Linux команда npx playwright install --with-deps chromium также устанавливает необходимые системные зависимости и может потребовать административные права. На существующем сервере сначала согласуйте такую установку с его обычным способом сопровождения; для CI удобнее отдельное подготовленное окружение. Команды приведены в официальной инструкции установки браузеров Playwright.

import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';

export default defineConfig({ publicDir: false, test: {
  include: ['tests/disclosure.browser.test.js'],
  browser: {
    enabled: true,
    provider: playwright(),
    headless: true,
    instances: [{ browser: 'chromium' }]
  }
}});

Мобильная ширина: что проверять дополнительно

Кнопка может правильно менять aria-expanded и одновременно оказаться за пределами экрана. Поле может проходить проверку DOM, но иметь неудобную высоту для касания. Поэтому отдельно проверяйте отсутствие горизонтальной прокрутки всей страницы, перенос длинных подписей, доступность основных действий и читаемость результата. Прокрутка кода внутри ограниченного блока допустима; разъехавшийся весь интерфейс — другая проблема.

Узкая ширина браузера полезна для проверки responsive CSS, но не воспроизводит все особенности телефона. Физическая клавиатура, виртуальная клавиатура, масштабирование при фокусе и touch-события могут вести себя иначе. В отчёте стоит писать, что именно проверено: viewport, браузер, эмуляция или настоящее устройство. Это помогает не превращать одну удачную картинку в обещание полной мобильной совместимости.

Для вашего проекта составьте короткий маршрут: открыть инструмент, выбрать задачу, ввести неправильные данные, исправить их, выполнить действие, отменить ожидание и скачать результат. Проверьте его мышью, клавиатурой и на узкой ширине. Тесты облегчают повторение отдельных условий, но удобство последовательности оценивается целиком, с понятными текстами и состояниями.

Опыт 8. Snapshot: заметить осмысленное изменение

Snapshot сохраняет ожидаемое представление результата: небольшой объект, фрагмент разметки или другую стабильную структуру. При следующем запуске Vitest сравнивает её с текущей версией и показывает различие. Это удобно, когда важно одновременно увидеть несколько связанных полей. Но слишком большой снимок трудно просматривать: значимое изменение теряется среди случайных атрибутов и технических деталей.

Выбирайте компактный результат и исключайте из него непредсказуемое время, случайные идентификаторы и порядок, который не является частью договора. Если такие данные важны, фиксируйте вход или проверяйте их отдельно по смыслу. Снимок должен помогать человеку принять изменение, а не превращаться в файл, который всегда обновляют не глядя.

Команда обновления snapshot не исправляет реализацию. Она говорит тесту принять новый эталон. Поэтому сначала прочитайте diff, убедитесь, что изменение ожидаемо, и только затем сохраните его. В мастерской ошибочный и исправленный варианты позволяют увидеть, как тест замечает отличие структуры. Подход подробно описан в руководстве Snapshot.

import { expect, test } from 'vitest';
import { createLab } from '../public/model.js';

test('компактная структура результата', () => {
  const summary = createLab('fixed').summary({
    id: 42, customer: 'Анна', cents: 53730, paid: false
  });
  expect(summary).toMatchInlineSnapshot(`
    {
      "amount": "537.30",
      "currency": "RUB",
      "customer": "Анна",
      "id": 42,
      "state": "pending",
    }
  `);
});

Coverage: какие участки кода выполнялись

Покрытие показывает, какие строки, функции и ветки были исполнены во время тестов. В архиве используется подключаемый provider V8. Отчёт помогает найти пропущенную ветку отказа или функцию, до которой ни один тест не добрался. Числа в мастерской должны соответствовать реально полученному отчёту, а не рассчитываться по количеству зелёных карточек.

Сто процентов покрытия не означает отсутствие ошибок. Код может выполниться, а assertion не проверит важный результат. Например, тест вызывает расчёт скидки, но сравнивает только тип возвращённого значения. Все строки отмечены как выполненные, а неправильная сумма остаётся незамеченной. Поэтому покрытие полезно как указатель непроверенных мест, а не как автоматическая оценка качества продукта.

Область отчёта задавайте явно: включайте рабочие модули, которые хотите анализировать, и не смешивайте их с зависимостями или служебным генератором записей. В учебном модуле deliveryFee намеренно проверен только самовывоз: городская доставка, регион и отказ неизвестной зоны показывают, почему покрытие веток требует внимания. Порог должен поддерживать вашу практику, а не поощрять бессмысленные вызовы ради процента. Параметры provider и отчётов описаны в официальном Coverage.

npm run coverage

# Покрытие относится к выполненному набору тестов.
# Наличие HTML-отчёта не подтверждает Browser Mode.

Опыт 9. Watch для разработки и run для проверки

Во время разработки удобно повторять подходящие тесты после изменения файла. Для этого используется watch-режим: процесс остаётся запущенным и помогает быстро увидеть эффект правки. В CI требуется другой режим — один конечный запуск. Команда должна выполнить набор и завершиться с понятным exit code, чтобы автоматизация могла принять решение о продолжении сборки.

В девятом опыте сравниваются настоящий успешный и намеренно неуспешный запуски. Живой компонент отдельно проверяет canPublish: все выбранные условия должны быть истинными, пустой набор не считается доказательством. Нажатие этой кнопки не запускает CI и ничего не публикует. При отказе настоящего процесса недостаточно сохранить красивый отчёт: CI должен получить ненулевой код завершения. Иначе следующий шаг может выпустить артефакт, несмотря на упавшие проверки. Скрипт записи убеждается, что ожидаемое падение действительно произошло.

Не включайте намеренно ошибочные варианты в обычный зелёный npm test без явной причины. Они нужны для обучения и доказательства чувствительности теста, а не для постоянного красного pipeline. Команды из архива разделяют рабочую проверку, запись сравнений и проверку целостности этих записей. Так читателю проще понять, какой результат считается успешным на каждом шаге.

# Один конечный запуск:
npm test

# Во время разработки, отдельным процессом до Ctrl+C:
npm run watch

# Новые failing/passing-записи учебных опытов:
npm run capture

# Проверка подготовленных записей:
npm run verify
npm run build-public

Windows: PowerShell, установка и первый запуск

Установите Node.js 24 LTS и откройте новый терминал после установки. Распакуйте архив в отдельную папку, перейдите в корень проекта и проверьте версию Node. В PowerShell используйте npm.cmd, если политика выполнения мешает запуску npm.ps1. Для самого учебного стенда не требуется ослаблять системную политику выполнения скриптов.

Сначала выполните чистую установку, затем обычные тесты и проверку записей. Мастерская запускается отдельной командой локального сервера из README. Она показывает подготовленные отчёты и живой компонент, а не превращает статический сайт в удалённый test runner. Для просмотра browser preview предусмотрена другая команда и отдельная конфигурация.

Ошибка отсутствующего браузера относится к установке provider/runtime, а не к результату assertions. Для автоматизированного Browser Mode понадобится установленный браузерный пакет соответствующего инструмента. Не заменяйте такой сбой отметкой «тест пройден». Сохраните текст ошибки, проверьте README и повторите установку в подходящем окружении. Собственные watch и preview-процессы завершайте Ctrl+C в их терминале.

node --version
npm.cmd ci
npm.cmd test
npm.cmd run capture
npm.cmd run verify
npm.cmd run coverage
npm.cmd run build-public

# Статическая мастерская на loopback-адресе из вывода:
npm.cmd run serve

# В другом терминале: отдельный режим с настоящим браузером.
npm.cmd run browser:preview

Linux: локальный запуск и окружение CI

На Linux используйте поддерживаемый Node 24 LTS и восстановите зависимости через npm ci. Обычные Node/jsdom-тесты не требуют графического рабочего стола. Настоящая браузерная проверка имеет дополнительные зависимости: браузерный бинарный файл и системные библиотеки. Их установка зависит от дистрибутива и выбранного provider, поэтому команды нужно выполнять в предусмотренном окружении.

Не запускайте тестовый сервер от root только потому, что так проще обойти ошибку доступа. Создайте рабочую директорию, доступную пользователю сборки, и храните артефакты отдельно от production-данных. Временный HTTP-сервер интеграционного теста должен слушать loopback и завершаться после проверки. Он не предназначен для публичного API и не требует открытия порта наружу.

Если локально всё проходит, а в CI появляется ошибка, сравните версии Node, lock-файл, переменные окружения, файловые пути и доступные системные зависимости. Linux чувствителен к регистру имени файла, поэтому импорт, случайно работавший на Windows, может оказаться неверным. Чистая установка из архива помогает обнаружить такие зависимости от предыдущего состояния рабочего каталога.

node --version
npm ci
npm test
npm run capture
npm run verify
npm run coverage
npm run build-public

# Browser Mode проверяется отдельно после установки runtime:
npm run browser:ci

CI/VPS: тестировать сборку, а не открывать runner в интернет

В CI обычно нужен короткий воспроизводимый порядок: получить исходники, установить закреплённые зависимости, запустить тесты, сохранить отчёты и завершить сборку при ошибке. На выделенном сервере или собственном runner действует тот же принцип. Vitest не обязан постоянно работать публичным веб-сервисом, чтобы приносить пользу вашему проекту.

Разделите права и каталоги. Пользователь сборки работает с тестовыми данными и артефактами, а production-приложение запускается своим способом. Не передавайте тестам реальные клиентские выгрузки и рабочие секреты без необходимости. Для webhook или API используйте отдельный тестовый контракт либо ограниченный локальный сервер, чтобы проверка не отправила настоящую операцию во внешнюю систему.

Публиковать на сайте можно статическую мастерскую и разрешённые отчёты. Watch, preview и runner оставляйте локальными или внутри инфраструктуры CI. При обновлении записи отчёта сохраняйте связь с исходниками и версиями пакетов. Тогда посетитель сможет скачать проект, повторить команду и понять, к какому коду относятся показанные результаты, даже если библиотека позже выпустит новую версию.

# Пример последовательности шагов CI:
npm ci
npm test
npm run coverage

# Ошибка любого обязательного шага должна останавливать pipeline.
# Browser job добавляется отдельно с установленным provider/runtime.

Как читать assertion diff и искать причину

Начинайте с имени упавшего теста и первого значимого различия. Expected показывает договорённый результат, Received — то, что вернула текущая реализация. Для суммы это может быть одно число, для payload — пропавшее поле, для DOM — неправильный атрибут. Затем откройте место assertion и входные данные, чтобы понять, что именно проверялось.

Если одновременно упали многие тесты, сначала проверьте общую причину: импорт, setup, зависимость, environment или изменённый контракт. Массовое падение не обязательно означает десятки независимых ошибок. И наоборот, один зелёный тест не подтверждает все соседние сценарии. Смотрите на область проверки и не расширяйте вывод за её пределы.

Для нестабильного теста ищите неконтролируемую зависимость: настоящее время, случайность, порядок выполнения, внешний API или общий изменяемый объект. Увеличение timeout иногда нужно для тяжёлой операции, но не должно скрывать гонку. Лучше сделать условие завершения явным и ждать наблюдаемое состояние, чем добавлять большой фиксированный sleep без понимания причины.

# Один интересующий тест по имени:
npm test -- -t "нулевая цена"

# PowerShell:
npm.cmd test -- -t "нулевая цена"

Очистка состояния после каждого опыта

Тесты должны оставлять окружение понятным следующему случаю. Удаляйте смонтированный DOM, восстанавливайте подмены и таймеры, закрывайте HTTP-серверы и освобождайте созданные ресурсы. Если один тест зависит от того, что другой уже подготовил данные, порядок запуска становится скрытой частью договора. При параллельном выполнении такая зависимость часто превращается в случайные отказы.

BeforeEach и afterEach полезны для повторяемой подготовки и очистки, но не превращайте их в большую невидимую программу. Читатель теста должен понимать, откуда взялся компонент и почему доступен нужный объект. Общий helper оправдан, когда он уменьшает шум и сохраняет явный смысл, а не скрывает все действия за универсальным «подготовить всё».

Тестовые фикстуры тоже являются кодом проекта. Небольшой набор характерных значений обычно полезнее огромной случайной выгрузки. Включите обычный случай, пустоту, границу, отказ и важную последовательность действий. При исправлении реального сбоя добавьте минимальное воспроизведение, чтобы будущий разработчик видел причину существования проверки и не удалил её как непонятную сложность.

Связь с интерфейсами и библиотеками из других материалов

Подходы из этой статьи легко перенести в знакомые компоненты. Для кеша API проверьте, что повторный запрос использует предусмотренную политику, а неудачное оптимистическое изменение откатывается. Практическое поведение таких состояний разобрано в TanStack Query во Vue. Для виртуального списка отдельно проверяйте данные и реальные размеры в браузере.

В серверном коде Vitest может проверять функции преобразования, контракты и небольшие HTTP-интеграции. Для Hono удобно разделять проверку router в памяти и настоящий HTTP. Для очереди важно проверять повторы, ошибку и идемпотентность задачи. Общая идея остаётся той же: сначала определите наблюдаемое поведение, затем выберите среду, в которой оно действительно проявляется.

Не копируйте тесты механически вместе с библиотекой. Пример формы не знает правил вашего продукта, а учебный timeout не обязан подходить боевому API. Возьмите структуру проверки и способ управления зависимостями, затем добавьте собственные входы, ограничения и сценарии пользователя. Именно это превращает набор демонстраций в полезную практику сопровождения проекта.

Что находится в архиве и как повторить запись

Архив содержит исходники живых компонентов, тестовые сценарии, отдельные конфигурации окружений, package-lock.json, отчёты и инструменты их воспроизведения. README объясняет команды Windows и Linux, условия Browser Mode и границу между записью runner и действиями страницы. Лицензии сторонних зависимостей сохранены рядом с исходниками.

Первый запуск делайте без изменений: установите зависимости, выполните обычные тесты и проверьте записи. Затем выберите одну задачу, измените вход или реализацию и повторите соответствующую проверку. Команда capture заново получает сравнительные отчёты; verify проверяет подготовленные данные. Покрытие выполняется отдельной командой, поэтому его нельзя вывести из одного факта успешного npm test.

Восстановление Browser Mode требует настоящего браузерного provider. Если этот этап не выполнялся в вашем окружении, оставьте его отдельным незавершённым пунктом. Это полезнее, чем молча заменить его jsdom. Для публикации статической мастерской достаточно готовых публичных файлов, а сами тесты и Node-зависимости остаются в скачиваемом проекте и вашей среде разработки.

Официальные источники и следующий полезный шаг

Для API и текущих ограничений используйте первичные источники: начало работы, Test API и test.each, mocks, таймеры, окружения, Browser Mode, snapshots и coverage. Сверяйте примеры с версией проекта и установленным provider.

После мастерской выберите одну проблемную операцию своего сервиса. Запишите ожидаемый результат, минимальный вход и конкретный отказ, который уже встречался или дорого обойдётся пользователю. Сделайте тест, который воспроизводит нарушение, исправьте реализацию и включите проверку в обычный запуск. Такой небольшой законченный цикл обычно даёт больше практической пользы, чем большой набор поверхностных проверок без ясной цели.

По мере роста проекта добавляйте уровни проверки там, где они нужны: быстрые функции, контракт с настоящим локальным сервисом, DOM-поведение, реальный браузер и ручная оценка удобства. Сохраняйте точное описание того, что подтверждает каждый результат. Тогда отчёт становится рабочим инструментом принятия изменений, а не просто списком зелёных отметок.