Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
🧪
Тестирование
Опубликовано:
19.10.2026
•
Обновлено:
19.10.2026

E2E-тестирование React-приложений на Playwright: пишем устойчивые тесты и устраняем flaky-сценарии

Тимофей Ищенко

Нестабильные (flaky) тесты — главная проблема сквозного тестирования современных SPA-приложений. В React-приложениях компоненты рендерятся асинхронно, внутренние состояния меняются на лету, а анимации и сетевые запросы нередко приводят к ложным падениям сценариев в пайплайнах CI/CD.

Фреймворк Playwright решает эту проблему благодаря механизму автоматического ожидания (auto-waiting), веб-ориентированным проверкам (web-first assertions) и изоляции контекстов браузера. Разберем, как проектировать надежные сценарии, правильно работать с авторизацией и быстро находить причины сбоев.


Устойчивые локаторы: почему CSS и XPath приводят к падениям

Частая причина хрупкости тестов — привязка к деталям реализации верстки: каскадным таблицам стилей, автоматически сгенерированным классам (CSS Modules, Styled Components) или жестким путям XPath. Любой рефакторинг UI неизбежно ломает такие селекторы, даже если визуально и функционально интерфейс работает корректно.

Playwright предлагает ориентироваться на пользовательский интерфейс (User-facing locators), то есть искать элементы так, как их воспринимает пользователь или скринридер.

// Ненадежно: завязано на реализацию DOM и классы
page.locator('.styles_button__3xY1z');
page.locator('//div[2]/form/button[1]');

// Надежно: завязано на доступность и семантику
page.getByRole('button', { name: 'Войти' });
page.getByLabel('Электронная почта');

Приоритет выбора локаторов

  1. getByRole(role, options) — поиск по роли доступности (button, heading, checkbox, dialog) и имени. Помогает поддерживать семантическую корректность и доступность (a11y) интерфейса.
  2. getByLabel(text) — оптимальный выбор для полей ввода форм (<input>, <textarea>), связанных с тегом <label>.
  3. getByPlaceholder(text) — используется для полей, где нет явного label, но задан понятный placeholder.
  4. getByText(text) — поиск статического контента, предупреждений, уведомлений или сообщений об успехе.
  5. getByTestId(id) — явный идентификатор (data-testid="submit-btn"). Применяется, когда составить семантический локатор невозможно: в сложных графических виджетах, графиках или нестандартных интерактивных элементах.

Сценарий 1: Авторизация и повторное использование состояния (storageState)

Авторизация через графический интерфейс перед каждым тестом замедляет прогон набора тестов в разы. Playwright позволяет выполнить вход один раз, сохранить сессию (куки, localStorage) в файл и переиспользовать ее во всех последующих тестах.

Базовый сценарий входа через UI

Для проверки самого процесса аутентификации пишется изолированный тест:

import { test, expect } from '@playwright/test';

test('пользователь успешно входит в личный кабинет', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('qa.user@example.test');
  await page.getByLabel('Пароль').fill('TestPassword123!');
  await page.getByRole('button', { name: 'Войти' }).click();

  await expect(page).toHaveURL(/\/dashboard/);
  await expect(
    page.getByRole('heading', { name: 'Личный кабинет' })
  ).toBeVisible();
});

Оптимизация: сохранение сессии через setup-проект

Чтобы не повторять процедуру логина в каждом сценарии, настройте этап инициализации:

// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('аутентификация и сохранение состояния', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('qa.user@example.test');
  await page.getByLabel('Пароль').fill('TestPassword123!');
  await page.getByRole('button', { name: 'Войти' }).click();

  await page.waitForURL('**/dashboard');
  await expect(
    page.getByRole('heading', { name: 'Личный кабинет' })
  ).toBeVisible();

  // Сохраняем cookies и localStorage
  await page.context().storageState({ path: authFile });
});

Подключите файл состояния в конфигурации playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

Безопасность данных: Каталог playwright/.auth/ содержит реальные токены сессий и учетные записи. Обязательно добавьте этот путь в файл .gitignore, чтобы избежать утечки авторизационных данных в репозиторий.


Сценарий 2: Оформление заявки на изолированных данных

При тестировании мутирующих операций (создание, редактирование, удаление сущностей) важно избегать конфликтов между параллельно запущенными тестами.

import { test, expect } from '@playwright/test';

test.describe('Управление заявками', () => {
  test('пользователь успешно создает новую заявку', async ({ page }) => {
    // Генерируем уникальный идентификатор для изоляции данных
    const uniqueTitle = `Заявка на аудит #${Date.now()}`;

    await page.goto('/requests/new');

    // Заполнение формы
    await page.getByLabel('Название заявки').fill(uniqueTitle);
    await page.getByLabel('Тип услуги').selectOption({ label: 'Консультация' });
    await page.getByLabel('Описание').fill('Требуется предварительная оценка проекта.');
    
    // Отправка формы
    await page.getByRole('button', { name: 'Отправить заявку' }).click();

    // Проверка результата: появление уведомления и наличие в списке
    await expect(page.getByText('Заявка успешно отправлена')).toBeVisible();
    await expect(page.getByRole('table')).toContainText(uniqueTitle);
  });
});

Борьба с flaky-тестами: отказ от sleep и waitForTimeout

Главный источник нестабильности тестов — использование фиксированных задержек вроде page.waitForTimeout(3000) или аналогов sleep. В локальном окружении трех секунд может хватать с запасом, а на нагруженном сервере CI/CD задержка сети окажется больше, и тест упадет. И наоборот: завышенные таймауты многократно увеличивают общее время выполнения сборки.

Playwright устраняет необходимость в ручных паузах с помощью двух концепций:

1. Auto-waiting

