Intermediate / Game loop
Test an Animation Loop without Waiting in Real Time

Waiting for real animation frames makes scheduler tests slow and unreliable. The shared loop accepts scheduling and cancellation functions, so a test can control callback delivery directly and verify lifecycle behavior with synthetic timestamps.
What you'll build
We will build a tiny callback queue for createLoop and use it to test the initial frame, normal delta, long-gap cap, duplicate starts, stop, and resume. The engine itself does not need to run for these checks.
Prerequisites
Know maps, callbacks, and assertions. Read core.js for the loop's optional schedule and cancel parameters. Browser defaults are used in production; tests pass their own functions.
Store callbacks instead of running them immediately
A fake scheduler returns an identifier and stores the callback. Cancellation removes that identifier. A test step takes one stored callback and invokes it with a chosen timestamp.
const pending = new Map();
let nextId = 0;
const schedule = callback => {
pending.set(++nextId, callback);
return nextId;
};
const cancel = id => pending.delete(id);
function step(now) {
const [id, callback] = pending.entries().next().value;
pending.delete(id);
callback(now);
}
Deleting the delivered callback before invoking it matches the one-shot nature of an animation request. If the loop wants another frame, it must schedule another callback. A fake that leaves every delivered callback in the map would overcount pending work and conceal lifecycle mistakes.
Verify time calculations directly
Start the loop and deliver timestamps 100, 116, and 1116 milliseconds. The expected deltas are zero, 0.016, and the capped 0.05 seconds. The first callback establishes an origin; the last demonstrates that the one-second gap is not passed through unbounded.
You can record every delta received by update and compare the resulting array. This test requires no waiting, display refresh assumptions, or screenshot timing. It checks the scheduler's arithmetic, not the browser's ability to render smoothly.
Use values whose expected differences are easy to calculate. Complicated timestamp sequences can make a test harder to review than the function it is supposed to verify.
Check the single-chain invariant
Call start twice before delivering a frame. The queue should still contain one callback. Deliver it and verify only one successor is queued. This catches the common bug where every Start click begins another independent animation chain.
Stop the loop and verify the queue becomes empty. Starting again should queue one callback whose first delivered delta is zero, even if its timestamp is much later. That is the resume behavior that prevents a paused gap from becoming movement.
The frame identifier is only a cancellation handle. The test should not rely on a particular browser identifier value or treat it as a clock.
Stop from inside update
A game often discovers completion during update. Create a loop whose update callback calls stop, then deliver one frame. After render returns, no successor should remain queued.
This case tests the running check after update and render. A scheduler that always requests another frame at the end can resurrect itself despite having canceled the previous handle. The current loop's flag protects that path.
You may still allow the final render so the completed state is visible. Decide that behavior explicitly; stopping scheduling does not necessarily mean skipping the picture produced by the final update.
Keep engine tests separate
The fake queue does not prove that Collector catches gems correctly or Memory rejects a third card. Those are engine rules and should be tested through the factories. Combining everything into one scheduler test makes a failure harder to locate.
Browser checks remain useful for event wiring, focus, and Canvas rendering. The point of the fake scheduler is to move precise time and lifecycle assertions out of fragile real-time browser waits.
Next steps
Read tools/test-engines.mjs for the project's actual queue-based checks, then add a regression case for one lifecycle concern you can explain in a sentence. A small deterministic scheduler test often provides stronger evidence than repeatedly playing a round and hoping a duplicate callback chain becomes visually obvious.