Testing
Test animations deterministically with a manual frame source and isolated clocks.
Animations depend on time and requestAnimationFrame. Replace both with a clock you control and tests become exact and fast.
A manual clock
@freestylejs/ani-core/testing ships createManualClock: a real clock (createClock) fed by a hand-cranked frame source and time source. It follows the production code path with deterministic time. It is not part of the root entry, so it never reaches your bundle.
import { createManualClock } from "@freestylejs/ani-core/testing";
const manual = createManualClock(); // time starts at 1000 ms| Member | Description |
|---|---|
clock | The Clock to pass to timelines and createStates. |
now | Current time in ms. |
pending | Queued frame requests (0 or 1 for a healthy clock). |
frame(ms = 16) | Advance time by ms and run one frame. |
frames(count, ms = 16) | Run count frames. |
advance(ms) | Move time forward without delivering a frame. |
drop() | Lose the pending frame request, as a browser may. |
setHidden(hidden) | Flip the injected visibility source and fire visibilitychange. Throws TypeError with visibility: false. |
Options: lagThreshold, maxDeltaTime, timeScale, onError (all forwarded to createClock), visibility (default true, the manual clock injects its own source) and reducedMotion (default false).
| Member | Description |
|---|---|
setReducedMotion(on) | Flip the injected reduced-motion preference. It is read at the start of each play(). |
Time is in milliseconds on the manual clock (now, frame(ms)) because it feeds the frame source; timelines still receive their dt in seconds.
Test a timeline
import * as a from "@freestylejs/ani-core";
import { createManualClock } from "@freestylejs/ani-core/testing";
import { expect, it } from "vitest";
it("reaches the target and leaves the clock", () => {
const manual = createManualClock();
const timeline = a.rafTimeline(
a.ani({ to: { x: 100 }, duration: 1, timing: a.timing.linear() }),
{ clock: manual.clock }
);
timeline.play({ from: { x: 0 } });
manual.frames(5, 100); // dt = 0.1 s each
expect(timeline.value).toEqual({ x: 50 });
expect(timeline.status).toBe("PLAYING");
manual.frames(10, 100);
expect(timeline.status).toBe("ENDED");
expect(manual.clock.size).toBe(0); // no leaked subscriber
expect(manual.pending).toBe(0); // no leaked frame request
});- Frames up to the clock's
lagThreshold(0.5 s) advance by their full duration. A longerframe(ms)simulates a suspension and advances by one ordinary frame; passlagThreshold: Infinityto drive arbitrary jumps in wall-clock time. - Pass
clocktocreateStates, and to timelines created by your code, so nothing touches the shared clock.
Reset the shared clock
If your code uses the default clock (defaultClock), isolate tests with resetDefaultClock(). It disposes the shared clock and restores its defaults (timing options, timeScale 1, not paused). The instance stays the same, so references to defaultClock remain valid.
import { resetDefaultClock } from "@freestylejs/ani-core/testing";
import { afterEach } from "vitest";
afterEach(() => {
resetDefaultClock();
});The same call is useful to tear down the shared clock on hot module reload.
Test for leaks
- After unmounting views,
clock.size(ordefaultClock.size) must be0. - Count
onUpdatesubscriptions by wrappingtimeline.onUpdateand counting subscribe minus unsubscribe calls. - Repeat mount and unmount many times (hundreds) and assert the counts do not grow.
Test bindings
- Pass your manual clock to the timelines and to
useAniStates/createAniStates(clockoption). - Run in
jsdom. Wrap React updates inact. - Assert on
element.styleafter callingmanual.frame(). - For server rendering, run in the
nodeenvironment and render. See SSR.
Test reduced motion
The manual clock starts with the preference off, so existing timings hold. Turn it on to test the 'jump' policy. See Reduced motion.
import * as a from "@freestylejs/ani-core";
import { createManualClock } from "@freestylejs/ani-core/testing";
import { expect, it } from "vitest";
it("completes at once when reduced motion is on", async () => {
const manual = createManualClock({ reducedMotion: true });
const timeline = a.rafTimeline(a.ani({ to: { x: 1 }, duration: 1 }), {
clock: manual.clock,
});
await expect(timeline.play({ from: { x: 0 } })).resolves.toBe("ended");
expect(manual.clock.size).toBe(0);
});Test errors
Callback errors are reported asynchronously. In a test, give the clock an onError to capture errors thrown by subscribers, and stub queueMicrotask to capture errors thrown by onUpdate callbacks:
import { createManualClock } from "@freestylejs/ani-core/testing";
const errors: unknown[] = [];
const manual = createManualClock({ onError: (error) => errors.push(error) });Related Components
clock-createClockoptions and guarantees.Large-scale usage- Ownership rules to assert in tests.