12k
All articles

Обнаружение UI-багов с помощью визуального регрессионного тестирования

Визуальное регрессионное тестирование в Playwright и Vitest находит UI-ошибки, которые пропускают unit и E2E-тесты, с советами по baseline, flakiness и CI.

OpenReplay Team
OpenReplay Team
Обнаружение UI-багов с помощью визуального регрессионного тестирования

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

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

Ключевые выводы

  • Визуальное регрессионное тестирование сравнивает отрендеренный скриншот с утверждённым эталоном; любое отклонение, превышающее настраиваемый порог, приводит к провалу теста, выявляя ошибки цвета, позиционирования и контекста наложения, которые функциональные тесты не замечают.
  • expect(page).toHaveScreenshot() в Playwright встроен и бесплатен; по умолчанию параметр animations установлен в 'disabled', а автоповтор продолжается до тех пор, пока два последовательных скриншота не совпадут — поэтому waitForTimeout не нужен.
  • Большинство случаев нестабильности тестов объясняются четырьмя причинами: CSS-анимации, поздняя загрузка веб-шрифтов, динамический контент и субпиксельное сглаживание, варьирующееся в зависимости от GPU и ОС — для каждой из них есть конкретное решение, а не просто увеличение порога.
  • В CI следует зафиксировать конкретный Docker-образ Playwright (mcr.microsoft.com/playwright:v1.61.0-noble), поскольку рендеринг шрифтов различается в разных операционных системах и в противном случае приводит к ложным срабатываниям.
  • В Vitest 4.x добавлено встроенное визуальное регрессионное тестирование через toMatchScreenshot() в стабильном браузерном режиме (Browser Mode), что предоставляет пользователям Vitest нативный путь без необходимости переходить на другой инструмент.

Что такое визуальное регрессионное тестирование?

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

Это отличается от снапшот-тестирования. Снапшот-тест сериализует отрендеренную разметку компонента и сравнивает текст; визуальный регрессионный тест сравнивает фактические отрендеренные пиксели. Снапшоты на уровне разметки не замечают ничего сугубо визуального — изменения z-index, замены цветового токена, резервного шрифта — поскольку сериализованный DOM остаётся идентичным, даже когда то, что видит пользователь, изменилось.

Почему юнит-тесты и E2E-тесты пропускают визуальные баги?

Юнит-тесты проверяют, что кнопка вызывает обработчик onClick; E2E-тесты проверяют, что нажатие на неё завершает определённый сценарий; ни те ни другие не могут сообщить вам, что кнопка отображается неправильным цветом, сдвинулась на двенадцать пикселей вправо или скрыта за модальным оверлеем — именно этот пробел заполняет визуальное регрессионное тестирование. Матрица покрытия наглядно показывает эту границу:

СценарийЮнит (Jest/Vitest)E2E (Playwright/Cypress)Визуальное
Кнопка рендерится в DOMДаЧастичноДа
Кнопка вызывает onClickДаДаНет
Нажатие завершает сценарийНетДаНет
Кнопка правильного цветаНетНетДа
Кнопка на правильной позицииНетНетДа
Кнопка не перекрыта оверлеемНетНетДа

Последняя строка — та самая, которая «выстреливает» в продакшне. Рассмотрим <CheckoutButton>, который работает корректно до тех пор, пока кто-то не добавляет <CookieBanner>, чей контекст наложения перекрывает его:

// CheckoutButton.tsx
export function CheckoutButton({ onCheckout }: { onCheckout: () => void }) {
  return (
    <button data-testid="checkout" onClick={onCheckout}>
      Complete purchase
    </button>
  );
}

Юнит-тест, проверяющий вызов обработчика клика, проходит — JSDOM, реализация DOM, используемая Jest и Vitest по умолчанию, не вычисляет вёрстку и контексты наложения, поэтому не может знать, что кнопка перекрыта. E2E-утверждение вида await expect(page.getByTestId('checkout')).toBeVisible() также проходит, поскольку Playwright считает элемент видимым, когда у него есть непустой ограничивающий прямоугольник и он не имеет display:none — перекрытие элементом с более высоким z-index не меняет ни того ни другого. Хотя последующий locator.click() может обнаружить перекрытие через проверки доступности действий в Playwright, одно лишь утверждение видимости этого не выявит. Разница скриншотов покажет баннер, расположенный поверх кнопки.

Как работает визуальное регрессионное тестирование?

