Анимации в JavaScript создают дополнительный слой сложности для тестирования, поскольку их поведение зависит от времени, кадрового цикла и браузерного движка. Библиотека Motion One работает поверх Web Animations API и requestAnimationFrame, что делает её поведение чувствительным к:
Основная задача тестирования анимаций заключается не в проверке визуальной плавности, а в контроле состояния DOM, вычисленных стилей и логики жизненного цикла анимации.
Motion One предоставляет несколько ключевых сущностей:
animate() — базовый запуск анимацииtimeline() — последовательности анимацийmotion() — декларативные анимации (в
React/DOM-обвязках)С точки зрения тестов важно, что каждая анимация возвращает контроллер с состоянием:
playStatefinishedcurrentTimecancel()finish()Эти свойства позволяют переводить анимацию в детерминированные состояния без ожидания реального времени.
Основной подход строится вокруг трёх уровней:
Ключевая идея — устранение реального времени через фейковые таймеры и ручное управление кадрами.
При тестировании Motion One часто применяется подмена таймеров:
import { animate } from "motion";
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
Однако Motion One в большей степени зависит не от
setTimeout, а от requestAnimationFrame,
поэтому одних таймеров недостаточно.
Для детерминированных тестов требуется управление кадрами вручную:
function createRafMock() {
let time = 0;
const callbacks = new Set();
global.requestAnimationFrame = (cb) => {
callbacks.add(cb);
return 1;
};
global.cancelAnimationFrame = (id) => {
callbacks.clear();
};
return {
step(ms) {
time += ms;
callbacks.forEach((cb) => cb(time));
}
};
}
Такой подход позволяет:
Основная цель — убедиться, что итоговое состояние DOM соответствует ожиданиям.
import { animate } from "motion";
test("animate изменяет opacity до 1", () => {
const raf = createRafMock();
const el = document.createElement("div");
document.body.appendChild(el);
animate(el, { opacity: 1 }, { duration: 1000 });
raf.step(1000);
expect(el.style.opacity).toBe("1");
});
Ключевой момент — проверка не промежуточных кадров, а конечного состояния.
Иногда требуется контроль интерполяции.
test("анимация изменяет значение по пути", () => {
const raf = createRafMock();
const el = document.createElement("div");
document.body.appendChild(el);
animate(el, { x: 100 }, { duration: 1000 });
raf.step(500);
const transform = getComputedStyle(el).transform;
expect(transform).not.toBe("none");
});
В таких случаях важно учитывать:
Motion One возвращает объект с Promise:
test("анимация завершается", async () => {
const raf = createRafMock();
const el = document.createElement("div");
document.body.appendChild(el);
const animation = animate(el, { opacity: 0 }, { duration: 300 });
raf.step(300);
await animation.finished;
expect(animation.playState).toBe("finished");
});
В тестах важно избегать реального ожидания и полностью синхронизировать промис через искусственный прогон времени.
Timeline объединяет несколько анимаций с временными оффсетами.
import { timeline } from "motion";
test("timeline выполняет последовательность", () => {
const raf = createRafMock();
const el = document.createElement("div");
document.body.appendChild(el);
const sequence = timeline([
[el, { x: 100 }, { duration: 200 }],
[el, { y: 100 }, { duration: 200, at: "+0" }]
]);
raf.step(200);
raf.step(200);
expect(sequence.playState).toBe("finished");
});
Ключевая сложность — корректная эмуляция временной шкалы, особенно
при использовании at, offset,
delay.
В средах без полноценного WAAPI требуется полифилл:
global.Element.prototype.animate = function () {
return {
finished: Promise.resolve(),
cancel: () => {},
play: () => {},
pause: () => {},
currentTime: 0
};
};
Такая заглушка используется только для логических тестов, не затрагивающих реальную анимацию.
Motion One часто работает с transform-матрицами:
Проверка через getComputedStyle:
test("transform применяется корректно", () => {
const el = document.createElement("div");
document.body.appendChild(el);
animate(el, { x: 50 });
const style = getComputedStyle(el).transform;
expect(style).toContain("matrix");
});
Для более точных проверок часто требуется нормализация матриц или использование вспомогательных утилит.
Motion One предоставляет события жизненного цикла:
test("onComplete вызывается", () => {
const raf = createRafMock();
let called = false;
animate(
document.createElement("div"),
{ opacity: 0 },
{
duration: 100,
onComplete: () => {
called = true;
}
}
);
raf.step(100);
expect(called).toBe(true);
});
Важно учитывать, что колбэки могут вызываться в микротасках после завершения кадра.
test("анимация отменяется", () => {
const raf = createRafMock();
const el = document.createElement("div");
const animation = animate(el, { opacity: 0 }, { duration: 1000 });
raf.step(200);
animation.cancel();
expect(animation.playState).toBe("idle");
});
Критично проверять:
В тестах производительности анимаций анализируются:
Пример упрощённого подсчёта вызовов:
let frames = 0;
global.requestAnimationFrame = (cb) => {
frames++;
cb(performance.now());
};
animate(document.createElement("div"), { x: 100 }, { duration: 1000 });
expect(frames).toBeLessThan(100);
E2E тесты позволяют проверять реальное поведение в браузере:
test("анимация завершает переход", async ({ page }) => {
await page.goto("/");
await page.click("#start");
await page.waitForSelector(".finished");
const opacity = await page.$eval("#box", el =>
getComputedStyle(el).opacity
);
expect(opacity).toBe("0");
});
В отличие от unit-тестов, здесь важна интеграция с реальным движком браузера.
Основные источники нестабильности:
Стратегии устранения:
Motion One учитывает prefers-reduced-motion.
test("учёт reduced motion", () => {
window.matchMedia = () => ({
matches: true,
addEventListener: () => {}
});
const animation = animate(
document.createElement("div"),
{ opacity: 1 }
);
expect(animation.duration).toBe(0);
});
Проверяется:
Полноценное покрытие анимаций включает:
animate, timeline)playState, finished)Тестирование Motion One требует строгого контроля времени и изоляции среды исполнения, поскольку любые реальные задержки приводят к нестабильности результатов.