Unit-тестирование анимаций в Velocity.js требует контроля над временем, состоянием DOM и асинхронными переходами. Основная сложность заключается в том, что анимации по своей природе зависят от реального времени, тогда как тестовая среда должна быть детерминированной.
Ключевая задача — превратить недетерминированное поведение (тайминги, кадры, easing-функции) в управляемый поток состояний, который можно проверять шаг за шагом.
Для этого анимации рассматриваются как функция перехода состояния DOM:
Velocity.js изменяет свойства напрямую через style, поэтому тестирование опирается на проверку DOM-узлов.
На практике используется сочетание следующих инструментов:
Базовая настройка Jest:
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
Важно учитывать, что Velocity.js использует requestAnimationFrame, поэтому иногда требуется дополнительная стабилизация:
global.requestAnimationFrame = (cb) => setTimeout(cb, 16);
global.cancelAnimationFrame = (id) => clearTimeout(id);
Unit-тесты часто проверяют начальное состояние перед запуском анимации. Это позволяет убедиться, что библиотека корректно применяет стартовые стили.
import Velocity from 'velocity-animate';
test('initial state before animation', () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
Velocity(el, { opacity: 1 }, { duration: 1000 });
expect(el.style.opacity).toBe('0');
});
Velocity.js по умолчанию устанавливает начальные значения в зависимости от текущего состояния DOM, поэтому тесты должны учитывать computed styles:
window.getComputedStyle = (el) => el.style;
Основная техника unit-тестирования анимаций — искусственное продвижение времени.
Velocity.js разбивает анимацию на кадры, поэтому важно управлять таймером синхронно.
test('animation completes after duration', () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
Velocity(el, { opacity: 0 }, { duration: 1000 });
jest.advanceTimersByTime(1000);
expect(el.style.opacity).toBe('0');
});
При необходимости проверяется промежуточное состояние:
jest.advanceTimersByTime(500);
В этом случае значение opacity должно находиться между начальным и конечным, но точное значение зависит от easing-функции.
Easing влияет на распределение значений во времени. Unit-тесты обычно не проверяют точные дробные значения, но проверяют монотонность изменения.
test('opacity decreases monotonically', () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
Velocity(el, { opacity: 0 }, { duration: 1000, easing: 'linear' });
const values = [];
for (let t = 0; t <= 1000; t += 250) {
jest.advanceTimersByTime(250);
values.push(parseFloat(el.style.opacity));
}
for (let i = 1; i < values.length; i++) {
expect(values[i]).toBeLessThanOrEqual(values[i - 1]);
}
});
Velocity.js поддерживает завершение через callback и Promise-like интерфейс.
test('callback is called on finish', () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
const done = jest.fn();
Velocity(el, { opacity: 0 }, {
duration: 500,
complete: done
});
jest.advanceTimersByTime(500);
expect(done).toHaveBeenCalledTimes(1);
});
test('promise resolves after animation', async () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
const p = Velocity(el, { opacity: 0 }, { duration: 500 });
jest.advanceTimersByTime(500);
await expect(p).resolves.toBe(el);
});
Velocity.js поддерживает цепочки анимаций, которые выполняются последовательно.
test('sequential animations', () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
Velocity(el, { opacity: 0 }, { duration: 300 });
Velocity(el, { translateX: 100 }, { duration: 300 });
jest.advanceTimersByTime(600);
expect(el.style.transform).toContain('translateX');
});
При тестировании цепочек важно учитывать очередь (queue). Velocity по умолчанию использует queue “fx”, поэтому параллельные и последовательные анимации могут конфликтовать.
Отключение очереди:
Velocity(el, { opacity: 0 }, { duration: 300, queue: false });
Transform представляет собой строку, что усложняет сравнение.
test('transform applied correctly', () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
Velocity(el, { translateX: 100 }, { duration: 0 });
expect(el.style.transform).toContain('translateX(100px)');
});
При сложных трансформациях используется нормализация строки:
const normalizeTransform = (str) =>
str.replace(/\s/g, '').toLowerCase();
Некоторые тестовые среды требуют явного контроля requestAnimationFrame.
let rafCallbacks = [];
global.requestAnimationFrame = (cb) => {
rafCallbacks.push(cb);
return rafCallbacks.length;
};
global.runFrames = () => {
const callbacks = [...rafCallbacks];
rafCallbacks = [];
callbacks.forEach(cb => cb());
};
Тестирование:
test('frame-based animation progression', () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
Velocity(el, { opacity: 0 }, { duration: 1000 });
runFrames();
runFrames();
expect(parseFloat(el.style.opacity)).toBeLessThan(1);
});
Velocity.js позволяет останавливать анимацию с помощью
stop.
test('animation can be stopped', () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
Velocity(el, { opacity: 0 }, { duration: 1000 });
jest.advanceTimersByTime(300);
Velocity(el, 'stop');
const valueAtStop = el.style.opacity;
jest.advanceTimersByTime(1000);
expect(el.style.opacity).toBe(valueAtStop);
});
Анимации часто меняют не только стили, но и состояние UI: классы, visibility, display.
test('display change after animation', () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
Velocity(el, { opacity: 0 }, {
duration: 300,
complete: () => {
el.style.display = 'none';
}
});
jest.advanceTimersByTime(300);
expect(el.style.display).toBe('none');
});
Повторный запуск анимации на одном элементе может перезаписывать state.
test('restarting animation resets state', () => {
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
Velocity(el, { opacity: 0 }, { duration: 500 });
Velocity(el, { opacity: 1 }, { duration: 500 });
jest.advanceTimersByTime(500);
expect(parseFloat(el.style.opacity)).toBeGreaterThan(0.5);
});
Velocity поддерживает списки элементов.
test('multiple elements animation', () => {
document.body.innerHTML = `
<div class="item"></div>
<div class="item"></div>
`;
const items = document.querySelectorAll('.item');
Velocity(items, { opacity: 0 }, { duration: 400 });
jest.advanceTimersByTime(400);
items.forEach(el => {
expect(parseFloat(el.style.opacity)).toBe(0);
});
});
Хотя unit-тесты не являются инструментом профилирования, можно выявить чрезмерные циклы обновления:
test('no excessive frame updates', () => {
let frameCount = 0;
global.requestAnimationFrame = (cb) => {
frameCount++;
cb();
};
document.body.innerHTML = `<div id="box"></div>`;
const el = document.getElementById('box');
Velocity(el, { opacity: 0 }, { duration: 200 });
expect(frameCount).toBeLessThan(20);
});
Velocity.js может вычислять стартовые значения через computed styles. В тестовой среде это часто мокается.
window.getComputedStyle = (el) => ({
opacity: el.style.opacity || '1'
});
Это позволяет избежать зависимости от реального layout-движка браузера и делает тесты воспроизводимыми.