Test store. Prices are the planned list prices stored as test data, license terms are drafts, and payments go to a Stripe test-mode account. No real money moves.

How to Test and Debug HTML5 Games: Automated Testing Best Practices for Phaser

Test Phaser games in layers. Keep the rules in plain modules and unit-test them with Vitest, validate level and economy data, then run Playwright end-to-end tests against the production build in Chromium, Firefox and WebKit, reading game state through a small test hook instead of pixels. Debug with Chrome DevTools breakpoints, the Performance and Memory panels and remote debugging, then finish on real phones.

The best practice for automated testing of Phaser.js HTML5 games in 2026 is to test in layers. Fast unit tests cover the rules, data tests cover levels and tuning, and a smaller set of end-to-end tests plays the real build in three browser engines. The trick that makes Phaser testable is architectural: keep game rules out of scenes, and give end-to-end tests a way to read game state, because a canvas has no DOM for a test to inspect. This guide shows that setup with Vitest and Playwright, then covers how to debug HTML5 games in Chrome and on phones. It is the testing chapter of our HTML5 game development best practices checklist.

What to test in an HTML5 game

LayerToolWhat it coversSpeed
UnitVitest in NodeRules, scoring, economy, random generation, save migrationsMilliseconds per test
DataVitest in NodeLevel files, tuning JSON, translations, solvabilityFast
Browser componentsVitest browser modeCode that needs a real DOM, canvas or Web AudioSeconds
End to endPlaywrightBoot, menus, a full level, input per device, no errorsSeconds to minutes
ManualReal phones and tabletsFeel, audio, gestures, frame rate, in-app browsersSlow, but irreplaceable

Put most of your tests at the top of the table. A unit test that checks a scoring rule runs in a millisecond and fails with a clear message; an end-to-end test that discovers the same bug takes a minute and fails with "timed out waiting for results screen".

Best practices for automated testing of Phaser games in 2026

1. Keep the rules out of Phaser scenes

A Phaser scene that calculates scores, checks win conditions and moves sprites can only be tested by running Phaser. Split it. Put the rules in plain TypeScript modules that never import Phaser, and let the scene read their state and draw it. Then the rules run in Node under Vitest with no browser at all.

src/sim/score.ts
export interface Match { size: number; combo: number }

export function scoreFor(m: Match): number {
  const multiplier = Math.min(1 + m.combo * 0.5, 4); // combos add 50%, capped at 4x
  return m.size * 10 * multiplier;
}
tests/unit/score.test.ts
import { describe, expect, it } from "vitest";
import { scoreFor } from "../../src/sim/score.ts";

describe("scoreFor", () => {
  it("scores a plain match of three", () => {
    expect(scoreFor({ size: 3, combo: 0 })).toBe(30);
  });
  it("caps the combo multiplier at 4x", () => {
    expect(scoreFor({ size: 3, combo: 99 })).toBe(120);
  });
});

The same split helps far beyond testing: a pure simulation can be replayed, run faster than real time, or moved to another engine. Our Phaser tutorial uses the same structure from the first scene.

2. Make randomness and time injectable

Tests need the same result every run. Never call Math.random() or Date.now() deep inside game rules; pass in a random function and a clock. A seeded generator gives you reproducible levels, reproducible bugs and daily challenges that are the same for everyone.

