Визуальное регрессионное тестирование в Docker


В нескольких проектах, где я работал, пробовали внедрить скриншот-тестирование компонентов. Принцип работы такой:

  1. меняем компонент
  2. делаем эталонный скриншот, сохраняем в репе
  3. повторяем на всех компонентах
  4. при повторных изменениях запускаем тесты, в которых сравниваются эталонные скриншоты и текущее отображение компонентов
  5. если все расхождения запланированные, меняем эталонные скрины; если есть незапланированные расхождения, поздравляю, тесты нашли регресс!

Такие тесты хорошо подходят для теста вёрстки элементов ui-kit-а (многочисленные состояниях ховера, фокуса, разные режимы компонента) или же каких-то критичных компонентов (форма логина, чекаут), которые никак нельзя случайно поломать.

Снепшотные тесты, юниты, e2e тут не помогут, нужно именно сравнение скринов. Звучит хорошо, но есть одно НО. Во всех случаях на моей практике дело упиралось в то, что скриншоты снимаются разными разработчиками на разных машинах с разными ОС. И в зависимости от этого скриншоты начинают расходиться. Шрифты другие. Субпиксельный рендеринг другой. Антиалиасинг другой. Также отличаются элементы форм. MacOS рендерит шрифты и формы иначе, чем Ubuntu. И тест падает, хотя визуально всё ок. Вместо помощи тесты начинают мешать и бесить.

Логично было бы предположить делать скриншоты на одной машине, например, вынести этот процесс на CI/CD или на отдельный сервер, но обычно с этим в компаниях сложно, нужно всё согласовывать, заводить, что есть заморочь.

И тут изучая Docker открыл для себя идеально подходящую под кейс фичу! Через Docker можно связать файловую систему хоста (ваша машина) с файловой системой запущенного контейнера. То есть храним скрины по прежнему в репо (развёрнутом у каждого разработчика), монтируем папку с ними внутрь Docker-контейнера, снимаем скриншоты, сравниваем, при необходимости обновляем. Внутри контейнера один и тот же образ с фиксированной ОС, фиксированной версией браузера, фиксированными шрифтами. И соотвественно таким же макаром запускается в CI.

Собрал пруф оф концепт — github.com/juwain/playwright-docker-screenshot-testing.

Что там вкратце, происходит: Storybook изолирует компоненты, Playwright делает скриншоты, Docker обеспечивает одинаковый рендеринг.

Оркестрация через docker-compose.yml (именно там указывается, какие локальные папки монтируются в контейнер):

services:
visual-tests:
build: .
volumes:
# Persist screenshots - accessible from host
- ./__screenshots__:/app/__screenshots__
# HTML report for visual diff
- ./html-report:/app/html-report
# Test results (traces, screenshots on failure)
- ./test-results:/app/test-results
# Source files for quick iteration (read-only)
- ./src:/app/src:ro
environment:
- CI=true
command: sh -c "npm run storybook -- --host 0.0.0.0 & sleep 15 && npx playwright test --project=visual"

Docker-образ с Playwright официальный от Microsoft уже содержит браузеры и все зависимости.

FROM mcr.microsoft.com/playwright:v1.48.0-noble

Каждый визуальный тест — это три действия: открыть стори, дождаться рендера, сравнить скриншот. Storybook рендерит в статическом режиме каждый отдельный компонент по адресу /iframe.html?id=${storyId}&viewMode=story, ждём пока загрузятся картинки и шрифты, можно снимать скриншот Playwright-ом и сравнивать тоже им.

Пример теста кнопки:

test.describe('Button', () => {
test('hover', async ({ page }) => {
const component = await gotoStory(page, 'design-system-button--primary');
const button = component.locator('button.button').first();
await button.hover();
await expect(component).toHaveScreenshot();
});
});

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

toHaveScreenshot: {
maxDiffPixels: 100,
animations: "disabled",
caret: "hide",
},

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

Отдельными cli-командами скрины апрувятся и обновляются.

{
"scripts": {
"test:visual": "docker compose up --build",
"test:visual:update": "playwright test --project=visual --update-snapshots",
"test:visual:approve": "playwright test --project=visual --last-failed --update-snapshots",
"test:visual:report": "playwright show-report html-report"
}
}