- name
- phaser-game-testing
- description
- Test Phaser games and canvas/WebGL applications with deterministic automation. Plan, implement, and debug frontend tests: unit/integration/E2E/visual/a11y for Phaser 3 games. Use agent-browser CLI for browser automation, Vitest/Jest/RTL, flaky test triage, CI stabilization, and Phaser games needing deterministic input plus screenshot/state assertions. Trigger: "test phaser game", "phaser testing", "game testing", "canvas testing", "webgl testing", "test", "E2E", "flaky", "visual regression", "Playwright".
# Phaser Game Testing
Test Phaser 3 games reliably: enable safe refactors by choosing the right test layer, making canvas/WebGL games observable, and eliminating nondeterminism so failures are actionable.
## Philosophy: Confidence Per Minute
Frontend tests fail for two reasons: the product is broken, or the test is lying. Your job is to maximize signal and minimize "test is lying".
**Before writing a test, ask**:
- What user risk am I covering (money, progression, auth, data loss, crashes)?
- What's the narrowest layer that catches this bug class (pure logic vs UI vs full browser)?
- What nondeterminism exists (time, RNG, async loading, network, animations, fonts, GPU)?
- What "ready" signal can I wait on besides `setTimeout`?
- What should a failure print/screenshot so it's diagnosable in CI?
**Core principles**:
1. **Test the contract, not the implementation**: assert stable user-meaningful outcomes and public seams.
2. **Prefer determinism over retries**: make time/RNG/network controllable; remove flake at the source.
3. **Observe like a debugger**: console errors, network failures, screenshots, and state dumps on failure.
4. **One critical flow first**: a reliable smoke test beats 50 flaky tests.
## Unit Testing for Pure Logic
**Decision Tree**: Is this pure logic? → Use unit tests, not browser automation.
For pure logic utilities (maze generation, score sorting, storage, math algorithms), use **Vitest** for fast, deterministic unit tests. Reserve browser automation (agent-browser) for integration contracts and UI flows.
### What Should Be Unit Tested
- ✅ **Maze generation algorithms** - Deterministic with seeded RNG
- ✅ **Score sorting/validation** - Pure data transformations
- ✅ **Storage utilities** - localStorage wrappers, serialization
- ✅ **Math/algorithm utilities** - Pathfinding, damage calculations
- ✅ **State management logic** - Reducers, state machines
- ✅ **RNG utilities** - Seeded random number generators
### Vitest Setup Pattern
```bash
npm install -D vitest @vitest/ui
```
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
```
```json
// package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui"
}
}
```
### Workflow: Logic Changes
1. **Write unit test first** → Verify logic in isolation
2. **Run tests** → `npm test`
3. **Test in browser if needed** → Only for integration verification
### Anti-Pattern: Browser Automation for Pure Functions
❌ **Wrong**: Using agent-browser to test a maze generation function
```bash
agent-browser eval "generateMaze(10, 10, 42)"
```
✅ **Correct**: Unit test with Vitest
```typescript
import { describe, it, expect } from 'vitest';
import { generateMaze } from './maze';
describe('generateMaze', () => {
it('generates same maze with same seed', () => {
const maze1 = generateMaze(10, 10, 42);
const maze2 = generateMaze(10, 10, 42);
expect(maze1).toEqual(maze2);
});
});
```
See `references/unit-testing-setup.md` for complete Vitest configuration and examples.
## Test Layer Decision Tree
Pick the cheapest layer that provides needed confidence:
| Layer | Speed | Use For |
| ------------- | ----------- | --------------------------------------------------------------------------------- |
| **Unit** | Fastest | Pure functions, reducers, validators, math, pathfinding, deterministic simulation |
| **Component** | Medium | UI behavior with mocked IO (React Testing Library, Vue Testing Library) |
| **E2E** | Slowest | Critical user flows across routing, storage, real bundling/runtime |
| **Visual** | Specialized | Layout/pixel regressions; for canvas/WebGL, only after locking determinism |
## Quick Start: First Smoke Test
1. **Define 1 critical flow**: "page loads → user can start → one key action works"
2. **Add a test seam** to the app (see below)
3. **Choose runner**: agent-browser CLI for E2E, unit tests for logic
4. **Fail loudly**: treat console errors and failed requests as test failures
5. **Stabilize**: seed RNG, freeze time, fix viewport, disable animations
## Concrete agent-browser Workflow: Testing a Game
Step-by-step sequence for testing a Phaser/canvas game:
**Important**: For Phaser games, **skip `snapshot -i`** and use `window.__TEST__` directly.
```
1. agent-browser open http://localhost:3000?test=1&seed=42
2. agent-browser eval "new Promise(r => { const c = () => window.__TEST__?.ready ? r(true) : setTimeout(c, 100); c(); })"
(Wait for game ready)
3. agent-browser errors
(Fail if any errors)
4. agent-browser eval "window.__TEST__.commands.goToScene('GameScene')"
(Use test seam commands instead of DOM clicks)
5. agent-browser eval "window.__TEST__.gameState()"
(Assert game state is correct)
6. agent-browser press ArrowRight
(Or WASD for movement)
7. agent-browser eval "window.__TEST__.gameState().player.x"
(Verify movement happened)
8. agent-browser screenshot gameplay-state.png
(Visual evidence after deterministic setup)
```
**Note**: For DOM-based UI (menus, buttons), you may still use `snapshot -i` and `click @e1`, but prefer test seam commands when available.
## Standardized Test Seam Pattern
**Important**: Agents should **skip `snapshot -i`** for Phaser games and go directly to `window.__TEST__`. The test seam provides all necessary access without DOM inspection.
### TestManager Singleton Pattern
Use a centralized TestManager singleton for consistent test seam management across all scenes:
```javascript
// utils/TestManager.js
class TestManager {
constructor() {
this.ready = false;
this.seed = null;
this.sceneKey = null;
this.scenes = new Map(); // sceneKey -> scene instance
}
registerScene(sceneKey, sceneInstance) {
this.scenes.set(sceneKey, sceneInstance);
this.sceneKey = sceneKey;
}
getCurrentScene() {
return this.scenes.get(this.sceneKey);
}
gameState() {
const scene = this.getCurrentScene();
if (!scene) return { scene: null };
return {
scene: this.sceneKey,
score: scene.gameState?.score || 0,
timer: scene.gameState?.timer || 0,
inventory: scene.gameState?.inventory || [],
// Add scene-specific state via scene.getTestState()
...(scene.getTestState ? scene.getTestState() : {})
};
}
commands = {
clickStartGame: () => {
const scene = this.getCurrentScene();
scene?.startGame?.();
},
collectCoin: (x, y) => {
const scene = this.getCurrentScene();
scene?.collectCoin?.(x, y);
},
goToScene: (key, data) => {
const game = this.getCurrentScene()?.scene?.game;
if (game) {
game.scene.start(key, data);
}
}
};
}
// Initialize singleton
window.__TEST__ = new TestManager();
```
### BaseScene Pattern
Create a BaseScene class that automatically registers with TestManager:
```javascript
// scenes/BaseScene.js
export class BaseScene extends Phaser.Scene {
constructor(config) {
super(config);
}
create() {
// Auto-register with TestManager
if (window.__TEST__) {
window.__TEST__.registerScene(this.scene.key, this);
}
// Scene-specific initialization
this.initScene();
}
// Override in subclasses
initScene() {}
// Override to provide scene-specific test state
getTestState() {
return {};
}
}
```
### Consistent `window.__TEST__` Structure
All scenes should expose the same structure:
```javascript
window.__TEST__ = {
sceneKey: null, // Current active scene (managed by TestManager)
ready: false, // true after first interactive frame
seed: null, // current RNG seed
gameState: () => ({
// JSON-serializable snapshot
scene: window.__TEST__.sceneKey,
score: gameState.score,
timer: gameState.timer,
inventory: gameState.inventory,
// Scene-specific state added via getTestState()
}),
commands: {
// Functional triggers (not raw Phaser API)
clickStartGame: () => {},
collectCoin: (x, y) => {},
goToScene: (key, data) => {},
reset: () => {},
seed: (n) => {},
}
};
```
### Anti-Pattern: Don't Implement `__TEST__` Individually
❌ **Wrong**: Each scene implements its own `window.__TEST__` structure
```javascript
// In GameScene.js
window.__TEST__ = { /* GameScene-specific */ };
// In MenuScene.js
window.__TEST__ = { /* MenuScene-specific */ };
```
✅ **Correct**: Use TestManager + BaseScene for consistent structure
```javascript
// All scenes extend BaseScene
class GameScene extends BaseScene {
getTestState() {
return { player: { x: this.player.x, y: this.player.y } };
}
}
```
### Agent Workflow with Standardized Seams
1. **Skip DOM snapshots**: `agent-browser snapshot -i` is unnecessary
2. **Go directly to test seam**: `agent-browser eval "window.__TEST__.ready"`
3. **Use commands**: `agent-browser eval "window.__TEST__.commands.goToScene('GameScene')"`
4. **Check state**: `agent-browser eval "window.__TEST__.gameState()"`
See `references/test-seam-standardization.md` for complete implementation guide.
## Recommended Test Seams (Legacy Pattern)
For projects not yet using TestManager, the basic pattern still works:
```javascript
window.__TEST__ = {
ready: false, // true after first interactive frame
seed: null, // current RNG seed
sceneKey: null, // current scene/route
state: () => ({
// JSON-serializable snapshot
scene: this.sceneKey,
player: { x, y, hp },
score: gameState.score,
entities: entities.map((e) => ({ id: e.id, type: e.type, x: e.x, y: e.y })),
}),
commands: {
// optional mutation commands
reset: () => {},
seed: (n) => {},
skipIntro: () => {},
},
};
```
**Rule**: Expose IDs + essential fields, not raw Phaser/engine objects.
**Note**: Prefer the TestManager pattern above for new projects.
## Anti-Patterns to Avoid
❌ **Testing the wrong layer**: E2E tests for pure logic
_Why tempting_: "Let's just test everything through the browser"
_Better_: Unit tests for logic; reserve E2E for integration contracts
❌ **Testing implementation details**: Asserting DOM structure/classnames
_Why tempting_: Easy to assert what you can see in DevTools
_Better_: Assert user-meaningful outputs (text, score, HP changes)
❌ **Sleep-driven tests**: `wait 2s then click`
_Why tempting_: Simple and "works on my machine"
_Better_: Wait on explicit readiness (DOM marker, `window.__TEST__.ready`)
❌ **Uncontrolled randomness**: RNG/time in assertions
_Why tempting_: "The game uses random, so the test should too"
_Better_: Seed RNG (`?seed=42`), freeze time, assert stable invariants
❌ **Pixel snapshots without determinism**: Canvas screenshots that flake
_Why tempting_: "I'll catch visual bugs automatically"
_Better_: Deterministic mode first; then screenshot at known stable frames
❌ **Retries as a strategy**: "Just bump retries to 3"
_Why tempting_: Quick fix that makes CI green
_Better_: Fix the flake source; retries hide real problems
## Debugging Failed Tests
When a test fails, gather evidence in this order:
1. **Console errors**: `agent-browser errors` or `agent-browser console`
2. **Network failures**: `agent-browser network requests` → check for non-2xx
3. **Screenshot**: `agent-browser screenshot failure-state.png` → visual state at failure
4. **App state**: `agent-browser eval "window.__TEST__.state()"`
View on GitHub