Testing
Compile a real scene, resolve frames, assert on resolved actors — and never hand-build a fixture.
Because at(frame) is pure and needs no renderer, testing a
scene is just calling it and asserting. No canvas, no browser, no video.
The core rule
Test through the runtime, not through fixtures. Compile a real scene with
createScene(...), resolve frames with compiled.at(frame), and assert on the resolved
actors.
Never hand-build ResolvedActor or FrameState literals
A hand-written resolved object encodes today's runtime shape and rots silently every time resolution changes. This class of fixture drift has repeatedly broken suites across renderers, desktop, scene3d and primitives — the test keeps passing while describing a runtime that no longer exists.
Actor tests
Spawn the actor in a scene generator, run its commands, resolve, assert.
import { createScene } from "@motionactor/core";
import { LineChart } from "../lineChart/actor";
it("animates trace progress over time", () => {
const compiled = createScene(function* (ctx) {
const chart = ctx.spawn(LineChart, {
content: { points: [{ x: 0, y: 12, label: "9 AM" }] },
});
yield* chart.traceIn(0.4);
});
const tracing = compiled.at(6).findActor(LineChart);
expect(tracing?.signals.reveal.traceProgress).toBeGreaterThan(0);
expect(tracing?.signals.reveal.traceProgress).toBeLessThan(1);
});Conventions
- Durations are authored in seconds;
at(frame)takes integer frames at the scene'sfps(default 30). A0.4second tween has settled by frame 12. - Look up actors by blueprint —
findActor(B),findActors(B),getActor(id, B). Do not probesignalsthrough casts. compiled.totalDurationis the length in frames. Render output normally samples0throughtotalDuration - 1; resolvingtotalDurationitself is useful for an endpoint assertion, but actors that have exited may already be absent.- Pick mid-animation frames. Asserting only the endpoints passes for a tween that teleports. Frame 6 of a 12-frame tween is the assertion that can actually fail.
Feeding channels
Write through the resolved handle, then re-resolve:
compiled.at(0).findActor(WeatherApp)?.channels.forecast.write({
...DEFAULT_WEATHER_FORECAST_SNAPSHOT,
city: "Orleans",
});
const resolved = compiled.at(10).findActor(WeatherApp);
expect(resolved?.signals.forecast.city).toBe("Orleans");Publications land in the runtime store, so a fresh at(...) after the write sees the
value. Channel values surface on resolved signals, so assertions read them exactly like
ordinary state.
View tests
Views are thin readers, so a view test reuses the same compile-and-resolve flow and renders the view with the resolved actor:
import { renderToStaticMarkup } from "react-dom/server.node";
const state = compiled.at(frame);
const markup = renderToStaticMarkup(<CardView actor={state.findActor(Card)!} />);What is worth testing
Tests that can fail for a mistake nobody chose:
- Timing — a mid-animation frame, not just the endpoints.
- Seeking — resolve backwards and assert the same answer as forwards.
- Channel precedence — a per-call override beating a publication.
- Child identity — the same child instance across frames.
- The bug you just fixed.
Not worth it: asserting an actor has the fields you just wrote, or that a view renders the prop you passed it.
The checked example
The Remotion example is verified in CI without rendering:
pnpm typecheck:examples
pnpm test:examplesThe behavior check covers midpoint timing, child identity, channel overrides and backward seeking. These checks do not bundle or render a video, verify Tailwind output, or validate published tarballs in a fresh consumer — those are separate release checks.