Рабочий процесс представляет собой пятишаговый цикл: захват эталона, запуск после изменения, сравнение нового скриншота с эталоном, проверка разницы, затем либо подтверждение изменения (обновление эталона), либо его отклонение (исправление кода). Playwright реализует это через механизм --update-snapshots.

При первом запуске теста, содержащего toHaveScreenshot(), эталон отсутствует, поэтому Playwright создаёт эталонное изображение. После того как эталон проверен и зафиксирован в репозитории, последующие запуски сравниваются с ним. Когда визуальное изменение является намеренным — переработанная кнопка, новый цветовой токен — выполните npx playwright test --update-snapshots, проверьте разницу в сгенерированном HTML-отчёте, зафиксируйте обновлённые файлы .png (для этих бинарных эталонов распространена практика использования Git LFS), и новый скриншот становится эталоном; следующий запуск CI сравнивается с ним. Флаг --update-snapshots регенерирует эталонные изображения на месте.

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

Техники сравнения и область охвата тестов

Инструменты визуального регрессионного тестирования используют одну из четырёх техник сравнения — попиксельную, по вёрстке, на основе DOM или с применением ИИ — и различаются по захватываемой области. Движок сравнения:

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

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

Относительно сравнения с применением ИИ: эта область часто неверно характеризуется. По состоянию на июнь 2026 года, визуальное сравнение на основе ИИ является основным платным предложением Applitools; Percy добавляет сортировку с помощью ИИ поверх попиксельного сравнения, тогда как Chromatic использует попиксельное и Git-aware сравнение, а не ИИ — а встроенные инструменты фреймворков (Playwright, Vitest browser mode) используют попиксельное сравнение с настраиваемыми порогами, чего достаточно для большинства команд при условии контроля нестабильности на ранних этапах.

Как настроить визуальное регрессионное тестирование с Playwright?

Playwright поставляется со встроенным утверждением для сравнения скриншотов, поэтому помимо @playwright/test никаких дополнительных зависимостей не требуется. Утверждение — expect(page).toHaveScreenshot(), задокументированное в справочнике API PageAssertions. Начните с конфигурации:

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

export default defineConfig({
  // Организуйте эталоны по браузерам, чтобы скриншоты из разных браузеров не конфликтовали.
  snapshotPathTemplate: '{testDir}/__screenshots__/{projectName}/{arg}{ext}',
  expect: {
    toHaveScreenshot: {
      // Допустимый абсолютный бюджет пикселей для шума субпиксельного рендеринга.
      maxDiffPixels: 100,
      // И относительный бюджет — 1% изображения — для более крупных захватов.
      maxDiffPixelRatio: 0.01,
    },
  },
  use: {
    // Фиксированный viewport: иначе изменения адаптивной вёрстки будут инвалидировать эталоны.
    viewport: { width: 1280, height: 720 },
  },
  // Зафиксируйте один браузер для детерминированных эталонов в CI.
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});

Два параметра несут основную нагрузку. Параметры maxDiffPixels и maxDiffPixelRatio задают допустимое отклонение до провала теста. Они существуют потому, что субпиксельное сглаживание варьируется между машинами — в документации Playwright прямо указано, что «разные операционные системы могут давать разные скриншоты». Порог maxDiffPixelRatio: 0.01 поглощает этот шум, не маскируя реальные регрессии.

Одно значение по умолчанию стоит понять, а не переопределять: для утверждения toHaveScreenshot() параметр animations по умолчанию равен 'disabled', что завершает конечные CSS-анимации и замораживает бесконечные перед захватом. Это отличается от page.screenshot(), где animations по умолчанию равен 'allow'. Таким образом, кадры в середине перехода уже обрабатываются для данного утверждения — вы не добавляете этот параметр, а полагаетесь на него.

Тест выглядит следующим образом:

// checkout.spec.ts
import { test, expect } from '@playwright/test';

test('checkout button is not obscured', async ({ page }) => {
  await page.goto('/checkout');
  // Web-first утверждение: ожидает появления элемента вместо фиксированного таймаута.
  await expect(page.getByTestId('checkout')).toBeVisible();
  await expect(page.getByTestId('checkout')).toHaveScreenshot('checkout-button.png');
});

Ограничение скриншота локатором вместо полной страницы сужает захватываемую область и фокусирует сравнение — именно это позволяет выявить перекрытие <CookieBanner> без необходимости обновлять эталон всей страницы при каждом несвязанном изменении.

Тестирование на уровне компонентов с Vitest browser mode

