npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

phaser-test-utils

v0.1.0

Published

Headless layout & UI testing framework for Phaser

Readme

phaser-test-utils

Headless layout & UI testing framework for Phaser. Write integration tests for your Phaser game's UI layouts and interactions that run in vitest without a browser. Tested against Phaser 4 (also compatible with Phaser 3.60+).

What it does

  • Boots Phaser in HEADLESS mode inside vitest's jsdom environment
  • Detects when rendered elements overlap when they shouldn't
  • Detects when elements don't fit within their container/panel
  • Verifies positioning, alignment, and spacing
  • Simulates pointer clicks and keyboard events for interaction testing
  • Provides vitest custom matchers for ergonomic test assertions
  • Produces visual ASCII debug output on assertion failures

Requirements

  • Phaser 4.x (also works with 3.60+)
  • vitest >= 4.0.0

Installation

npm install --save-dev phaser-test-utils

Setup

vitest.config.ts

import { defineConfig } from "vitest/config";
import path from "path";

export default defineConfig({
    resolve: {
        alias: {
            // Phaser's main field points to source which has dev-only requires.
            // Use the pre-built ESM dist instead.
            phaser: path.resolve(__dirname, "node_modules/phaser/dist/phaser.esm.js"),
        },
    },
    test: {
        environment: "jsdom",
        setupFiles: ["phaser-test-utils/setup"],
    },
});

The setup file registers vitest custom matchers. If you don't want the matchers, skip the setupFiles entry and call setupLayoutMatchers() manually in your tests.

Note: The jsdom environment is required since Phaser needs DOM APIs (document, window, Image, HTMLCanvasElement, etc.) to boot.

Import style

Phaser's ESM dist uses named exports. Always import as:

import * as Phaser from "phaser";

Usage

Creating a test game

import { describe, it, afterEach } from "vitest";
import * as Phaser from "phaser";
import { createTestGame, step } from "phaser-test-utils";

describe("my game", () => {
    let destroy: (() => void) | undefined;

    afterEach(() => {
        destroy?.();
        destroy = undefined;
    });

    it("creates a scene", async () => {
        const {
            game,
            scene,
            destroy: d,
        } = await createTestGame({
            width: 800, // default: 800
            height: 600, // default: 600
            scene: {
                key: "test",
                create() {
                    this.add.rectangle(100, 100, 50, 50);
                },
            },
        });
        destroy = d;

        // Scene is now ready, game loop is stopped.
        // Use step() to advance the game loop manually.
        step(game); // advance 1 frame (~16.67ms)
        step(game, 5); // advance 5 frames
        step(game, 1, 33); // advance 1 frame with custom delta (33ms)
    });
});

createTestGame returns a promise that resolves once the scene's create() method has finished. The game loop is automatically stopped so you control frame progression with step().

You can also pass a Scene class

class MyScene extends Phaser.Scene {
    constructor() {
        super({ key: "my-scene" });
    }
    create() {
        // ...
    }
}

const { game, scene, destroy } = await createTestGame({ scene: MyScene });

Layout Assertions

Function-based API

import {
    expectNoOverlap,
    expectNoOverlaps,
    expectContainedIn,
    expectAllContainedIn,
    expectAligned,
    expectMinGap,
    expectAbove,
    expectLeftOf,
    expectValidLayout,
} from "phaser-test-utils";

expectNoOverlap(a, b)

Assert that two objects do not overlap. Accepts game objects or Phaser.Geom.Rectangle.

const btnA = scene.add.rectangle(50, 50, 40, 40);
const btnB = scene.add.rectangle(150, 50, 40, 40);
expectNoOverlap(btnA, btnB); // passes - no overlap

expectNoOverlaps(objects)

Assert that no objects in the array overlap each other (pairwise O(n^2) check).

expectNoOverlaps([btnA, btnB, btnC]);

expectContainedIn(child, parent)

Assert that child is fully contained within parent. Accepts game objects or Phaser.Geom.Rectangle.

const panel = new Phaser.Geom.Rectangle(0, 0, 400, 300);
const label = scene.add.rectangle(200, 150, 100, 50);
expectContainedIn(label, panel); // passes

expectAllContainedIn(children, parent)

Assert all children are contained within the parent.

expectAligned(a, b, axis, tolerance?)

Assert two objects are aligned on the given axis. Default tolerance: 1px.

Axis options: 'centerX', 'centerY', 'top', 'bottom', 'left', 'right'

expectAligned(title, subtitle, "centerX"); // horizontally centered
expectAligned(leftPanel, rightPanel, "top"); // tops aligned

expectMinGap(a, b, minPixels)

Assert minimum gap (in pixels) between two objects.

expectMinGap(header, content, 20); // at least 20px gap

expectAbove(upper, lower)

Assert that upper is positioned above lower (no vertical overlap).

expectAbove(title, body);

expectLeftOf(left, right)

Assert that left is positioned to the left of right (no horizontal overlap).

expectLeftOf(saveBtn, cancelBtn);

expectValidLayout(children, container, opts?)

Composite check: verifies both containment and no-overlap for a group of children within a container.

const panel = new Phaser.Geom.Rectangle(0, 0, 400, 300);
expectValidLayout([btn1, btn2, btn3], panel);
// With options:
expectValidLayout(children, panel, { noOverlaps: true, containChildren: true });

Vitest Custom Matchers

When the setup file is loaded (or setupLayoutMatchers() is called), these matchers are available:

expect(btnA).not.toOverlapWith(btnB);
expect(label).toBeContainedIn(panel);
expect(title).toBeAbove(subtitle);
expect(saveBtn).toBeLeftOf(cancelBtn);
expect(header).toBeAlignedWith(body, "centerX");
expect(header).toBeAlignedWith(body, "left", 2); // tolerance: 2px

Input Simulation

Pointer events

import {
    simulateClick,
    simulatePointerDown,
    simulatePointerUp,
    simulatePointerMove,
} from "phaser-test-utils";

// Step first to process pending interactive object registrations
step(game);

// Click at center of a game object
simulateClick(game, button);
step(game); // process the click

// Or use individual events at specific coordinates
simulatePointerDown(game, 100, 200);
simulatePointerMove(game, 150, 200);
simulatePointerUp(game, 150, 200);
step(game);

Important: You must call step(game) at least once after createTestGame before simulating clicks on interactive objects. This step processes the input system's pending registrations from setInteractive().

Keyboard events

import { simulateKeyDown, simulateKeyUp, simulateKeyPress } from "phaser-test-utils";

// Simulate key press (down + up)
simulateKeyPress(game, Phaser.Input.Keyboard.KeyCodes.SPACE);
step(game);

// Or individual events
simulateKeyDown(game, Phaser.Input.Keyboard.KeyCodes.LEFT);
step(game);
simulateKeyUp(game, Phaser.Input.Keyboard.KeyCodes.LEFT);
step(game);

Debug Output

When layout assertions fail, you get detailed error messages with visual ASCII debug:

expectNoOverlap failed:
  "save-btn": { x: 100, y: 200, w: 120, h: 40, right: 220, bottom: 240 }
  "cancel-btn": { x: 180, y: 210, w: 120, h: 40, right: 300, bottom: 250 }
  Overlap region: { x: 180, y: 210, w: 40, h: 30, right: 220, bottom: 240 }

  Visual:
    A = "save-btn", B = "cancel-btn", # = overlap
    |AAAAAAAAAAAA##BBBBBBBB|
    |AAAAAAAAAAAA##BBBBBBBB|
    |AAAAAAAAAAAA##BBBBBBBB|
    |            ##BBBBBBBB|

Containment failures show which edges overflow:

expectContainedIn failed:
  Child "label": { x: -10, y: 50, w: 200, h: 30, right: 190, bottom: 80 }
  Parent "panel": { x: 0, y: 0, w: 400, h: 300, right: 400, bottom: 300 }

  Overflow:
    Left: 10px past parent left edge

How It Works

  1. Phaser HEADLESS mode (type: Phaser.HEADLESS) skips renderer creation entirely - no WebGL or Canvas rendering. This is a built-in Phaser feature designed for unit testing.

  2. jsdom provides the DOM APIs that Phaser needs to boot (document, window, HTMLCanvasElement, etc.). jsdom comes built-in with vitest's jsdom environment.

  3. Texture boot workaround: jsdom's Image element doesn't fire onload for base64 data URIs. Phaser's TextureManager waits for 3 default textures to load before completing boot. createTestGame creates minimal canvas-based placeholder textures and manually completes the boot sequence.

  4. getBounds() on all Phaser game objects is pure math - it computes world-space axis-aligned bounding boxes from the object's position, size, origin, scale, and rotation without needing a renderer.

  5. Pointer simulation bypasses the ScaleManager's coordinate transform (which returns Infinity in headless mode) and sets pointer position directly in game-space coordinates before routing through Phaser's input plugin system.

API Reference

Game lifecycle

| Function | Description | | ------------------------------- | --------------------------------------------------------------- | | createTestGame(config) | Boot a headless Phaser game. Returns { game, scene, destroy } | | step(game, frames?, deltaMs?) | Advance the game loop by N frames |

Layout assertions

| Function | Description | | ----------------------------------------------- | ------------------------------------- | | expectNoOverlap(a, b) | Assert two objects don't overlap | | expectNoOverlaps(objects) | Assert no pairwise overlaps | | expectContainedIn(child, parent) | Assert child fits inside parent | | expectAllContainedIn(children, parent) | Assert all children fit inside parent | | expectAligned(a, b, axis, tolerance?) | Assert alignment on axis | | expectMinGap(a, b, minPixels) | Assert minimum spacing | | expectAbove(upper, lower) | Assert vertical ordering | | expectLeftOf(left, right) | Assert horizontal ordering | | expectValidLayout(children, container, opts?) | Composite containment + overlap check | | setupLayoutMatchers() | Register vitest custom matchers |

Input simulation

| Function | Description | | ------------------------------------------ | ------------------------------ | | simulateClick(game, target) | Click at center of game object | | simulatePointerDown(game, x, y, button?) | Pointer down at coordinates | | simulatePointerUp(game, x, y, button?) | Pointer up at coordinates | | simulatePointerMove(game, x, y) | Pointer move to coordinates | | simulateKeyDown(game, keyCode) | Key down event | | simulateKeyUp(game, keyCode) | Key up event | | simulateKeyPress(game, keyCode) | Key down + up |

Utilities

| Function | Description | | -------------------- | -------------------------------------------------------------------- | | resolveBounds(obj) | Get Phaser.Geom.Rectangle bounds from any game object or Rectangle | | getObjectName(obj) | Get human-readable name for debug output | | formatRect(rect) | Format a Rectangle as a string |

License

MIT