src/sim/rng.ts
// mulberry32: a tiny, fast seeded generator, fine for gameplay (not for security)
export function seeded(seed: number): () => number {
  return () => {
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

3. Test your data, not only your code

Our games keep tuning (costs, waves, levels and economy) in JSON files, which is good for designers and dangerous without tests. A data test loads every level and checks invariants: unique IDs, references that resolve, prices that rise, no enemy wave with zero enemies, every translation key present in every language.

Puzzle games can go one step further with a solver: a test that searches each level for a solution, so you never ship an impossible level and you learn the minimum number of moves for star ratings. Four puzzle games in our catalog are tagged solver-proven: Shape Factory Flow, Magnet Maze, Cannon Courier and Bounce Architect.

4. Give end-to-end tests a test hook

Playwright finds HTML elements by role, text or test ID. A Phaser game draws its sprites into a canvas, so there is nothing for a locator to find. Don't fall back to clicking hard-coded pixel positions; they break on every layout change. Instead, expose a small, read-mostly test hook when the URL contains a flag, and let tests ask the game where things are.

src/test-hook.ts
// Installed only when the page URL contains ?test=1, so players never see it.
export function installTestHook(game: Phaser.Game, sim: Sim): void {
  if (!new URLSearchParams(location.search).has("test")) return;
  (window as any).__game = {
    state: () => ({ booted: sim.booted, level: sim.levelId, moves: sim.moves, busy: sim.busy }),
    tapPoint: (id: string) => {
      // world position -> page position (camera zoom omitted for brevity)
      const obj = sim.find(id);
      const cam = game.scene.getScene("Play").cameras.main;
      const rect = game.canvas.getBoundingClientRect();
      const scale = rect.width / game.scale.width;
      return { x: rect.left + (obj.x - cam.scrollX) * scale, y: rect.top + (obj.y - cam.scrollY) * scale };
    },
  };
}

Keep HTML parts of the game (menus, dialogs, results screens) testable the normal way, with data-testid attributes and page.getByTestId().

5. Wait for state, never sleep

Games are full of animations, tweens and loading. A test that clicks during a tween, or sleeps for "long enough", passes on your machine and fails in a slower engine. Wait for the condition you need instead:

tests/e2e/board.spec.ts
import { expect, test } from "@playwright/test";

test("a tap on the board counts as a move, with no errors", async ({ page }) => {
  const errors: string[] = [];
  page.on("pageerror", (e) => errors.push(e.message));
  page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });

  await page.goto("/?test=1&mute=1");
  await page.waitForFunction(() => (window as any).__game?.state().booted === true);
  await expect(page.locator("canvas")).toBeVisible();

  const p = await page.evaluate(() => (window as any).__game.tapPoint("tile-3-4"));
  await page.mouse.click(p.x, p.y);

  await expect.poll(() => page.evaluate(() => (window as any).__game.state().moves)).toBe(1);
  await page.waitForFunction(() => (window as any).__game.state().busy === false); // animations done
  expect(errors).toEqual([]);
});

Two habits in that test are worth copying everywhere. Collecting pageerror and console errors turns every end-to-end test into a crash detector for free. And expect.poll or waitForFunction replace every waitForTimeout: if a test needs to wait, it should say what it is waiting for.

6. Control time instead of waiting for it

A level with a two-minute timer shouldn't take two minutes to test. You have two options. A game-side option is a time-scale setting on the test hook that runs the simulation faster. A browser-side option is Playwright's Clock API, which overrides Date, timers, requestAnimationFrame and performance, so page.clock.install() followed by page.clock.runFor() can advance a game by minutes in one call. The game-side option is usually simpler for long simulations; the clock is ideal for daily challenges and date-dependent rewards.

7. Test the production build, muted

Run end-to-end tests against the built game, not the dev server, because minification, asset paths and code splitting break in ways the dev server never shows. Playwright's webServer option can start vite preview for you. Start games muted from the URL so audio autoplay rules don't add noise; our games accept a mute parameter for exactly this, and for portals.

8. Cover three engines and two kinds of device

playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "tests/e2e",
  use: { baseURL: "http://localhost:4173", screenshot: "only-on-failure", trace: "retain-on-failure" },
  projects: [
    { name: "desktop-chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "desktop-firefox", use: { ...devices["Desktop Firefox"] } },
    { name: "desktop-webkit", use: { ...devices["Desktop Safari"] } },
    { name: "mobile-chromium", use: { ...devices["Pixel 7"] } },
    { name: "mobile-webkit", use: { ...devices["iPhone 15"] } },
  ],
  webServer: { command: "npx vite preview --port 4173 --strictPort", url: "http://localhost:4173" },
});

Decide which tests run where by input type, not by browser name: mouse and keyboard tests on the desktop projects, touch tests on the mobile ones. Then every desktop test runs in all three engines automatically. Firefox and WebKit surface real bugs that Chromium hides, especially around storage, audio and timing. With trace: "retain-on-failure", a failing run leaves a trace you can open in Playwright's Trace Viewer to step through every action, network request and console message.

Know what this doesn't prove. Playwright's own browsers page explains that its WebKit is built from WebKit's main branch with patches and is not branded Safari. Device profiles emulate the viewport, user agent and touch, not a phone's CPU, GPU or operating system. A pass on the mobile-webkit project is good evidence that the game works on iPhones, not proof.

9. Use screenshots sparingly

