Визуальная регрессия: почему попиксельное сравнение пропускает реальные изменения

Визуальная регрессия: почему попиксельное сравнение пропускает реальные изменения

Большинство open-source-инструментов визуальной регрессии — BackstopJS, Wraith или проверки скриншотов в Playwright и Cypress — опираются на одну и ту же идею: сделать скриншот до и после, а затем посчитать пиксели, которые изменились. Движок почти всегда один и тот же: сравнить два изображения пиксель за пикселем.

Это просто, надёжно и идеально для отлова крупной поломки вёрстки. Но у этого подхода есть структурный компромисс, который мало кто измеряет. Мы измерили — с цифрами в руках — и на случаях, выбранных, чтобы его обнажить, результат однозначен.

Две настройки, которые нельзя путать

Путаница повсюду, поэтому давайте проясним. У такого инструмента есть две разные настройки:

  1. Допуск на пиксель: при каком отличии цвета пиксель считается «по-настоящему» изменившимся. Это регулятор чувствительности к цвету, применяемый пиксель за пикселем.
  2. Порог решения: после подсчёта различающихся пикселей смотрят, какой процент изображения изменился, чтобы решить, проходит тест или падает. Эталонный инструмент BackstopJS задаёт его по умолчанию на уровне 0,1 % пикселей.

Наш тест следует именно этой логике: мы считаем различающиеся пиксели (инструмент на чувствительности по умолчанию), затем применяем порог решения 0,1 %, чтобы сказать «проходит» или «падает».

Дилемма порога

Этот процентный порог запирает сравнение всего изображения в компромисс, который он не может выиграть:

  • С процентным порогом (настройка по умолчанию, 0,1 %) → вы пропускаете мелкие реальные изменения. Кнопка, сменившая цвет, скруглившаяся граница, статус ячейки «OK» → «ОШИБКА»: всё это весит ничтожную долю пикселей. Ниже 0,1 % тест становится зелёным, хотя страница заметно изменилась. Реальное изменение остаётся незамеченным.
  • Совсем без допуска (другая крайность, настройка Playwright по умолчанию) → вы падаете на малейшем пикселе отличия, включая обычное сглаживание контуров — те полупрозрачные пиксели по краям букв и форм, которые слегка меняются от одной отрисовки к другой. Результат: ложные тревоги пачками, и команда в итоге игнорирует инструмент.

Настоящие инструменты предлагают предохранители — маскировать зоны, игнорировать области, обрезать. Они работают, но их нужно настраивать заранее, вручную, зона за зоной. Локализованная чувствительность не автоматическая.

Тест в реальных условиях

Мы сравнили на точно одной и той же странице (отрисованной браузером, зафиксированной воспроизводимо, ширина экрана 1280 px, захват всей страницы) два подхода:

  • Delta-QA: наш компаратор, который сравнивает элемент за элементом (он сопоставляет элементы между двумя версиями и сравнивает пиксели только на самом тонком уровне);
  • сравнение всего изображения: мы сравниваем два скриншота пиксель за пикселем, затем применяем порог решения 0,1 %.

Пять случаев, выбранных, чтобы обнажить слепое пятно:

Случай Изменение Delta-QA (элемент за элементом) Сравнение всего изображения (% изменённых пикселей, вердикт при пороге 0,1 %)
Перемещение треугольник перемещается по странице 2 сигнала: исчез + появился на элементе 0,005 % → НЕ ОБНАРУЖЕНО
Перестановка карточки и меню переставлены 13 локализованных сигналов (какие карточки, какие пункты) 0,63 % → падает, но размытое пятно, без уточнений
Тонкое изменение граница скруглена с 12 px до 30 px 1 сигнал на нужной карточке 0,011 % → НЕ ОБНАРУЖЕНО
Локальный цвет заголовок карточки зелёный → фиолетовый нужный заголовок (интенсивность 0,996) + 2 слабых родительских сигнала 5,03 % → падает, но размытое пятно, без уточнений
Ячейка таблицы статус строки FAIL → WARN 2 сильных сигнала на нужных строках 0,036 % → НЕ ОБНАРУЖЕНО

