8 min read
Visual Regression Testing for Lottie Animations
You can't diff a playing animation the way you diff a static screenshot, but Lottie's playback is fully deterministic — the same JSON at the same frame always renders the same pixels — which makes it straightforward to test with the right approach.
Test fixed frames, not continuous playback
Rather than trying to capture and diff video, pick a handful of representative frames (say, 0%, 25%, 50%, 75%, and 100% of the animation's duration), jump to each one with goToAndStop, and screenshot the result.
import { test, expect } from '@playwright/test';
test('animation renders correctly at key frames', async ({ page }) => {
await page.goto('/test-harness/animation.html');
const totalFrames = await page.evaluate(() => (window as any).animation.totalFrames);
const checkpoints = [0, 0.25, 0.5, 0.75, 1].map((p) => Math.floor(p * (totalFrames - 1)));
for (const frame of checkpoints) {
await page.evaluate((f) => (window as any).animation.goToAndStop(f, true), frame);
await expect(page.locator('#lottie-container')).toHaveScreenshot(`frame-${frame}.png`);
}
});A dedicated test harness page
Build a minimal HTML page that loads a single animation from a query parameter or fixed path and exposes the player instance on window (as referenced above). Point your test runner at that harness rather than a full application page — it removes unrelated UI from the screenshot and makes the animation the only thing that can cause a diff.
Pin your renderer version
Renderer updates (antialiasing tweaks, SVG rasterization changes) can shift pixels by a barely-visible amount that still fails a strict pixel-diff. Pin the exact version of your Lottie player library in CI, and only update it deliberately — re-baselining your screenshots in the same change — rather than letting it drift and produce noisy, unrelated test failures.
Use it to verify optimization passes
This same technique is the most reliable way to confirm an optimizer hasn't changed how an animation looks: snapshot the original file at your checkpoint frames, run it through optimization, snapshot the result at the same frames, and diff the two sets directly against each other rather than against a stored baseline. A truly lossless optimization should produce pixel-identical (or near-identical, allowing for minor floating-point rounding in path data) results.