Класична історія. Дизайнер попросив підняти space-2 з 8 на 10 пікселів, бо кнопка виглядала затісною. Розробник змінив одне значення в токенах, тести зелені, TypeScript щасливий, PR змержили. Через тиждень приходить продуктова команда і каже, що в мобільному чекауті ціна тепер налазить на кнопку, а в таблиці транзакцій зникло два рядки на екран.

Жоден юніт-тест такого не спіймає. Компілятор теж. Компонент рендериться, пропси правильні, функція повертає що треба. Просто виглядає воно тепер інакше в дванадцяти місцях, про які ніхто не подумав.

Візуальні регресійні тести існують рівно для цього: вони порівнюють скріншот компонента до і після зміни, і показують різницю прямо в PR. У статті про версіонування дизайн-системи я згадував їх одним рядком. Тут розберемо, як реально їх поставити і не кинути через два тижні.

Що саме ловить візуальний тест, а що ні

Механіка проста. Ви один раз робите baseline: набір еталонних скріншотів усіх компонентів у всіх варіантах. На кожному наступному PR система рендерить ті самі компоненти заново і порівнює пікселі з baseline. Є різниця - тест падає і показує три картинки: було, стало, підсвічений диф.

Ловить добре: зміни токенів, які розповзлися далі, ніж очікували. Зламану верстку від оновлення бібліотеки. Компонент, який поїхав від довгого тексту. Зникле focus-кільце. Різницю між світлою і темною темою. Все, що ви побачили б очима, якби мали час відкрити всі 300 екранів.

Не ловить: логіку, доступність з погляду скрінрідера, порядок табуляції, продуктивність. Візуальний тест не знає, чи правильна кнопка. Він знає тільки, що вона стала на два пікселі нижче. Тому це доповнення до інтеракційних тестів і Design QA, а не заміна.

Головна користь не в тому, що тест ловить баги. Вона в тому, що диф стає частиною код-рев'ю. Розробник більше не може змінити токен і не побачити наслідків - CI покаже йому всі 12 екранів до того, як він натисне merge.

Мінімальний сетап за один вечір

Якщо у вас уже є Storybook, ви за годину отримаєте робочі тести. Якщо Storybook немає, спершу він: візуальні тести без нього доведеться писати руками для кожного стану.

  1. Переконайтеся, що кожен компонент має стори на ключові варіанти. Тест бачить рівно те, що є в сторі. Немає сторі на disabled - ніхто не помітить, коли він зламається.
  2. Заведіть проєкт на Chromatic і візьміть токен.
  3. Додайте пакет: npm install --save-dev chromatic.
  4. Перший прогін: npx chromatic --project-token=<токен>. Він збілдить Storybook, зніме скріншоти всіх сторі й запропонує прийняти їх як baseline.
  5. Додайте той самий рядок у GitHub Actions на кожен PR і збережіть токен у секретах репозиторію.

Усе. З цього моменту кожен PR отримує коментар зі списком змінених компонентів і посиланням на рев'ю дифів. Приймаєте зміну - вона стає новим baseline. Відхиляєте - PR червоний.

Порахуйте снапшоти до того, як увімкнете CI

Це те, на чому команди обпікаються найчастіше. Chromatic на безкоштовному тарифі дає 5 000 снапшотів на місяць, і закінчуються вони швидше, ніж здається.

Рахунок такий: кількість сторі × кількість вьюпортів × кількість тем × кількість білдів. Сорок компонентів по шість сторі в кожному - це 240 сторі. Дві теми і два вьюпорти дають 960 снапшотів за один білд. П'ять PR за тиждень - і ви вигорили місячний ліміт за шість днів. Далі або платний тариф (він починається зі $179 на місяць), або тести просто ставляться на паузу до наступного місяця.

Що з цим робити:

  • Увімкніть TurboSnap. Він аналізує, які файли змінилися в комміті, і знімає тільки ті сторі, яких зміна реально торкнулася. На PR, де правили один компонент, це різниця між 960 і 12 снапшотами.
  • Не множте вьюпорти без потреби. Два вьюпорти замість чотирьох - це мінус половина рахунку. Мобільний і десктоп покривають більшість регресій.
  • Другу тему знімайте не для всього. Достатньо базових компонентів, де кольори реально прив'язані до токенів теми.
  • Не ганяйте білд на кожен пуш. Один прогін на PR і один на мердж у main.

Головна причина, чому такі тести кидають: шум

Тест падає, розробник відкриває диф, а там зсув на один піксель через те, що шрифт довантажився пізніше. Наступного разу падає через анімацію, спійману на середині. Потім через дату "3 хвилини тому" в картці. Після п'ятого хибного спрацювання команда починає тиснути "прийняти все" не дивлячись, і тести перетворюються на ритуал.

