lenec ru

← все посты

Visual regression в Playwright: screenshot baseline без боли

16K

Visual regression — это тесты, которые проверяют, что страница выглядит так же, как раньше. Не «работает» (это делают функциональные тесты), а именно «выглядит». Сценарий из жизни: разработчик правит margin кнопки на 4px, тесты зелёные, дизайнер пишет в Slack «вы сломали отступы на главной». Visual regression ловит это до прода.

В Playwright инструменты для скриншотов встроены, но из коробки набор быстро становится источником флакая. Покажу, что попробовала за пять лет работы с визуалкой, что зашло, что не зашло.

Базовый сценарий

Минимальный visual regression в Playwright выглядит так:

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

test('главная страница не изменилась', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('home.png', {
    maxDiffPixels: 100,
    threshold: 0.2,
  });
});

Первый прогон сохраняет baseline-скриншот, последующие сравнивают с ним. Если разница больше порога — тест падает, в репорт кладётся diff-картинка.

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

Главная боль: динамический контент

Любой динамический пиксель на странице — будущий flaky-тест. Список того, что я обязательно скрываю или фиксирую перед скриншотом:

  • Текущая дата и время.
  • Аватарки пользователей с randomuser.me и подобных сервисов.
  • Анимации входа (fade-in элементов).
  • Анимированные индикаторы загрузки.
  • Видео и iframe-ы с внешним контентом.
  • Reklamные блоки.
  • Ошибки шрифтов до их полной загрузки.

Перед скриншотом гоняю стандартный preflight:

async function preparePageForSnapshot(page: import('@playwright/test').Page) {
  await page.addStyleTag({
    content: `
      *, *::before, *::after {
        animation: none !important;
        transition: none !important;
        caret-color: transparent !important;
      }
    `,
  });

  // Дождаться полной загрузки шрифтов
  await page.evaluate(() => document.fonts.ready);

  // Ждём, пока все картинки догрузятся
  await page.waitForFunction(() => {
    const imgs = Array.from(document.images);
    return imgs.every((i) => i.complete && i.naturalWidth > 0);
  });
}

test('главная — снимок', async ({ page }) => {
  await page.goto('/');
  await preparePageForSnapshot(page);
  await expect(page).toHaveScreenshot('home.png');
});

Эта функция убирает 80% «само-флакая». Дальше идут точечные маски на оставшийся динамический контент.

Маски через locator

Playwright поддерживает mask в опциях скриншота. Передаёшь массив локаторов — они закрашиваются в фиксированный цвет:

await expect(page).toHaveScreenshot('feed.png', {
  mask: [
    page.getByRole('time'),
    page.getByRole('img', { name: /аватар/i }),
    page.getByTestId('online-counter'),
  ],
});

Плюс — не надо менять разметку. Минус — если локатор не найдёт элемент, маска не наложится, тест упадёт по разнице. Поэтому маски пишутся на стабильные ARIA-роли и data-testid.

Где брать стабильные данные

Visual regression на продовом окружении — гарантированный кошмар. Контент меняется каждые пять минут, любой A/B покрасит набор. Базовое правило: визуалка идёт на отдельном тестовом стенде с фиксированными seed-данными.

Делаю так:

  1. Перед прогоном поднимаю базу данных из снапшота. Postgres-снапшот восстанавливается за секунды.
  2. Время приложения замораживаю через переменную окружения APP_FIXED_TIME=2026-05-23T10:00:00Z. Бекенд читает её и возвращает фронту фиксированную дату.
  3. Внешние сервисы замокированы через WireMock или собственный заглушечный сервис.
  4. Шрифты включены через локальный CDN, не через внешний.

Без этих четырёх шагов набор будет красить через раз. Я знаю, что писать «стабильный стенд для визуалки» дорого, но альтернатива — толстая папка с baseline-скриншотами, которую раз в неделю кто-то пересохраняет «потому что флакают».

Гранулярность снимков

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

test('кнопка primary в hover-состоянии', async ({ page }) => {
  await page.goto('/styleguide');
  const button = page.getByRole('button', { name: 'Primary' });
  await button.hover();
  await expect(button).toHaveScreenshot('button-primary-hover.png');
});

На styleguide-странице (Storybook, Ladle) живут все ключевые компоненты в фиксированных состояниях. Один компонент — один-два снимка на ключевые состояния (hover, focus, disabled, loading). Падения локализуются по компоненту, дизайнеры быстро понимают, где правка.

Кросс-браузерность

Скриншоты в Chromium и в WebKit отличаются. Анти-алиасинг шрифтов, рендер скруглений, обработка субпикселей. Если визуалку гонять в обоих — придётся хранить две папки baseline-ов и принимать падения в каждом релизе браузера.

Моё решение — основной набор только в одном браузере (Chromium), отдельный набор «smoke» в WebKit и Firefox с гораздо более либеральным maxDiffPixelRatio. Этот smoke ловит только сильные отклонения вроде «у нас всё съехало».

Хранение baseline

Baseline-скриншоты могут весить много. У меня в крупном проекте папка занимала 800 МБ. Класть в Git напрямую — оно будет тормозить. Использую Git LFS:

git lfs track "*.png"
git add .gitattributes
git add tests/__screenshots__

Альтернатива — отдельный bucket S3 и скрипт, который скачивает baseline перед прогоном. Подходит, если у команды нет привычки работать с LFS.

Что делать с осознанным изменением UI

Дизайнер сказал «новый отступ 24px вместо 20». Тесты падают. Нужно перегенерить baseline:

npx playwright test --update-snapshots

Главное — не запускать это вслепую на CI. Сделать локально, посмотреть глазами на изменения, закоммитить с понятным сообщением «design: новые отступы по PR-123». Иначе через неделю никто не помнит, какие изменения были осознанными, а какие «само сломалось».

Что не зашло

  • Process на каждый релиз пересохранять все baseline. Это то же самое, что отключить тесты. Проверка перестаёт находить регрессии.
  • Visual regression на продовых данных. Контент динамический, пересохранения каждый день, набор бесполезен.
  • Снимки полных страниц на каждом виде viewport. Десктоп + ноут + планшет + мобила — это четыре раза больше baseline-ов и четыре раза больше времени прогона. Реальная польза — только на адаптивных компонентах.
  • Сравнение через Percy/Chromatic как замена локальной визуалке. Сервисы хорошие, но добавляют внешнюю зависимость и платную подписку. На небольшой команде стоковый Playwright + LFS закрывают задачу без подписки.

Чек-лист

  1. Перед скриншотом отключить анимации и transitions.
  2. Дождаться document.fonts.ready и загрузки картинок.
  3. Замаскировать аватарки, время, счётчики.
  4. Гонять на стенде с фиксированной БД и замороженным временем.
  5. Делать снимки на уровне компонентов в styleguide, а не страниц целиком.
  6. Один основной браузер для строгих сравнений, остальные — smoke.
  7. Baseline в LFS или S3, не в обычном Git.
  8. Update snapshots — только локально и осознанно.

Visual regression — это не «бесплатное покрытие UI» из учебника. Это отдельный набор тестов с собственной инфраструктурой, собственной flaky-rate и собственными ритуалами поддержки. Сделанная без подготовки — она будет первым кандидатом на удаление через месяц. Сделанная с подготовкой — экономит часы код-ревью каждую неделю.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.