Screenshot comparisons (toHaveScreenshot in Playwright, or toMatchScreenshot in Vitest 4's browser mode) catch layout regressions in menus and HUDs. On a WebGL canvas they are fragile: GPU drivers, font rendering and anti-aliasing differ between machines. If you use them, generate baselines on the same operating system as your CI, mask animated regions, freeze time first, and allow a small pixel tolerance. For gameplay, assert on state through the test hook instead.

How to debug HTML5 games using Chrome

Tests tell you something broke. Chrome DevTools tells you why.

Breakpoints that don't drown you

A breakpoint in a game loop fires 60 times a second, which is useless. Chrome's breakpoint types solve that:

  • Conditional breakpoints pause only when an expression is true, such as enemy.hp < 0 or player.y > 2000.
  • Logpoints print a message without pausing and without editing your code, which beats sprinkling console.log through a loop.
  • Event listener breakpoints pause on the next pointerdown or keydown, the fastest way to find which handler eats an input.
  • Exception breakpoints pause where an error is thrown, including caught ones, which is how you find a save error that a try/catch swallowed.

Add a debugger; statement in code for a one-off pause, and remove it before you commit.

Slow frames and stutter

Record a few seconds in the Performance panel and look for long frames in the Frames section, then read the Main track's flame chart to see what ran. Turn on Frame rendering stats in the Rendering tab for a live FPS and dropped-frame overlay. Our HTML5 game performance guide covers what to fix once you find the slow part.

Memory leaks between scenes

The classic Phaser leak is a scene that adds listeners, timers or tweens on create and never removes them on shutdown. Each restart adds another copy, and the game slows down after a few levels. Take a heap snapshot in the Memory panel, restart the level five times, take another, and compare: objects whose count grows by exactly five are your leak.

Draw what the player can't see

Many bugs are invisible: a hitbox larger than its sprite, a trigger in the wrong place, a body that never sleeps. In Phaser's Arcade Physics, set debug: true in the physics config to draw bodies and velocities. For your own systems, add a debug overlay behind a URL flag that draws hit areas and prints key state on screen.

Debugging on phones

For Android, enable USB debugging on the phone, open chrome://inspect#devices in desktop Chrome, and inspect the phone's tab with full DevTools, as described in Chrome's remote debugging guide. For iPhones and iPads, enable Web Inspector in Safari's advanced settings on the device and open the page from the Develop menu of Safari on a Mac; Apple documents the steps in inspecting iOS and iPadOS.

Inspecting WebGL frames

When a WebGL scene draws wrong, Spector.js captures a single frame and shows every draw call, texture and shader state. It is available as an extension for Chrome and Firefox and works with any WebGL engine, including Phaser and Three.js.

Reproduce before you fix

The fastest bug fix starts with a reliable reproduction. If your game records its random seed and the player's inputs per simulation step, any bug report becomes a replay you can run in a test. That is one more payoff of injectable randomness and a fixed-step loop.

What automation can't tell you

Automated tests prove the game boots, plays and doesn't crash across engines. They don't tell you whether it's fun, whether the audio starts on an iPhone with the ringer switch off, whether a swipe fights the browser's own gestures, or whether a three-year-old Android phone holds its frame rate. Before release, play every game on at least one real iPhone and one real Android phone, including inside an in-app browser if your players arrive from social media.

If you want to see a complete test setup rather than build one, every game in our catalog of HTML5 games ships with its tests/ folder, so you can read and run the same unit and end-to-end tests we do.

Frequently asked questions

What is the best way to test a Phaser game automatically?

Keep game rules in plain TypeScript modules and unit-test them with Vitest, which is fast and needs no browser. Add data tests for levels and tuning files, and a small set of Playwright end-to-end tests that boot the production build and play through a level in Chromium, Firefox and WebKit.

Can Playwright test a canvas game?

Yes, but not by finding sprites with locators, because canvas content isn't in the DOM. Expose a test hook behind a URL flag that reports game state and screen positions, click at those positions with page.mouse, and assert on state. HTML menus and dialogs can still use normal locators.

Do I need Phaser's headless mode for unit tests?

Usually not. Phaser has a HEADLESS renderer type that creates no Canvas or WebGL renderer, but the game still runs as a full Phaser instance in a browser-like environment. If the rules live in plain modules, you can test them directly in Node, which is faster and simpler.

How do I debug an HTML5 game on an iPhone?

Enable Web Inspector in Safari's advanced settings on the iPhone, connect it to a Mac, and open the page from the Develop menu in desktop Safari. You get the console, network and storage tools for the page running on the phone.

We make and sell HTML5 game source code; links to our games are marked as ours.