E2E тестирование форм

В современных фронтенд-приложениях валидация форм часто строится вокруг связки схемной валидации и менеджера состояния формы. Одним из распространённых решений становится использование Yup как декларативного описания правил и resolver, который связывает схему с системой управления формой.

YupResolver выполняет роль адаптера между схемой валидации и механизмом обработки формы. Он преобразует результат проверки Yup в формат, понятный библиотекам управления формами, прежде всего в контексте интеграции с React Hook Form.

Ключевая задача резолвера заключается в унификации процесса валидации:

  • запуск схемы проверки при изменении значений формы
  • сбор ошибок в стандартизированную структуру
  • синхронизация состояния формы и результатов валидации

Схемная валидация с использованием Yup

Yup реализует декларативный подход, при котором правила описываются как схема данных. Это позволяет отделить бизнес-логику проверки от компонентов интерфейса.

Пример базовой схемы:

import * as Yup from "yup";

const schema = Yup.object({
  email: Yup.string()
    .email("Некорректный формат email")
    .required("Email обязателен"),

  password: Yup.string()
    .min(8, "Минимум 8 символов")
    .required("Пароль обязателен"),
});

Схема становится единым источником истины для валидации. Она может переиспользоваться как в клиентской части, так и в тестах.

Важной особенностью Yup является поддержка:

  • синхронной и асинхронной валидации
  • кастомных правил через test
  • условной логики через when
  • вложенных структур объектов и массивов

Подключение YupResolver к системе формы

Интеграция схемы валидации осуществляется через резолвер:

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";

const form = useForm({
  resolver: yupResolver(schema),
  mode: "onChange",
});

Механизм работы можно описать следующим образом:

  1. Пользователь изменяет значение поля
  2. React Hook Form инициирует пересчёт состояния
  3. YupResolver запускает проверку схемы
  4. Результат преобразуется в структуру ошибок
  5. Ошибки синхронизируются с состоянием формы

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

Структура ошибок и её влияние на UI

Результат работы резолвера имеет предсказуемую структуру:

{
  values: {},
  errors: {
    email: {
      type: "validation",
      message: "Некорректный формат email"
    }
  }
}

Такая структура позволяет UI-слою не зависеть от логики валидации. Интерфейс оперирует только состоянием формы и текстами ошибок.

В сложных формах с вложенными структурами (например, адреса, списки товаров) ошибки могут быть вложенными:

errors: {
  user: {
    address: {
      city: {
        message: "Город обязателен"
      }
    }
  }
}

Особенности работы с асинхронной валидацией

Асинхронные проверки часто используются для:

  • проверки уникальности email
  • проверки существования логина
  • обращения к серверной бизнес-логике

Yup поддерживает асинхронные тесты:

Yup.string().test(
  "check-email",
  "Email уже используется",
  async (value) => {
    const res = await fetch(`/api/check-email?email=${value}`);
    const data = await res.json();
    return data.available;
  }
);

При использовании YupResolver асинхронная валидация становится частью общего потока обработки формы. Это важно учитывать при E2E тестировании, так как поведение формы становится зависимым от сетевых задержек и состояния API.

Основные принципы E2E тестирования форм

E2E тестирование форм направлено на проверку поведения интерфейса в условиях, максимально приближенных к реальному использованию.

Проверяются следующие аспекты:

  • корректность отображения ошибок
  • поведение при вводе данных
  • взаимодействие с API
  • блокировка отправки при невалидных данных
  • успешная отправка при валидных данных

Основной фокус переносится с изолированных функций на полный пользовательский сценарий.

Подготовка окружения для тестирования

E2E тестирование обычно реализуется через инструменты вроде Cypress или Playwright. В основе лежит автоматизация браузера и управление DOM-событиями.

Пример структуры теста:

test("форма регистрации", async ({ page }) => {
  await page.goto("/register");

  await page.fill("input[name=email]", "test@test.com");
  await page.fill("input[name=password]", "123");

  await page.click("button[type=submit]");
});

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

Тестирование сценариев валидации

Проверка обязательных полей

Классический сценарий заключается в отправке пустой формы:

await page.click("button[type=submit]");

await expect(page.locator("text=Email обязателен")).toBeVisible();
await expect(page.locator("text=Пароль обязателен")).toBeVisible();

Такой тест проверяет связку UI → React Hook Form → YupResolver.