В этих пяти случаях три реальных изменения — перемещение элемента, скругление границы, статус ячейки — оказываются ниже стандартного порога решения 0,1 %. Инструмент с настройками по умолчанию объявляет их «без изменений». А ведь это именно те регрессии, которые визуальный тест должен ловить.

Для двух случаев, которые попиксельное сравнение всё же «обнаруживает» (перестановка, цвет), оно говорит лишь одно: «X % пикселей где-то изменились». Никакого понятия о том, какой элемент и какого рода изменение. Delta-QA же называет точный элемент и квалифицирует изменение (перемещён, добавлен, удалён, изменён).

Почему уровень элементов меняет всё

Delta-QA не сравнивает одно большое изображение. Он:

  1. перестраивает дерево элементов страницы;
  2. сопоставляет каждый элемент между двумя версиями (по его содержимому, затем позиции);
  3. сравнивает пиксели только на самом тонком уровне и обнаруживает собственные изменения блока, игнорируя зоны его уже изменённых под-элементов;
  4. исключает сглаживание контуров из подсчёта действительно различающихся пикселей.

Следствие: он может быть очень чувствительным (поймать границу в 1 px на большом блоке), не утопая в вариациях сглаживания, потому что этот шум исключён, а каждый сигнал привязан к конкретному элементу. Перемещение — это не «красное пятно»: это элемент, помеченный как исчез на старом месте и появился на новом. Локализованная чувствительность автоматическая, без масок, которые нужно готовить заранее.

Методология — и её пределы

Мы дорожим строгостью, поэтому вот тёмная сторона:

  • Одна и та же страница для обоих. Оба подхода исходят из точно одной и той же отрисованной и зафиксированной страницы — без перекоса отображения.
  • Цифры проверены против эталонного инструмента. Наш стенд пересчитывает отличие цвета так же, как самый используемый инструмент попиксельного сравнения. Мы сверили 5 случаев с этим официальным инструментом: на резком изменении цвета оба дают 5,036 % против 5,034 % — почти идентично. В остальных случаях эталонный инструмент считает ещё меньше пикселей (он игнорирует сглаживание контуров) — то есть он ещё более склонен пропускать мелкие изменения. Цифры в таблице — его собственные.
  • Delta-QA пересигналит (и мы это признаём). На изменении цвета он выдаёт 3 сигнала: настоящий (заголовок, интенсивность 0,996) + 2 очень слабых родительских сигнала (0,005 и 0,001). Это намеренно: мы поднимаем всё, а ползунок чувствительности интерфейса скрывает эти слабые сигналы по умолчанию. Но скажем прямо: сырой подсчёт — это не «1 изменение = 1 сигнал».
  • Один контекст теста. Эти измерения сделаны на одном размере экрана, странице в покое, на контролируемых тестовых страницах. Мы ничего не утверждаем про несколько размеров экрана, интерактивные состояния (наведение, фокус) или по-настоящему шумные страницы — другие задачи.
  • Перестановка. Delta-QA классифицировал переставленные карточки как «изменённые», а не «перемещённые», но локализованно по элементам — что всё равно намного выше размытого пятна.

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

Что нужно запомнить

Если ваш тест визуальной регрессии опирается на сравнение всего изображения с процентным порогом в настройке по умолчанию, он скорее всего пропускает изменения, которые видят ваши пользователи — перемещения, локальные цвета, микро-изменения стиля. Снижение порога ловит их, но будит ложные тревоги; маски помогают, но настраиваются зона за зоной, заранее.

Сравнение элемент за элементом — это не настройка: это другая архитектура, которая возвращает одновременно чувствительность и точность — а в придачу имя элемента и характер изменения.

Что почитать дальше


Воспроизводимый тест: сравнение «всего изображения» сделано эталонным open-source-инструментом (пакет pixelmatch, Node/npm) на его чувствительности по умолчанию, затем с порогом решения 0,1 % как у BackstopJS — на точно той же зафиксированной странице, что и Delta-QA.