Класична історія. Дизайнер попросив підняти space-2 з 8 на 10 пікселів, бо кнопка виглядала затісною. Розробник змінив одне значення в токенах, тести зелені, TypeScript щасливий, PR змержили. Через тиждень приходить продуктова команда і каже, що в мобільному чекауті ціна тепер налазить на кнопку, а в таблиці транзакцій зникло два рядки на екран.
Жоден юніт-тест такого не спіймає. Компілятор теж. Компонент рендериться, пропси правильні, функція повертає що треба. Просто виглядає воно тепер інакше в дванадцяти місцях, про які ніхто не подумав.
Візуальні регресійні тести існують рівно для цього: вони порівнюють скріншот компонента до і після зміни, і показують різницю прямо в PR. У статті про версіонування дизайн-системи я згадував їх одним рядком. Тут розберемо, як реально їх поставити і не кинути через два тижні.
Що саме ловить візуальний тест, а що ні
Механіка проста. Ви один раз робите baseline: набір еталонних скріншотів усіх компонентів у всіх варіантах. На кожному наступному PR система рендерить ті самі компоненти заново і порівнює пікселі з baseline. Є різниця - тест падає і показує три картинки: було, стало, підсвічений диф.
Ловить добре: зміни токенів, які розповзлися далі, ніж очікували. Зламану верстку від оновлення бібліотеки. Компонент, який поїхав від довгого тексту. Зникле focus-кільце. Різницю між світлою і темною темою. Все, що ви побачили б очима, якби мали час відкрити всі 300 екранів.
Не ловить: логіку, доступність з погляду скрінрідера, порядок табуляції, продуктивність. Візуальний тест не знає, чи правильна кнопка. Він знає тільки, що вона стала на два пікселі нижче. Тому це доповнення до інтеракційних тестів і Design QA, а не заміна.
Головна користь не в тому, що тест ловить баги. Вона в тому, що диф стає частиною код-рев'ю. Розробник більше не може змінити токен і не побачити наслідків - CI покаже йому всі 12 екранів до того, як він натисне merge.
Мінімальний сетап за один вечір
Якщо у вас уже є Storybook, ви за годину отримаєте робочі тести. Якщо Storybook немає, спершу він: візуальні тести без нього доведеться писати руками для кожного стану.
- Переконайтеся, що кожен компонент має стори на ключові варіанти. Тест бачить рівно те, що є в сторі. Немає сторі на
disabled- ніхто не помітить, коли він зламається. - Заведіть проєкт на Chromatic і візьміть токен.
- Додайте пакет:
npm install --save-dev chromatic. - Перший прогін:
npx chromatic --project-token=<токен>. Він збілдить Storybook, зніме скріншоти всіх сторі й запропонує прийняти їх як baseline. - Додайте той самий рядок у 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, але одразу закладайте час на стабілізацію.
Чеклист на перший тиждень
- Пройтися по базових компонентах і дописати сторі на стани, яких бракує.
- Вимкнути анімації, заморозити дати, замокати мережу в тестовому режимі.
- Прогнати перший білд локально й прийняти baseline.
- Порахувати снапшоти на місяць за формулою вище і увімкнути TurboSnap.
- Додати крок у CI на PR, токен - у секрети.
- Домовитись у команді, хто дивиться дифи поза межами задачі.
- Прожити тиждень і виписати всі хибні спрацювання. Полагодити їх до того, як вони стануть звичкою.
Візуальні тести не роблять дизайн-систему кращою. Вони роблять її передбачуваною: зміна токена перестає бути лотереєю, а рев'ю перестає покладатися на те, що хтось згадає перевірити чекаут. Для команди, де дизайнер і розробник ділять одну систему, це найдешевша страховка з усіх можливих.
Якщо хочеться подивитись, як це виглядає в живому проєкті, я розбираю подібні речі на YouTube-каналі UX Hero.