Если в вашем стеке уже используется Vitest, версия 4.0 перевела Browser Mode в стабильный статус и добавила встроенное визуальное регрессионное тестирование через toMatchScreenshot(). Это функция версии 4.x — в ветке 2.x такого утверждения нет, — поэтому в качестве минимальной версии укажите ^4.0. Настройка использует провайдер Playwright:

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

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: playwright(),
      instances: [{ browser: 'chromium' }],
    },
  },
});
// Button.visual.test.ts
import { expect, test } from 'vitest';
import { page } from 'vitest/browser';
import { render } from 'vitest-browser-react';
import { CheckoutButton } from './CheckoutButton';

test('checkout button matches baseline', async () => {
  render(<CheckoutButton onCheckout={() => {}} />);
  // Захватываем конкретный локатор элемента — документация Vitest помечает
  // захват всей страницы как антипаттерн.
  await expect(page.getByTestId('checkout')).toMatchScreenshot();
});

Провайдер — это функция playwright() из @vitest/browser-playwright, передаваемая как объект, а не строка 'playwright', использовавшаяся в более ранних версиях. Импорт контекста — из vitest/browser. Перед обновлением существующей настройки browser mode ознакомьтесь с руководством по миграции Vitest, поскольку пути импорта изменились в v4.

Тестирование на уровне компонентов со сторис Storybook

Если ваши компоненты находятся в Storybook (текущая версия v10.4), вам не нужно ничего переписывать для добавления визуального покрытия — повторно используйте уже поддерживаемые сторис. Старый паттерн с jest-image-snapshot и postVisit в test-runner теперь является устаревшим Node-подходом: он не работает в текущем браузерном режиме Storybook на базе Vitest, поскольку jest-image-snapshot зависит от Node.js.

Актуальный бесплатный локальный аналог — подключить сторис в браузерный тест Vitest с помощью API portable stories Storybook (composeStories) и использовать то же утверждение toMatchScreenshot() из приведённой выше настройки Vitest:

// CheckoutButton.visual.test.tsx
import { expect, test } from 'vitest';
import { page } from 'vitest/browser';
import { render } from 'vitest-browser-react';
import { composeStories } from '@storybook/react-vite';
import * as stories from './CheckoutButton.stories';

// Компонуем сторис с его аргументами, декораторами и аннотациями проекта,
// чтобы тест рендерил ровно то же состояние компонента, которое описывает сторис.
const { Primary } = composeStories(stories);

test('CheckoutButton matches its story baseline', async () => {
  render(<Primary />);
  // Ограничиваемся элементом, а не всей страницей.
  await expect(page.getByTestId('checkout')).toMatchScreenshot();
});

Это сохраняет единый источник истины — сторис — как для отрендеренных состояний компонента, так и для его визуального эталона. Два важных момента. Утверждение выполняется в отдельном браузерном тесте Vitest, а не через аддон Storybook Vitest, который не поддерживает toMatchScreenshot (выбрасывает Invalid Chai property: toMatchScreenshot); аддон обрабатывает тесты взаимодействия и доступности, но не попиксельные сравнения. Собственный продукт Storybook для визуального тестирования — Chromatic, платный облачный сервис, который следует выбрать, если вам нужны размещённые кросс-браузерные эталоны и управляемый рабочий процесс проверки вместо хранения эталонов в репозитории.

Почему мои визуальные тесты постоянно падают?

Большинство случаев нестабильности визуальных тестов объясняются четырьмя причинами: CSS-анимации, создающие кадры в середине перехода; веб-шрифты, загружающиеся после создания скриншота; динамический контент (временны́е метки, аватары, счётчики), который закономерно различается между запусками; и субпиксельное сглаживание, варьирующееся в зависимости от GPU и ОС — для каждой из них есть конкретное решение, а не просто увеличение порога. Повышение maxDiffPixels для подавления шума одновременно делает набор тестов слепым к реальным регрессиям, что сводит на нет весь смысл.

Вспомогательная функция стабилизации устраняет контролируемые источники проблем перед созданием скриншота:

// prepare-page.ts
import { Page } from '@playwright/test';

export async function preparePageForScreenshot(page: Page) {
  // 1. Принудительно устанавливаем нулевую длительность анимаций и переходов
  //    как дополнительную меру (toHaveScreenshot уже отключает их, но
  //    page.screenshot и видимые промежуточные состояния тоже выигрывают от этого).
  await page.addStyleTag({
    content: `*, *::before, *::after {
      animation-duration: 0s !important;
      transition-duration: 0s !important;
    }`,
  });

  // 2. Ожидаем загрузки веб-шрифтов. document.fonts.ready разрешается,
  //    когда загрузка шрифтов завершена, предотвращая рефлоу из-за резервного
  //    шрифта, который сдвигает текст.
  await page.waitForFunction(() => document.fonts.ready.then(() => true));

  // 3. Ожидаем завершения загрузки всех изображений.
  await page.waitForFunction(() =>
    Array.from(document.images).every((img) => img.complete)
  );

  // 4. Нейтрализуем динамический контент вместо его мокирования повсюду.
  await page.locator('[data-dynamic]').evaluateAll((els) =>
    els.forEach((el) => ((el as HTMLElement).style.visibility = 'hidden'))
  );
}

