Функциональные тесты проверяют бизнес-логику и наличие элементов в DOM, но бессильны перед визуальными дефектами: выпадение текста за пределы контейнера (overflow: hidden), наложение модального окна на кнопку из-за сломанного z-index или сдвиг сетки CSS Grid на 4 пикселя. Промышленный стандарт — визуальное регрессионное тестирование в Playwright с использованием метода toHaveScreenshot(). Чтобы тесты не падали из-за различий рендеринга шрифтов на macOS, Windows и Linux, прогон визуальных тестов стандартизируется в официальных контейнерах Docker.
[ Эталонный скриншот ] [ Текущий снимок страницы ]
(Golden Baseline) (page.screenshot)
│ │
└─────────────────┬─────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Playwright Pixel-by-Pixel Diff Engine (pixelmatch) │
│ - Применение масок (mask: ['.timestamp', '.avatar']) │
│ - Сравнение по цветовому порогу threshold: 0.2 │
│ - Расчет отношения несовпадающих пикселей │
└───────────────────────────┬─────────────────────────────┘
│
┌────────────────┴────────────────┐
▼ ▼
Diff <= maxDiffPixelRatio Diff > maxDiffPixelRatio
[ SUCCESS ] [ FAILURE ]
Тест успешно пройден Генерация visual-diff.png
Блокировка merge в CI/CDНастройка snapshotPathTemplate, порогов threshold (0.2) и допустимого расхождения maxDiffPixelRatio (0.01).
Использование animations: 'disabled' и caret: 'hide' для предотвращения мерцания мигающего курсора и переходов CSS.
Передача локаторов mask: [page.getByTestId('dynamic-time')] для заливки нестатичных зон сплошным цветом.
Создание npm-скриптов test:visual через docker run mcr.microsoft.com/playwright для генерации эталонов на Linux.
Пайплайн проверки визуальной регрессии с выгрузкой артефактов HTML-отчета при обнаружении несовпадений.
Разработчик сделал эталонные скриншоты тестов на своем рабочем ноутбуке с macOS, закомитил их и отправил в репозиторий. Пайплайн в GitHub Actions (запущенный на виртуальной машине ubuntu-latest) упал с ошибкой различия 3% пикселей. В чем фундаментальная причина сбоя?
Playwright запускает Chromium в Docker с фиксированным разрешением экрана (viewport: 1280x720) и отключенными аппаратными шрифтовыми ускорениями.
Исключаются любые вариации между локальными машинами разработчиков.
Тест ожидает полной загрузки кастомных веб-шрифтов через document.fonts.ready и стабилизации сетевых запросов.
Предотвращает снимок в момент замены системного шрифта (FOUT).
Playwright автоматически скрывает курсор мыши, замораживает CSS-анимации и заливает маскируемые элементы розовым боксом.
Снимок отражает только детерминированную визуальную верстку.
При обнаружении расхождений генерируется визуальный трехпанельный отчет (Expected | Actual | Diff) с выгрузкой в Pull Request.
Разработчик визуально видит каждый сместившийся пиксель.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
snapshotDir: './e2e/__snapshots__',
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.005, // Допускается расхождение не более 0.5% пикселей
threshold: 0.2, // Цветовой порог чувствительности (0 - строго, 1 - свободно)
animations: 'disabled', // Автоматическая заморозка всех CSS и GIF анимаций
caret: 'hide', // Скрытие мигающего текстового курсора
scale: 'css', // Нормализация DPI экранов Retina к стандартному CSS-пикселю
},
},
use: {
baseURL: 'http://localhost:3000',
viewport: { width: 1280, height: 720 },
trace: 'on-first-retry',
},
projects: [
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 14'] },
},
],
});import { test, expect } from '@playwright/test';
test.describe('Визуальный регрессионный аудит страницы тарифов', () => {
test('страница тарифов соответствует эталонному снимку на десктопе', async ({ page }) => {
await page.goto('/pricing');
// Ожидаем полную загрузку всех веб-шрифтов
await page.evaluate(() => document.fonts.ready);
// Маскируем динамические блоки: таймер обратного отсчета акции и дату
const countdownTimer = page.locator('[data-testid="countdown-timer"]');
const dynamicTimestamp = page.locator('[data-testid="page-timestamp"]');
// Делаем снимок всей прокручиваемой страницы
await expect(page).toHaveScreenshot('pricing-page-desktop.png', {
fullPage: true,
mask: [countdownTimer, dynamicTimestamp],
maskColor: '#8b5cf6', // Заливка маски акцентным цветом
});
});
test('модальное окно подтверждения подписки центрировано и корректно отрисовано', async ({ page }) => {
await page.goto('/pricing');
await page.getByRole('button', { name: /выбрать тариф pro/i }).click();
const modalDialog = page.getByRole('dialog', { name: /подтверждение заказа/i });
await expect(modalDialog).toBeVisible();
// Снимок только конкретного компонента модального окна (Component Screenshot)
await expect(modalDialog).toHaveScreenshot('checkout-modal.png');
});
});name: Visual Regression Quality Gate
on:
pull_request:
branches: [main]
jobs:
visual-tests:
runs-on: ubuntu-latest
# Запуск внутри официального Docker-контейнера Playwright для стандартизации шрифтов
container:
image: mcr.microsoft.com/playwright:v1.49.0-jammy
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Build Application
run: pnpm build
- name: Start Production Server
run: pnpm preview --port 3000 &
env:
NODE_ENV: production
- name: Run Playwright Visual Tests
run: pnpm exec playwright test e2e/visual
- name: Upload Visual Diff Artifacts on Failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-diff-report
path: playwright-report/
retention-days: 7Если один разработчик команды запускает playwright test --update-snapshots на macOS, а второй — на Windows, репозиторий превращается в бесконечную войну коммитов скриншотов. Внедрите правило: обновление эталонов производится строго через Docker: docker run --rm -v $(pwd):/work -w /work mcr.microsoft.com/playwright pnpm test --update-snapshots.
Если вызвать toHaveScreenshot() до разрешения промиса document.fonts.ready, браузер сделает снимок с системным шрифтом (Arial/Times), а через 50 мс подгрузит интер-шрифт (Inter). Это вызовет случайные сбои в 20% прогонов.
При параметре fullPage: true Playwright делает серию прокруток страницы и сшивает полосы. Если на сайте подключен CSS scroll-behavior: smooth или параллакс-скрипты, скриншот получится размытым со смещенными слоями. Отключайте скролл-эффекты перед тестом.
На посадочной странице отображается вращающийся 3D-глобус на WebGL и блок последних новостей со случайным порядком. Как гарантировать стабильность визуального теста?