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:

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