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

Визуальное тестирование анимаций в JavaScript-библиотеках — одна из наиболее сложных областей автоматизации качества интерфейса. В случае Velocity.js сложность возрастает из-за высокой точности управления временем, плавностью интерполяции и работы через requestAnimationFrame, что делает поведение анимаций чувствительным к окружению выполнения тестов.

Основная проблема визуального тестирования анимаций заключается в недетерминированности времени исполнения. Даже одинаковый код Velocity.js может давать разные результаты в зависимости от:

  • нагрузки CPU во время выполнения тестов
  • особенностей браузерного движка
  • таймингов requestAnimationFrame
  • работы CI-окружения (Docker, виртуальные машины)
  • фоновых процессов системы

Velocity.js опирается на высокоточную интерполяцию значений во времени. Любое отклонение временного шага приводит к визуальным расхождениям, которые в классических snapshot-тестах интерпретируются как ошибка.

Изоляция времени как основа тестируемости

Ключевой подход к тестированию анимаций Velocity.js — контроль времени выполнения. Вместо реального времени используется его искусственное моделирование.

В тестовых средах применяются следующие техники:

  • подмена таймеров (setTimeout, setInterval)
  • управление performance.now()
  • фиксация requestAnimationFrame
  • использование виртуального времени

Пример с использованием Jest и fake timers:

jest.useFakeTimers();

Velocity(element, { opacity: 1 }, { duration: 1000 });

jest.advanceTimersByTime(500);
// проверка промежуточного состояния

jest.advanceTimersByTime(500);
// проверка финального состояния

Такой подход позволяет устранить фактор реального времени и добиться повторяемости результатов.

Контроль requestAnimationFrame

Velocity.js использует requestAnimationFrame как основной механизм обновления анимаций. В тестовой среде его необходимо стабилизировать.

Один из подходов — переопределение:

global.requestAnimationFrame = (cb) => {
  return setTimeout(() => cb(Date.now()), 16);
};

Более строгая версия — синхронный вызов:

global.requestAnimationFrame = (cb) => cb(performance.now());

Второй вариант делает анимацию мгновенной, что удобно для логического тестирования, но не подходит для визуальных проверок пиксельного уровня.

Проверка промежуточных состояний анимации

Velocity.js позволяет контролировать прогресс анимации через время. Это даёт возможность тестировать не только конечный результат, но и промежуточные кадры.

Типичный сценарий тестирования:

  1. Запуск анимации
  2. Переход на 25% времени
  3. Проверка значений стилей
  4. Переход на 50%
  5. Проверка состояния
  6. Переход к завершению
Velocity(element, { left: "100px" }, { duration: 1000 });

jest.advanceTimersByTime(250);
expect(element.style.left).toBe("25px");

jest.advanceTimersByTime(250);
expect(element.style.left).toBe("50px");

При этом важно учитывать, что Velocity.js может использовать easing-функции, из-за чего линейные ожидания не всегда корректны.

Учёт easing-функций при тестировании

Easing изменяет распределение значений во времени. Например, ease-in замедляет начало анимации, а ease-out — конец.

Это приводит к тому, что линейные проверки вида:

expect(value).toBe(progress * target);

становятся некорректными.

Вместо этого используется вычисление ожидаемого значения через ту же easing-функцию, что и в анимации:

import { easeInOutQuad } from "./easing";

const expected = easeInOutQuad(0.5) * 100;

Такой подход синхронизирует тестовую модель с реальной логикой Velocity.js.

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

Классический способ проверки визуального результата — сравнение скриншотов.

Velocity.js часто тестируется в связке с инструментами:

  • Playwright
  • Puppeteer
  • Cypress

Схема теста:

  1. запуск анимации
  2. фиксация состояния DOM
  3. создание скриншота
  4. сравнение с эталоном

Особенность анимаций — необходимость фиксации конкретного кадра:

await page.evaluate(() => {
  Velocity(document.querySelector(".box"), { opacity: 0.5 }, { duration: 1000 });
});

await page.waitForTimeout(500);
await page.screenshot({ path: "frame-500ms.png" });

Важный момент — стабильность рендеринга шрифтов, субпиксельных значений и антиалиасинга, которые могут давать ложные различия.

Проблема субпиксельной интерполяции

Velocity.js активно работает с дробными значениями, что приводит к субпиксельным координатам:

  • left: 33.333px
  • opacity: 0.6667
  • scale: 1.024

В разных браузерах эти значения могут округляться по-разному, что делает пиксельные сравнения нестабильными.

Для уменьшения шума применяются подходы:

  • округление значений перед проверкой
  • нормализация стилей
  • отключение сглаживания в тестовом окружении
const normalize = (value) => Math.round(parseFloat(value));

expect(normalize(element.style.left)).toBe(50);

Отключение анимаций в тестах

Часто визуальные тесты не требуют реальной анимации. Velocity.js позволяет переключить поведение на мгновенное выполнение.

Подходы:

  • установка duration: 0
  • глобальное переопределение Velocity
  • мокирование функции
Velocity.mock = (element, props) => {
  Object.assign(element.style, props);
};

В CI это значительно снижает флейки и ускоряет выполнение тестов.

Тестирование цепочек анимаций

Velocity.js поддерживает последовательные анимации:

Velocity(element, { opacity: 0 })
  .then(() => Velocity(element, { translateX: 100 }));

При тестировании цепочек важно учитывать:

  • порядок выполнения промисов
  • завершение предыдущей анимации
  • состояние DOM между шагами

Проверка может выглядеть так:

await Velocity(element, { opacity: 0 });
expect(element.style.opacity).toBe("0");

await Velocity(element, { translateX: "100px" });
expect(element.style.transform).toContain("100px");

Интеграция с CI и проблема флейков

В CI-окружениях визуальные тесты Velocity.js часто становятся нестабильными из-за:

  • различий в GPU ускорении
  • отсутствия аппаратного рендеринга
  • разной частоты таймеров
  • параллельного выполнения тестов

Для стабилизации применяются стратегии:

  • последовательный запуск тестов анимаций
  • фиксация viewport
  • отключение аппаратного ускорения
  • использование headless-браузеров с одинаковыми флагами

Сравнение кадров через Canvas

Более точный подход, чем DOM-снапшоты, — рендеринг элемента в canvas и сравнение пикселей.

Алгоритм:

  1. захват кадра
  2. перевод в ImageData
  3. сравнение массива пикселей
const diff = (img1, img2) => {
  for (let i = 0; i < img1.data.length; i++) {
    if (img1.data[i] !== img2.data[i]) return false;
  }
  return true;
};

Этот метод позволяет выявлять даже минимальные расхождения в анимации Velocity.js.

Тестирование производительности анимаций

Визуальное тестирование включает не только корректность, но и производительность.

Velocity.js может быть протестирован по:

  • времени кадра (frame time)
  • FPS
  • количеству layout/reflow операций

Пример измерения:

let frames = 0;

function measure() {
  frames++;
  requestAnimationFrame(measure);
}

measure();

setTimeout(() => {
  console.log(frames);
}, 1000);

Падение FPS при анимациях указывает на перегрузку DOM-операциями.

Синхронизация тестов с виртуальным временем

Наиболее стабильный подход — полная виртуализация времени:

  • Jest Fake Timers
  • Sinon Fake Timers
  • кастомные time controllers

Это позволяет:

  • запускать анимации мгновенно
  • фиксировать состояние на любом этапе
  • устранять влияние системы

Velocity.js в таких условиях становится полностью детерминированным, что критично для масштабных UI-наборов и дизайн-систем.