Перед выполнением любого действия (click, fill, check) Playwright автоматически проверяет условия готовности элемента (actionability checks):

  • элемент прикреплен к DOM (attached);
  • элемент видим на экране (visible);
  • элемент стабилен (не анимируется и не смещается);
  • элемент доступен для взаимодействия (не перекрыт другими слоями и не имеет атрибута disabled).

2. Web-first assertions

Утверждения вида expect(locator).toBeVisible() или expect(locator).toHaveText() работают асинхронно. Они опрашивают DOM до тех пор, пока условие не выполнится или не истечет глобальный таймаут (по умолчанию 5 секунд).

// Неправильно: синхронная проверка и искусственная пауза
await page.waitForTimeout(2000);
const isVisible = await page.getByRole('alert').isVisible();
expect(isVisible).toBe(true);

// Правильно: Web-first assertion с автоматическим ожиданием
await expect(page.getByRole('alert')).toBeVisible();

Изоляция контекста: BrowserContext и параллельный запуск

Каждый тест в Playwright по умолчанию выполняется в собственном объекте BrowserContext — изолированной песочнице, аналогичной режиму инкогнито. Контексты не делят между собой cookies, кэш, сессионные данные и localStorage.

Чтобы поддерживать надежность параллельного запуска:

  • Не создавайте цепочек зависимостей: сценарий "Редактирование заявки" не должен рассчитывать на то, что перед ним выполнился сценарий "Создание заявки".
  • Используйте уникальные сущности: при параллельной работе нескольких воркеров создавайте данные с уникальными префиксами (например, UUID или timestamp) либо выделяйте каждому воркеру отдельного тестового пользователя.
  • Очищайте сайд-эффекты: если тест изменяет глобальные настройки приложения, возвращайте их в исходное состояние через API в хуках test.afterEach или test.afterAll.

Диагностика сбоев: как находить причины падений с помощью Trace Viewer

Когда тест падает на CI-сервере, текстового лога и статичного скриншота часто недостаточно, чтобы понять контекст ошибки. Trace Viewer предоставляет полную временную шкалу выполнения теста с DOM-снапшотами до и после каждого шага, сетевыми логами и выводом консоли браузера.

Настройка сбора трейсов

В playwright.config.ts настройте запись трейсов при возникновении сбоев:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // Сохраняет трейс только при первом повторе упавшего теста
    trace: 'on-first-retry',
    // Альтернатива: 'retain-on-failure' — сохраняет трейс при любом сбое
  },
});

Анализ результатов

После завершения прогона файл трейса (trace.zip) можно открыть локально через консоль:

npx playwright show-trace path/to/trace.zip

В интерфейсе Trace Viewer доступны:

  • Action Timeline: покадровый просмотр состояния интерфейса в момент кликов и ввода данных;
  • DOM Snapshot: живой интерактивный слепок верстки для каждого шага (можно инспектировать селекторы прямо внутри трейса);
  • Network Tab: точный список всех HTTP-запросов и ответов с заголовками и телами, что позволяет выявить сетевые ошибки (4xx/5xx) или зависшие запросы;
  • Console: ошибки JavaScript и предупреждения React, возникшие в браузере во время выполнения сценария.

Чек-лист: правила написания надежных E2E-тестов

  1. Используйте семантические локаторы: отдавайте приоритет getByRole и getByLabel перед CSS-классами и структурой DOM.
  2. Исключите waitForTimeout: полагайтесь на встроенный auto-waiting и асинхронные матчеры expect(locator).*.
  3. Изолируйте тестовые данные: формируйте независимые сущности для каждого сценария, чтобы тесты могли выполняться параллельно в произвольном порядке.
  4. Оптимизируйте авторизацию: сохраняйте сессию в storageState и не храните сгенерированные файлы с токенами в системе контроля версий.
  5. Включайте Trace Viewer для CI: сохраняйте трейсы при сбоях, чтобы мгновенно локализовать причины деградации UI или сетевых задержек.

Часто задаваемые вопросы (FAQ)

Почему page.waitForTimeout() считается плохой практикой?

Фиксированная пауза замедляет выполнение тестов и не гарантирует стабильности: если сервер ответит чуть медленнее таймаута, тест упадет. Playwright самостоятельно дожидается доступности элементов и завершения асинхронных операций через Web-first assertions.

Как проверить валидацию формы в React при асинхронном вводе?

Используйте метод locator.fill() или locator.pressSequentially() (если в React-компоненте используется маска ввода), после чего проверяйте состояние ошибки через веб-ассерт: await expect(page.getByText('Неверный формат email')).toBeVisible().

Чем Trace Viewer превосходит обычную запись видео?

Видео показывает только плоскую картинку экрана. Trace Viewer содержит интерактивную копию DOM, позволяет исследовать код элементов на любом шаге, видеть сетевые вызовы API и консольные ошибки браузера.

Как обрабатывать появление динамических модальных окон или тостов?

Ищите модальные окна по их семантической роли: page.getByRole('dialog') или page.getByRole('alert'). Механизм auto-waiting автоматически дождется завершения анимации появления перед кликом по элементам внутри окна.

Что делать, если тесты конфликтуют из-за общей базы данных?

Запускайте воркеры с разделением пользователей или генерируйте уникальные идентификаторы для создаваемых записей (например, префиксы с меткой времени). Для тяжелых интеграций используйте создание тестовых контекстов через API перед началом сценария.


Заключение

Стабильность сквозного тестирования в React достигается за счет корректной архитектуры тестов: отказа от привязки к верстке в пользу семантических локаторов, полного исключения жестких таймаутов и изоляции контекста каждого прогона. Использование возможностей Playwright — от storageState до Trace Viewer — превращает E2E-тесты из источника ложных срабатываний в надежный инструмент контроля качества в CI/CD пайплайне.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также