Тому шум треба вбити на старті, а не колись потім:

  • Анімації. Вимкніть їх у тестовому режимі глобально через prefers-reduced-motion або опцію pauseAnimationAtEnd. Скріншот має ловити кінцевий стан, а не випадковий кадр.
  • Дати й рандом. Заморозьте час і сідніть генератор. "Сьогодні о 14:32" у сторі має бути захардкоджене значення, а не new Date().
  • Дані з мережі. Сторі не повинна ходити в API. Мокайте відповіді, інакше кожен деплой бекенду ламатиме вам візуальні тести.
  • Шрифти. Переконайтеся, що збірка чекає на завантаження шрифтів перед знімком, інакше половина дифів буде про підміну гарнітури.
  • Аватарки й зображення з CDN. Кладіть локальні заглушки.

Правило просте: якщо ви двічі побачили диф, який нічого не означає, зупиніться і почините джерело шуму. Один вечір на це економить місяці довіри до тестів.

Які сторі покривати, а які ні

Спокуса зняти все дуже велика, але покриття 100% тут працює проти вас: більше снапшотів означає більше шуму, довший CI і швидше вигорілий ліміт.

Робочий мінімум виглядає так. Обов'язково покриваються всі базові компоненти дизайн-системи в їхніх ключових станах: default, hover, focus, disabled, error, loading. Далі - складені патерни, які найчастіше ламаються: форма, таблиця, картка, модалка, порожній стан. І окремо - два-три реальні екрани цілком, бо саме на композиції видно проблеми, яких не видно на ізольованому компоненті.

Не покривайте візуально те, що змінюється щотижня за продуктовою логікою. Маркетинговий блок, який редагують у CMS, буде давати червоне щоразу, і сенсу в цьому нуль.

Хто аппрувить дифи

Технічна частина простіша за організаційну. Диф - це питання "ця зміна навмисна?", і відповісти на нього не завжди може автор PR.

Найздоровіший розподіл: розробник дивиться дифи у своєму PR і приймає очевидне (він сам змінив padding, він бачить, що змінився padding). Але якщо диф зачепив компоненти поза межами задачі, підключається дизайнер або мейнтейнер дизайн-системи. Саме тут візуальні тести починають окупатись: дизайнер вперше бачить наслідки зміни токена до релізу, а не після.

І окреме правило: масове прийняття дифів без перегляду забороняється. Якщо змін занадто багато, щоб їх переглянути, це сигнал розбити PR, а не швидше клікати.

Playwright замість Chromatic: коли це виправдано

Chromatic зручний, але це зовнішній сервіс і гроші за снапшоти. Альтернатива - робити скріншоти самому через Playwright: await expect(page).toHaveScreenshot() порівнює знімок з еталоном, який лежить у вас у репозиторії.

Плюси: безкоштовно, все всередині, повний контроль. Мінуси серйозніші, ніж здається: скріншоти, зняті на macOS і в Linux-контейнері CI, відрізняються на рівні згладжування шрифтів, тому генерувати baseline треба в Docker; еталони лежать у гіті й розпухають; і не буде UI для рев'ю дифів - тільки картинки в артефактах збірки.

Є і третій шлях: Storybook 9 і 10 дозволяють ганяти сторі як тести через @storybook/addon-vitest у справжньому браузері (під капотом там той самий Playwright), а візуальні знімки додати аддоном на кшталт storybook-addon-vis. Це компроміс: свій CI, але вся конфігурація сторі перевикористовується.

Практична порада: якщо у вас команда до десяти людей і немає виділеного інфраструктурника - беріть Chromatic з TurboSnap і не витрачайте тиждень на Docker з шрифтами. Якщо є жорсткі вимоги не віддавати збірку назовні - Playwright, але одразу закладайте час на стабілізацію.

Чеклист на перший тиждень

  1. Пройтися по базових компонентах і дописати сторі на стани, яких бракує.
  2. Вимкнути анімації, заморозити дати, замокати мережу в тестовому режимі.
  3. Прогнати перший білд локально й прийняти baseline.
  4. Порахувати снапшоти на місяць за формулою вище і увімкнути TurboSnap.
  5. Додати крок у CI на PR, токен - у секрети.
  6. Домовитись у команді, хто дивиться дифи поза межами задачі.
  7. Прожити тиждень і виписати всі хибні спрацювання. Полагодити їх до того, як вони стануть звичкою.

Візуальні тести не роблять дизайн-систему кращою. Вони роблять її передбачуваною: зміна токена перестає бути лотереєю, а рев'ю перестає покладатися на те, що хтось згадає перевірити чекаут. Для команди, де дизайнер і розробник ділять одну систему, це найдешевша страховка з усіх можливих.

Якщо хочеться подивитись, як це виглядає в живому проєкті, я розбираю подібні речі на YouTube-каналі UX Hero.