Шаг 2 опирается на document.fonts.ready — промис, который разрешается после завершения загрузки шрифтов документа и связанных операций вёрстки. Это правильный сигнал «шрифты отрисованы», значительно надёжнее фиксированной паузы.

Обратите внимание, чего эта вспомогательная функция намеренно не делает: она никогда не вызывает page.waitForTimeout(). Руководство по лучшим практикам Playwright не рекомендует фиксированные таймауты («Never wait for timeout in production»). Очевидная альтернатива, waitForLoadState('networkidle'), также не рекомендуется — в документации Playwright сказано «полагаться на веб-утверждения для оценки готовности». Правильный подход — web-first утверждения и явное ожидание элементов; к тому же toHaveScreenshot() уже автоматически повторяет попытки до совпадения двух последовательных скриншотов, что само по себе поглощает большую часть остаточного временно́го шума.

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

await expect(page).toHaveScreenshot('dashboard.png', {
  mask: [page.getByTestId('last-login'), page.locator('img[data-avatar]')],
});

Это чище, чем манипуляции с CSS-видимостью, поскольку замаскированная область по-прежнему занимает место в вёрстке, так что окружающая вёрстка сравнивается именно так, как она отрендерена.

Как запускать визуальные тесты в CI без ложных срабатываний?

Рендеринг шрифтов различается между Ubuntu, macOS и Alpine Linux даже при одинаковой версии браузера, поэтому эталон, захваченный на MacBook разработчика, будет давать ложные срабатывания при сравнении со скриншотом из раннера GitHub Actions на Ubuntu — зафиксируйте CI на конкретном официальном Docker-образе mcr.microsoft.com/playwright:v1.61.0-noble, чтобы устранить эту переменную. Плавающие теги вроде :latest больше не публикуются для этих образов, поэтому тег с конкретной версией — не опция, а необходимость.

Практическое правило, следующее из этого: генерируйте эталоны внутри того же контейнера, который использует CI. Эталон, захваченный локально на другой ОС, — наиболее распространённый источник расхождений только в CI. Создавайте эталоны в CI (или в зафиксированном контейнере локально) и фиксируйте их в репозитории.

Для самих эталонов распространена практика использования Git LFS — PNG-файлы бинарны и со временем раздувают репозиторий. Запись в .gitattributes направляет их в Git LFS:

