Verify Matter.js Collision Events in Automated Tests
Verifying collision events in Matter.js within an automated test
suite requires configuring a headless physics engine, registering event
listeners to capture collision lifecycle triggers, stepping the engine
deterministically, and asserting that the expected collision pairs were
detected. This guide explains how to isolate the physics simulation from
the browser rendering loop and use test runners like Jest or Vitest to
reliably assert collisionStart,
collisionActive, and collisionEnd events.
Setting Up a Headless Physics Simulation
Automated test suites typically run in environments without a DOM or
display, such as Node.js. Matter.js is modular, meaning you do not need
Matter.Render or Matter.Runner to simulate
physics. You only need Matter.Engine,
Matter.World, and Matter.Bodies.
To begin, initialize an engine and disable gravity if you want total control over body movements:
import Matter from 'matter-js';
const { Engine, World, Bodies, Events } = Matter;
const engine = Engine.create({
gravity: { x: 0, y: 0, scale: 0 }
});Registering Event Listeners with Test Spies
Matter.js dispatches collision events via
Matter.Events.on(engine, eventName, callback). The primary
events are:
collisionStart: Fired at the first frame where two bodies overlap.collisionActive: Fired for every frame bodies continue to touch.collisionEnd: Fired immediately after bodies separate.
Attach a test spy or mock function (such as jest.fn() or
vi.fn()) to the target collision event to record
occurrences and payload data.
const onCollisionStart = jest.fn();
Events.on(engine, 'collisionStart', onCollisionStart);Stepping the Engine Manually
In production, Matter.Runner synchronizes simulation
steps with the browser frame rate. In automated tests, you should
advance physics deterministically using
Engine.update(engine, delta).
Provide a fixed delta time (typically 1000 / 60 for 60
FPS) to simulate frames:
// Step the simulation forward by one frame (~16.67ms)
Engine.update(engine, 1000 / 60);Complete Test Example
Below is a complete test using Jest/Vitest that positions two bodies
directly over each other, steps the engine, and verifies that the
collisionStart event fired with the correct bodies.
import Matter from 'matter-js';
describe('Matter.js Collision Verification', () => {
let engine;
let world;
beforeEach(() => {
engine = Matter.Engine.create({
gravity: { x: 0, y: 0, scale: 0 }
});
world = engine.world;
});
test('should fire collisionStart event when bodies intersect', () => {
const collisionSpy = jest.fn();
Matter.Events.on(engine, 'collisionStart', collisionSpy);
// Create two overlapping bodies
const bodyA = Matter.Bodies.rectangle(100, 100, 50, 50, { label: 'BoxA' });
const bodyB = Matter.Bodies.rectangle(110, 100, 50, 50, { label: 'BoxB' });
Matter.World.add(world, [bodyA, bodyB]);
// Advance engine by one step
Matter.Engine.update(engine, 1000 / 60);
// Assert that the collision listener was triggered
expect(collisionSpy).toHaveBeenCalledTimes(1);
// Inspect the collision event payload
const eventPayload = collisionSpy.mock.calls[0][0];
const pair = eventPayload.pairs[0];
// Matter.js normalizes pairs, so verify both participating bodies
const bodiesInvolved = [pair.bodyA.label, pair.bodyB.label];
expect(bodiesInvolved).toContain('BoxA');
expect(bodiesInvolved).toContain('BoxB');
});
test('should fire collisionEnd when bodies separate', () => {
const collisionEndSpy = jest.fn();
Matter.Events.on(engine, 'collisionEnd', collisionEndSpy);
const bodyA = Matter.Bodies.rectangle(100, 100, 50, 50);
const bodyB = Matter.Bodies.rectangle(100, 100, 50, 50);
Matter.World.add(world, [bodyA, bodyB]);
// First step triggers collisionStart
Matter.Engine.update(engine, 1000 / 60);
// Move bodyB away to resolve the collision
Matter.Body.setPosition(bodyB, { x: 500, y: 500 });
// Next step detects separation and triggers collisionEnd
Matter.Engine.update(engine, 1000 / 60);
expect(collisionEndSpy).toHaveBeenCalledTimes(1);
});
});Best Practices for Test Reliability
- Normalize Pair Matching: When validating
collisions, do not assume
pair.bodyAis your first object andpair.bodyBis your second. Matter.js sorts pairs internally by body ID. Always assert that the pair contains both items regardless of order. - Assign Labels: Use the
labelproperty when creating bodies (e.g.,Bodies.rectangle(x, y, w, h, { label: 'Player' })) to make test failure logs and assertions readable. - Isolate Tests: Re-create the
Engineinstance in abeforeEachhook to avoid state leakage from residual bodies or collision pairs between tests.