Проверка форматных ошибок

await page.fill("input[name=email]", "invalid-email");
await page.blur("input[name=email]");

await expect(page.locator("text=Некорректный формат email")).toBeVisible();

Blur-событие важно, так как многие формы используют режим onBlur или onChange.

Проверка минимальной длины

await page.fill("input[name=password]", "123");
await page.blur("input[name=password]");

await expect(page.locator("text=Минимум 8 символов")).toBeVisible();

Асинхронные сценарии в E2E тестах

Асинхронная валидация требует ожиданий, связанных с завершением сетевых запросов.

Мокирование API

При тестировании уникальности email часто используется перехват запросов:

await page.route("**/api/check-email*", (route) =>
  route.fulfill({
    status: 200,
    body: JSON.stringify({ available: false }),
  })
);

После этого выполняется ввод:

await page.fill("input[name=email]", "test@test.com");
await page.blur("input[name=email]");

await expect(page.locator("text=Email уже используется")).toBeVisible();

Синхронизация состояния формы и тестов

Одной из сложностей E2E тестирования становится асинхронность обновления состояния формы.

YupResolver может выполнять проверку:

  • при каждом изменении значения
  • при потере фокуса
  • при отправке формы

Это влияет на стратегию ожиданий:

await expect(page.locator("text=Ошибка")).toBeVisible({
  timeout: 5000,
});

Использование явных ожиданий предпочтительнее фиксированных задержек.

Работа с вложенными формами

Вложенные структуры данных требуют проверки глубинных ошибок:

await page.fill("input[name='user.address.city']", "");
await page.click("button[type=submit]");

await expect(page.locator("text=Город обязателен")).toBeVisible();

Такие сценарии часто используются в формах профиля, заказов и анкет.

Стратегии устойчивых селекторов

При E2E тестировании форм критично использовать стабильные селекторы:

  • data-testid
  • data-cy
  • data-e2e

Пример:

await page.fill("[data-testid=email-input]", "test@test.com");

Это снижает зависимость тестов от структуры DOM и CSS.

Тестирование поведения при сабмите

Финальный этап сценария связан с отправкой формы:

await page.fill("input[name=email]", "test@test.com");
await page.fill("input[name=password]", "12345678");

await page.click("button[type=submit]");

await expect(page).toHaveURL("/dashboard");

При использовании YupResolver отправка формы блокируется при наличии ошибок, что также проверяется:

await page.click("button[type=submit]");

await expect(page).toHaveURL("/register");

Сочетание YupResolver и тестовой архитектуры

Использование YupResolver влияет на структуру E2E тестов, так как формальная логика валидации становится централизованной.

Это приводит к следующим характеристикам тестовой модели:

  • единый источник правил валидации
  • предсказуемое поведение ошибок
  • возможность переиспользования схем в тестах
  • снижение дублирования логики проверки

Схема Yup может использоваться не только в UI, но и в тестовой среде для подготовки эталонных данных, что повышает согласованность между приложением и тестами.

Типовые ошибки при E2E тестировании форм

Часто встречающиеся проблемы:

  • отсутствие ожиданий асинхронной валидации
  • нестабильные селекторы
  • несинхронизированное состояние UI
  • игнорирование debounce-логики в onChange
  • отсутствие моков внешних API

Эти факторы напрямую влияют на стабильность тестов, особенно при использовании схемной валидации через YupResolver.

Поведение debounce и влияние на тесты

В некоторых реализациях формы используется задержка перед валидацией:

setTimeout(() => validate(), 300);

Это требует увеличения таймаутов ожидания в тестах или использования clock-mocking:

await page.clock.install();
await page.clock.fastForward(500);

Проверка комплексных сценариев

Комплексные формы включают:

  • множественные поля
  • условную валидацию
  • динамическое добавление элементов

Пример массива:

await page.click("button#add-item");
await page.fill("input[name='items[0].name']", "");
await page.click("button[type=submit]");

YupResolver корректно агрегирует ошибки по массивам, что проверяется через соответствие структуры DOM.

Интеграция с бизнес-логикой

Валидация через Yup часто отражает бизнес-правила:

  • ограничения по возрасту
  • зависимости между полями
  • ограничения на комбинации значений

E2E тестирование фиксирует соответствие этих правил реальному поведению интерфейса, обеспечивая непротиворечивость между слоем UI и доменной логикой.