Перейти к содержимому

22. Visual Testing

Visual Testing

Visual testing сравнивает скриншоты: при первом запуске создаётся эталон, при последующих — сравнивается с ним.

// Весь viewport
await expect(page).toHaveScreenshot('homepage.png');
// Отдельный элемент
await expect(page.locator('.hero-section')).toHaveScreenshot('hero.png');
// С настройками
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 100, // допустимое число отличающихся пикселей
threshold: 0.2, // порог разницы (0-1)
animations: 'disabled', // отключить анимации
mask: [page.locator('.timestamp')], // скрыть динамические элементы
});
Окно терминала
# Создать эталонные скриншоты
npx playwright test --update-snapshots
# Эталоны сохраняются в:
# tests/e2e/__snapshots__/homepage.spec.ts-snapshots/
// Скрыть элементы с динамическим контентом
await expect(page).toHaveScreenshot('page.png', {
mask: [
page.locator('.timestamp'),
page.locator('.user-avatar'),
page.locator('[data-dynamic]'),
],
maskColor: '#FF00FF', // цвет маски (для отладки)
});
// Стабилизировать перед скриншотом
await page.evaluate(() => {
// Остановить анимации
document.querySelectorAll('*').forEach(el => {
(el as HTMLElement).style.animationPlayState = 'paused';
});
});
await expect(page).toHaveScreenshot('stable.png');
await expect(page).toHaveScreenshot('full-page.png', {
fullPage: true, // весь контент, не только viewport
});
Окно терминала
npx playwright show-report

В отчёте видно:

  • Эталон
  • Актуальный скриншот
  • Diff (разница выделена красным)
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';
test('visual comparison', async ({ page }) => {
await page.goto('/dashboard');
const screenshot = await page.screenshot();
const baseline = fs.readFileSync('./baselines/dashboard.png');
const img1 = PNG.sync.read(baseline);
const img2 = PNG.sync.read(screenshot);
const diff = new PNG({ width: img1.width, height: img1.height });
const numDiffPixels = pixelmatch(
img1.data, img2.data, diff.data,
img1.width, img1.height,
{ threshold: 0.1 }
);
expect(numDiffPixels).toBeLessThan(100);
});
const viewports = [
{ width: 320, height: 568, name: 'mobile' },
{ width: 768, height: 1024, name: 'tablet' },
{ width: 1440, height: 900, name: 'desktop' },
];
for (const viewport of viewports) {
test(`looks correct on ${viewport.name}`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('/');
await expect(page).toHaveScreenshot(`home-${viewport.name}.png`);
});
}
// Или через playwright.config.ts projects
projects: [
{
name: 'Mobile',
use: { ...devices['iPhone 12'] },
},
{
name: 'Desktop',
use: { viewport: { width: 1440, height: 900 } },
},
],
// Визуальное тестирование UI компонентов в Storybook
// npx playwright test --config=storybook.config.ts
import { test, expect } from '@playwright/test';
test('Button component visual', async ({ page }) => {
await page.goto('http://localhost:6006/iframe.html?id=button--primary');
await expect(page.locator('#storybook-root')).toHaveScreenshot('button-primary.png');
});
// 1. Отключить анимации в конфиге
export default defineConfig({
use: {
animations: 'disabled',
},
});
// 2. Очищать динамические данные перед скриншотом
await page.evaluate(() => {
document.querySelectorAll('.timestamp').forEach(el => {
el.textContent = 'Jan 1, 2024';
});
});
// 3. Подождать стабильность
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500); // иногда нужно
  1. Добавь visual тесты для главной страницы и страницы товаров
  2. Настрой маскирование для timestamp и avatars
  3. Проверь responsive дизайн на трёх разных разрешениях
  • Visual testing ловит неожиданные визуальные регрессии
  • toHaveScreenshot() — автоматическое сравнение с эталоном
  • mask — скрыть динамические элементы
  • —update-snapshots — обновить эталоны после изменений дизайна