tests/**/__screenshots__/** filter=lfs diff=lfs merge=lfs -text

Проверяйте изменения эталонов в пул-реквестах так же тщательно, как код. Когда визуальный тест падает в CI, загружайте HTML-отчёт Playwright как артефакт сборки, чтобы ревьюеры могли открыть сравнение «до и после». Утверждённое изменение — это намеренный коммит обновления эталона в том же PR, что и код, вызвавший его, — никогда не отдельный коммит «починить тесты», скрывающий суть изменений.

Что тестировать, а что оставить в покое?

Тестируйте компоненты и сценарии, где визуальное изменение обойдётся вам дорого — сломанная кнопка оформления заказа, свернувшаяся навигация, компонент дизайн-системы, используемый на 40 страницах; пропускайте высокодинамичные дашборды и виджеты третьих сторон, где контент закономерно меняется при каждом рендеринге. Стоимость визуального теста — время на проверку каждого расхождения, поэтому тратьте его там, где регрессия обходится дорого, а рендеринг стабилен.

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

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

Визуальные регрессионные тесты валидируют UI относительно управляемых эталонов в CI, но продакшн рендерится на реальных устройствах, с неконтролируемыми viewport’ами, шрифтами, которые могут не загрузиться, и сторонними скриптами, вызывающими рефлоу страницы — session replay реконструирует и воспроизводит записанную сессию, поэтому проблема с вёрсткой, проскользнувшая мимо вашего набора тестов, проявляется в записях затронутых сессий.

Ваша тестовая матрица фиксирует один viewport и один браузер; пользователь на ноутбуке с разрешением 1366×768, с веб-шрифтом, загрузка которого завершилась таймаутом, и с локалью, удлинившей все подписи, сталкивается с вёрсткой, которую ваши эталоны никогда не описывали. Визуальное тестирование — это предотвращение на границе PR; session replay — это обнаружение на границе продакшна. Они покрывают разные режимы отказов и дополняют, а не дублируют друг друга.

Как выбрать инструмент для визуального регрессионного тестирования?

Инструменты визуального регрессионного тестирования делятся на три категории — встроенные в фреймворки, облачные сервисы и самостоятельно размещаемые инструменты — и выбор определяется потребностями команды, а не списком функций. Встроенные в фреймворки инструменты — Playwright и Vitest browser mode — бесплатны, работают в вашем существующем CI и хранят эталоны в вашем репозитории; компромисс в том, что управление эталонами и межсредовой согласованностью лежит на вас. Облачные сервисы, такие как Percy и Chromatic, берут на себя хранение эталонов и рабочие процессы проверки, предлагая бесплатные тарифы (Percy включает 5 000 скриншотов в месяц; платные планы Chromatic начинаются от $179/месяц), ценой внешней зависимости. Самостоятельно размещаемые варианты, такие как BackstopJS, держат всё внутри компании с бо́льшими накладными расходами на конфигурацию. Для команды, уже использующей Playwright или Vitest в CI, нативный путь не требует дополнительных затрат и является достаточным при условии контроля нестабильности на ранних этапах.

Нативный для фреймворка путь закрывает класс ошибок, которые функциональные тесты структурно не замечают, без новых вендоров и регулярных расходов. Начните с добавления одного утверждения toHaveScreenshot() к вашему наиболее критичному компоненту — кнопке оформления заказа, основной навигации — сгенерируйте эталон внутри того же контейнера, который использует ваш CI, и воспринимайте первое упавшее расхождение как код-ревью, а не как тест, который нужно заглушить.

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

В чём разница между визуальным регрессионным тестированием и снапшот-тестированием?

Снапшот-тестирование сериализует отрендеренную разметку компонента в текст и сравнивает эту строку, тогда как визуальное регрессионное тестирование сравнивает фактические отрендеренные пиксели скриншота. Снапшоты на уровне разметки не замечают ничего сугубо визуального — изменения z-index, замены цветового токена или резервного шрифта — поскольку сериализованный DOM остаётся идентичным, даже когда то, что видит пользователь, изменилось. Визуальное регрессионное тестирование выявляет ошибки только рендеринга, которые снапшот-сравнения структурно не могут обнаружить.

Почему мой визуальный тест проходит локально, но падает в CI?

Рендеринг шрифтов и субпиксельное сглаживание различаются между операционными системами, поэтому эталон, захваченный на ноутбуке с macOS, даёт ложные срабатывания при сравнении со скриншотом из раннера GitHub Actions на Ubuntu. Решение — генерировать эталоны внутри того же зафиксированного контейнера, который использует ваш CI, например официального Docker-образа mcr.microsoft.com/playwright:v1.61.0-noble, а не фиксировать эталоны, захваченные на другой ОС. Кросс-средовые эталоны — наиболее распространённый источник расхождений только в CI.

Нужен ли мне платный сервис вроде Percy или Applitools для визуального регрессионного тестирования?

Нет. Playwright поставляется со встроенным утверждением для сравнения скриншотов через expect(page).toHaveScreenshot(), а Vitest 4.x добавил встроенное визуальное регрессионное тестирование через toMatchScreenshot() в стабильном Browser Mode — оба бесплатны и работают в вашем существующем CI с эталонами, хранящимися в репозитории. Платные сервисы, такие как Percy и Chromatic, добавляют хранение эталонов и размещённые рабочие процессы проверки, а Applitools добавляет сравнение на основе ИИ, но встроенного в фреймворки попиксельного сравнения с настраиваемыми порогами достаточно для большинства команд при условии контроля нестабильности на ранних этапах.

Следует ли захватывать всю страницу или отдельный компонент в визуальном тесте?

По возможности ограничивайте скриншот конкретным локатором элемента, а не всей страницей. Захват на уровне компонента сужает область сравнения, снижает шум, ускоряет проверку и позволяет избежать необходимости обновлять эталон всей страницы при каждом несвязанном изменении. Собственная документация Vitest помечает захват всей страницы как антипаттерн. Оставьте захват на уровне страницы для критических end-to-end сценариев, таких как оформление заказа, где важны взаимодействия вёрстки между элементами, и используйте тесты на уровне компонентов для элементов дизайн-системы и изолированного UI.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.