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:
- •Test the contract, not the implementation: assert stable user-meaningful outcomes and public seams.
- •Prefer determinism over retries: make time/RNG/network controllable; remove flake at the source.
- •Observe like a debugger: console errors, network failures, screenshots, and state dumps on failure.
- •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
npm install -D vitest @vitest/ui
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
// package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui"
}
}
Workflow: Logic Changes
- •Write unit test first → Verify logic in isolation
- •Run tests →
npm test - •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
agent-browser eval "generateMaze(10, 10, 42)"
✅ Correct: Unit test with Vitest
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
- •Define 1 critical flow: "page loads → user can start → one key action works"
- •Add a test seam to the app (see below)
- •Choose runner: agent-browser CLI for E2E, unit tests for logic
- •Fail loudly: treat console errors and failed requests as test failures
- •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:
// 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:
// 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:
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
// In GameScene.js
window.__TEST__ = { /* GameScene-specific */ };
// In MenuScene.js
window.__TEST__ = { /* MenuScene-specific */ };
✅ Correct: Use TestManager + BaseScene for consistent structure
// All scenes extend BaseScene
class GameScene extends BaseScene {
getTestState() {
return { player: { x: this.player.x, y: this.player.y } };
}
}
Agent Workflow with Standardized Seams
- •Skip DOM snapshots:
agent-browser snapshot -iis unnecessary - •Go directly to test seam:
agent-browser eval "window.__TEST__.ready" - •Use commands:
agent-browser eval "window.__TEST__.commands.goToScene('GameScene')" - •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:
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:
- •Console errors:
agent-browser errorsoragent-browser console - •Network failures:
agent-browser network requests→ check for non-2xx - •Screenshot:
agent-browser screenshot failure-state.png→ visual state at failure - •App state:
agent-browser eval "window.__TEST__.state()" - •Classify the flake (see references/flake-reduction.md):
- •Readiness? → add explicit wait
- •Timing? → control animation/physics
- •Environment? → lock viewport/DPR
- •Data? → isolate test data
Graduation Criteria: When Is Testing "Enough"?
Minimum viable test suite:
- • 1 smoke test that proves the app loads and primary action works
- • Test seam exists (
window.__TEST__with ready flag and state) - • Deterministic mode for canvas/games (
?test=1enables seeding) - • Console errors fail tests (no silent failures)
- • CI runs tests on every push
Level up when:
- •Critical paths (auth, payment, save/load) have dedicated E2E
- •Unit tests cover complex logic (pathfinding, damage calc, state machines)
- •Visual regression on key screens (menu, HUD) with locked determinism
Visual Regression with imgdiff.py
For pixel comparison of screenshots:
# Compare baseline to current python scripts/imgdiff.py baseline.png current.png --out diff.png # Allow small tolerance (anti-aliasing differences) python scripts/imgdiff.py baseline.png current.png --max-rms 2.0
Exit codes: 0 = identical, 1 = different, 2 = error
UI Slicing Regressions (Nine-Slice / Ribbons / Bars)
Canvas UI issues (panel seams, segmented ribbons, invisible HUD fills) are best caught with a dedicated UI harness instead of the full gameplay flow.
- •Build a simple
test.html/scene that loads only the UI assets. - •Render raw slices next to assembled panels (multi-size), and include ribbon/bars with both “raw crop + scale” and “stitched multi-slice” views.
- •Expose
window.__TEST__with.commands.showTest(n)so agent-browser can toggle each mode deterministically. - •Capture targeted screenshots (panels, ribbons, bars) and diff them in CI.
See references/phaser-canvas-testing.md for the deterministic setup + screenshot workflow.
For general Phaser UI components (not just slicing), use the same idea via standalone component test scenes (phaser-component-test-scenes skill): one scene per component, test via ?scene=ComponentNameTestScene.
Direct Scene Access for Testing
For testing specific scenes without navigating the full game flow, use URL parameters to start directly at a scene.
URL Parameter Pattern
http://localhost:3000?scene=GameScene&test=1&seed=42
Implementation in main.ts
// main.ts
const params = new URLSearchParams(window.location.search);
const sceneParam = params.get('scene');
const isTestMode = params.has('test');
const seedParam = params.get('seed');
const config: Phaser.Types.Core.GameConfig = {
// ... other config
scene: sceneParam
? [sceneParam] // Start directly at specified scene
: [BootScene, PreloaderScene, MenuScene, GameScene], // Normal flow
};
const game = new Phaser.Game(config);
// If test mode, initialize with seed
if (isTestMode && seedParam) {
const seed = parseInt(seedParam);
seedRNG(seed);
window.__TEST__.seed = seed;
}
TestManager Integration
// In TestManager
commands: {
goToScene: (key, data) => {
const game = this.getCurrentScene()?.scene?.game;
if (game) {
game.scene.start(key, data);
}
}
}
Agent Workflow
# Test specific scene directly
agent-browser open http://localhost:3000?scene=GameScene&test=1&seed=42
# Or navigate via test seam
agent-browser eval "window.__TEST__.commands.goToScene('GameScene', { level: 1 })"
Workflow: For testing specific scenes, use ?scene=SceneName instead of navigating full game flow.
Variation Guidance
Adapt approach based on context:
- •DOM app: Standard agent-browser selectors, wait for text/elements
- •Canvas game: Test seams mandatory, wait via
window.__TEST__.ready - •Hybrid: DOM for menus, test seams for gameplay
- •CI-only GPU: May need software rendering flags or skip visual tests
- •UI slicing regressions: For nine-slice/ribbon/bar artifacts, prefer a small UI harness scene/page with deterministic modes and targeted screenshots (
references/phaser-canvas-testing.md).
Test Seam Discovery
Always check for window.__TEST__ before DOM interactions
Test seams are PRIMARY method for Phaser game testing:
- •Each scene creates its own test seam in
create()method - •Test seam
sceneKeymay lag on scene transitions (use console logs as fallback) - •Check source code for available test seam commands
- •Document discovered commands in progress.txt
Standard Readiness Check Patterns
Use exponential backoff for test seam discovery:
# Standard readiness check with exponential backoff
wait_for_test_seam() {
local max_attempts=5
local attempt=0
while [ $attempt -lt $max_attempts ]; do
local delay=$((2 ** $attempt)) # 1s, 2s, 4s, 8s, 16s
sleep $delay
if agent-browser eval "typeof window.__TEST__ !== 'undefined' && typeof window.__TEST__.commands !== 'undefined'"; then
echo "Test seam ready"
return 0
fi
attempt=$((attempt + 1))
done
echo "Test seam not available after $max_attempts attempts"
return 1
}
JavaScript pattern with exponential backoff:
// Exponential backoff readiness check
async function waitForTestSeam(maxAttempts = 5) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s
await sleep(delay);
const isReady = await checkTestSeamReady();
if (isReady) {
return true;
}
}
return false;
}
Retry Strategy Guidance
Retry patterns for test seam discovery:
# Retry with exponential backoff
max_retries=5
attempt=0
while [ $attempt -lt $max_retries ]; do
if agent-browser eval "window.__TEST__?.ready"; then
echo "Test seam ready"
break
fi
attempt=$((attempt + 1))
sleep $((2 ** $attempt)) # Exponential backoff: 2s, 4s, 8s, 16s, 32s
done
Discovery workflow:
- •Check for
window.__TEST__availability with exponential backoff - •If not available, check source code for test seam setup
- •Look for
window.__TEST__.commandsin scene files - •Document available commands
- •Retry with exponential backoff if initial check fails
Test Seam Patterns
Direct Property Access (Preferred)
PREFERRED: Use direct property checks instead of polling readiness flags
# ✅ CORRECT: Direct property check
agent-browser eval "window.__TEST__?.sceneKey === 'GameScene'"
agent-browser eval "window.__TEST__?.commands?.clickStartGame"
# ❌ INEFFICIENT: Polling readiness flag
agent-browser eval "
new Promise((resolve) => {
const check = () => {
if (window.__TEST__?.ready) {
resolve(true);
} else {
setTimeout(check, 100);
}
};
check();
})
"
Why Direct Property Access is Better:
- •Immediate check (no polling delay)
- •More reliable (readiness flags may not be reliable)
- •Simpler code (no Promise chains)
- •Faster execution (no async overhead)
Readiness Flag Reliability
Important: window.__TEST__?.ready may not be reliable. Prefer direct property checks:
# ✅ PREFERRED: Direct property check
agent-browser eval "window.__TEST__?.sceneKey || false"
agent-browser eval "Object.keys(window.__TEST__?.commands || {}).length > 0"
# ⚠️ LESS RELIABLE: Readiness flag
agent-browser eval "window.__TEST__?.ready || false"
When Readiness Flags May Not Work:
- •Scene transitions (flag may lag)
- •Complex initialization (flag may not update)
- •Test seam setup issues (flag may not be set)
Solution: Use direct property checks instead
Timeout Handling
Include timeout handling (max 5 seconds) before fallback:
# Timeout-based check with direct property access
check_test_seam_with_timeout() {
local max_wait=5 # 5 seconds max
local elapsed=0
while [ $elapsed -lt $max_wait ]; do
if agent-browser eval "window.__TEST__?.sceneKey || false"; then
echo "Test seam available"
return 0
fi
sleep 1
elapsed=$((elapsed + 1))
done
echo "Test seam not available after $max_wait seconds"
return 1
}
If test seam isn't available after timeout:
- •Document limitation in progress.txt
- •Proceed with alternative verification (code review, TypeScript compilation)
- •Don't wait indefinitely - test seams may not be available in all contexts
Framework-Specific Command References
Common test seam commands by framework:
Phaser 3:
- •
window.__TEST__.commands.goToScene(key, data) - •
window.__TEST__.commands.gameState() - •
window.__TEST__.commands.setTimer(seconds) - •
window.__TEST__.sceneKey(current scene)
React/Web Apps:
- •
window.__TEST__.commands.navigate(route) - •
window.__TEST__.commands.getState() - •
window.__TEST__.currentRoute
Graceful Degradation:
- •If test seams unavailable: Use DOM inspection or code review
- •Document limitation: Note why test seam wasn't used
- •Alternative verification: TypeScript compilation, code review
Coordinate System Documentation
World Coordinates vs Screen Coordinates
Understanding Phaser coordinate systems is critical for UI positioning tasks.
World Coordinates:
- •Game world space (e.g., 800x600 game world)
- •Camera-independent (objects exist in world space)
- •Used for game objects, sprites, physics bodies
- •Example:
sprite.x = 400(400 pixels from world origin)
Screen Coordinates:
- •Viewport/camera space (what player sees)
- •Camera-dependent (changes with camera scroll)
- •Used for UI elements, HUD, overlays
- •Example:
ui.x = 400(400 pixels from screen origin)
Key Difference:
// World coordinates (game object) sprite.x = 400; // 400 pixels in world space // Screen coordinates (UI element) ui.x = 400; // 400 pixels from screen edge (camera-independent)
Camera Scroll Offset Handling
When calculating positions, account for camera scroll:
// ❌ WRONG: Not accounting for camera scroll const worldX = 400; sprite.x = worldX; // May be off-screen if camera scrolled // ✅ CORRECT: Account for camera scroll const cameraX = this.cameras.main.scrollX; const worldX = 400; sprite.x = cameraX + worldX; // Correct position relative to camera
For UI Elements (Screen Coordinates):
// UI elements use screen coordinates (camera-independent) ui.x = 400; // Always 400 pixels from screen edge, regardless of camera
Position Calculation Patterns
Pattern 1: Center Text on Screen
// Calculate text width first const textWidth = text.width; const screenWidth = this.cameras.main.width; const centerX = (screenWidth - textWidth) / 2; text.x = centerX; // Center text horizontally
Pattern 2: Position Relative to Another Object
// Position button below text const textBottom = text.y + text.height; const spacing = 20; button.y = textBottom + spacing;
Pattern 3: Account for Origin
// Sprite origin affects position calculation sprite.setOrigin(0.5, 0.5); // Center origin sprite.x = 400; // Center of sprite at x=400 // If origin is (0, 0), sprite.x is top-left corner sprite.setOrigin(0, 0); sprite.x = 400; // Top-left corner at x=400
Common Gotchas About Position Calculations
Gotcha 1: Not Accounting for Text Width
// ❌ WRONG: Assuming fixed width text.x = 400; // May not be centered if text width varies // ✅ CORRECT: Calculate based on actual width const textWidth = text.width; const centerX = (screenWidth - textWidth) / 2; text.x = centerX;
Gotcha 2: Confusing World vs Screen Coordinates
// ❌ WRONG: Using world coordinates for UI ui.x = sprite.x; // UI will move with camera scroll // ✅ CORRECT: Use screen coordinates for UI ui.x = 400; // UI stays fixed on screen
Gotcha 3: Not Accounting for Origin
// ❌ WRONG: Assuming origin is (0, 0) sprite.x = 100; // May not be where expected if origin is (0.5, 0.5) // ✅ CORRECT: Account for origin sprite.setOrigin(0.5, 0.5); sprite.x = 100; // Center of sprite at x=100
WebGL Warning Handling
Known Non-Critical WebGL Warnings
Some WebGL warnings are non-critical and can be ignored:
Warning: WebGL context lost
- •When to ignore: During development, if game still works
- •When to investigate: If game stops working or performance degrades
- •Common cause: Browser resource limits, GPU driver issues
Warning: Texture size exceeds maximum
- •When to ignore: If texture is automatically scaled down
- •When to investigate: If texture quality is unacceptable
- •Common cause: Very large textures, old GPU
Warning: Shader compilation failed
- •When to ignore: Never - this is always critical
- •Action: Always investigate shader compilation failures
- •Common cause: Shader syntax errors, unsupported features
When to Ignore vs Investigate Warnings
Ignore WebGL warnings when:
- •Game functions correctly despite warning
- •Warning is known browser/GPU limitation
- •Warning doesn't affect gameplay
- •Performance is acceptable
Investigate WebGL warnings when:
- •Game stops working or crashes
- •Performance degrades significantly
- •Visual artifacts appear
- •Shader compilation fails
- •Texture quality is unacceptable
WebGL Capability Detection Patterns
Check WebGL support before testing:
# Check WebGL support
agent-browser eval "
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
gl ? 'WebGL supported' : 'WebGL not supported'
"
Check WebGL context:
# Check if WebGL context is active
agent-browser eval "
const game = window.__TEST__?.getCurrentScene()?.scene?.game;
if (game) {
const renderer = game.renderer;
renderer.gl ? 'WebGL active' : 'Canvas2D fallback'
} else {
'Game not initialized'
}
"
Sprite Origin Adjustments
Sprite Textures May Not Be Visually Centered
Important: Sprite textures may not be visually centered even with origin (0.5, 0.5).
Why This Happens:
- •Texture has transparent padding
- •Texture has uneven padding
- •Texture dimensions don't match visual content
Solution: Adjust origin or reposition sprite
Origin Adjustment Patterns
Pattern 1: Fine-Tune Origin
// Standard center origin sprite.setOrigin(0.5, 0.5); // Fine-tune if visually off-center sprite.setOrigin(0.4, 0.5); // Slightly left of center sprite.setOrigin(0.6, 0.5); // Slightly right of center
Pattern 2: Adjust Position Instead
// If origin adjustment doesn't work, adjust position sprite.setOrigin(0.5, 0.5); sprite.x = targetX + offsetX; // Add offset to compensate sprite.y = targetY + offsetY;
Pattern 3: Measure and Calculate
// Measure visual bounds const visualWidth = sprite.width; // May differ from texture width const visualHeight = sprite.height; // Calculate offset const offsetX = (textureWidth - visualWidth) / 2; const offsetY = (textureHeight - visualHeight) / 2; // Adjust position sprite.x = targetX + offsetX; sprite.y = targetY + offsetY;
When to Adjust Origins vs Reposition Sprites
Adjust origin when:
- •Sprite is consistently off-center
- •Offset is consistent across sprites
- •You want to change anchor point permanently
Reposition sprite when:
- •Offset varies by sprite
- •You need precise pixel positioning
- •Origin adjustment doesn't work
Example:
// ✅ CORRECT: Adjust origin for consistent offset sprite.setOrigin(0.4, 0.5); // All sprites use this origin // ✅ CORRECT: Reposition for precise placement sprite.setOrigin(0.5, 0.5); sprite.x = targetX + 5; // 5 pixel offset for this sprite
Common Patterns
Pattern 1: Successful UI Layout Calculation
Example from real task:
// Calculate text width first const textWidth = this.add.text(0, 0, "Score: 100", style).width; const screenWidth = this.cameras.main.width; // Center text horizontally const centerX = (screenWidth - textWidth) / 2; text.x = centerX; // Position below with spacing const spacing = 20; button.y = text.y + text.height + spacing;
Key Points:
- •Calculate text width before positioning
- •Account for screen width (not world width)
- •Use spacing constants for consistency
Pattern 2: Successful Coordinate System Usage
Example from real task:
// World coordinates for game object player.x = 400; // 400 pixels in world space // Screen coordinates for UI scoreText.x = 100; // 100 pixels from screen edge (camera-independent) // Account for camera scroll when needed const cameraX = this.cameras.main.scrollX; enemy.x = cameraX + 500; // 500 pixels ahead of camera
Key Points:
- •Use world coordinates for game objects
- •Use screen coordinates for UI
- •Account for camera scroll when needed
Pattern 3: Successful Origin Handling
Example from real task:
// Set origin first
sprite.setOrigin(0.5, 0.5);
// Position at target
sprite.x = 400;
sprite.y = 300;
// Fine-tune if visually off-center
if (sprite appears off-center) {
sprite.setOrigin(0.4, 0.5); // Adjust origin
// OR
sprite.x += 5; // Adjust position
}
Key Points:
- •Set origin before positioning
- •Fine-tune if visually off-center
- •Use origin adjustment or position offset
Troubleshooting: Coordinate Confusion
Problem: UI Element Not Where Expected
Symptoms:
- •UI element appears in wrong location
- •UI element moves with camera scroll
- •UI element position changes unexpectedly
Diagnosis:
- •Check if using world vs screen coordinates
- •Verify origin is set correctly
- •Check if camera scroll is affecting position
Solution:
// For UI elements, use screen coordinates ui.x = 100; // Screen coordinate (camera-independent) // If using world coordinates, account for camera const cameraX = this.cameras.main.scrollX; ui.x = cameraX + 100; // World coordinate (camera-dependent)
Problem: Text Not Centered
Symptoms:
- •Text appears off-center
- •Text position changes with text content
Diagnosis:
- •Check if text width is calculated
- •Verify center calculation is correct
- •Check if origin is set correctly
Solution:
// Calculate text width first const textWidth = text.width; const screenWidth = this.cameras.main.width; // Center calculation const centerX = (screenWidth - textWidth) / 2; text.x = centerX; // Set origin to left (default for text) text.setOrigin(0, 0.5); // Left-aligned, vertically centered
Problem: Sprite Position Wrong After Origin Change
Symptoms:
- •Sprite moves when origin is changed
- •Sprite position doesn't match expected location
Diagnosis:
- •Origin change affects position calculation
- •Position needs adjustment after origin change
Solution:
// Set origin first sprite.setOrigin(0.5, 0.5); // Then set position sprite.x = 400; sprite.y = 300; // If position is wrong, adjust sprite.x += offsetX; sprite.y += offsetY;
Complete Test Seam Command Reference by Scene
Reference: See phaser-test-seam-patterns skill for comprehensive command catalog.
MainMenu Scene
- •
clickStartGame()- Navigate to GameScene - •
clickHighScores()- Navigate to HighScoresScene - •
clickSettings()- Navigate to SettingsScene
GameScene
- •
setTimer(seconds)- Set timer to specific value - •
fastForwardTimer(seconds)- Fast forward timer - •
triggerGameOver()- Force game over state - •
movePlayerTo(x, y)- Move player to position - •
movePlayerToExit()- Move player to exit (complete level) - •
collectAnyCoin()- Collect nearest coin - •
collectCoin(x, y)- Collect coin at position - •
gameState()- Get current game state (score, timer, player position)
GameOverScene
- •
clickPlayAgain()- Restart game - •
clickMainMenu()- Navigate to MainMenu - •
getFinalScore()- Get final score
HighScoresScene
- •
clickMainMenu()- Navigate to MainMenu - •
getHighScores()- Get high scores list
Common Commands (All Scenes)
- •
goToScene(key, data)- Navigate to any scene - •
gameState()- Get current game state - •
reset()- Reset game state - •
seed(n)- Set RNG seed
Command Discovery Patterns:
- •Check scene
create()method forwindow.__TEST__.commandsdefinition - •Look for test seam setup in scene files
- •Check for TestManager singleton pattern (centralized commands)
- •Document scene-specific commands in progress.txt
Scene Navigation Workflows:
# Navigate from MainMenu to GameScene
agent-browser eval "window.__TEST__.commands.clickStartGame()"
# Wait for transition
agent-browser wait 2000
# Verify scene change (use console logs as fallback)
agent-browser console
# Navigate directly to scene
agent-browser eval "window.__TEST__.commands.goToScene('GameScene', { level: 1 })"
Common Testing Scenario Templates:
Scenario 1: Test Game Flow
# Start at MainMenu agent-browser open http://localhost:3000?scene=MainMenu&test=1&seed=42 # Navigate to game agent-browser eval "window.__TEST__.commands.clickStartGame()" agent-browser wait 2000 # Set timer for quick testing agent-browser eval "window.__TEST__.commands.setTimer(5)" # Collect coin agent-browser eval "window.__TEST__.commands.collectAnyCoin()" # Verify score updated agent-browser eval "window.__TEST__.gameState().score"
Scenario 2: Test Game Over
# Start at GameScene agent-browser open http://localhost:3000?scene=GameScene&test=1&seed=42 # Trigger game over agent-browser eval "window.__TEST__.commands.triggerGameOver()" agent-browser wait 2000 # Verify GameOverScene agent-browser eval "window.__TEST__.sceneKey" # Test play again agent-browser eval "window.__TEST__.commands.clickPlayAgain()"
Test Seam Debugging Patterns:
- •If command not found: Check scene source code for command definition
- •If sceneKey doesn't update: Use console logs as fallback verification
- •If command fails: Check if scene is initialized (wait for
window.__TEST__.ready) - •If navigation fails: Verify scene key spelling matches Phaser scene registration
Scene Transition Testing
Use test seam commands for navigation:
- •
clickStartGame()- Navigate to game - •
clickPlayAgain()- Restart game - •
goToScene(key, data)- Navigate to any scene directly
Scene Navigation Patterns
Pattern 1: Direct Scene Navigation
# Navigate directly to scene (preferred)
agent-browser eval "window.__TEST__.commands.goToScene('GameScene', { level: 1 })"
agent-browser wait 500 # Minimal wait for transition
agent-browser eval "window.__TEST__.sceneKey === 'GameScene'"
Pattern 2: Navigation via UI Commands
# Navigate via UI command agent-browser eval "window.__TEST__.commands.clickStartGame()" agent-browser wait 500 # Minimal wait for transition # Verify with console logs (fallback if sceneKey lags) agent-browser console
Pattern 3: Scene Navigation with Retry
# Navigate with retry on failure
navigate_to_scene() {
local scene=$1
local max_attempts=3
local attempt=0
while [ $attempt -lt $max_attempts ]; do
agent-browser eval "window.__TEST__.commands.goToScene('$scene')"
agent-browser wait 500
if agent-browser eval "window.__TEST__.sceneKey === '$scene'"; then
echo "Successfully navigated to $scene"
return 0
fi
attempt=$((attempt + 1))
sleep $((2 ** $attempt)) # Exponential backoff
done
echo "Failed to navigate to $scene after $max_attempts attempts"
return 1
}
Wait patterns:
- •Wait 500ms after transition (minimal wait, not 2 seconds)
- •Use console logs to verify scene transitions
- •Don't rely solely on test seam
sceneKey(known to lag) - •Retry navigation with exponential backoff if needed
Pattern:
# Navigate to game agent-browser eval "window.__TEST__.commands.clickStartGame()" # Wait for transition (minimal wait) agent-browser wait 500 # Verify with console logs (fallback) agent-browser console
Timer Testing
Use test seam setTimer(seconds) for direct manipulation
Never wait for natural countdown - use timer manipulation:
- •Set timer to low value (e.g., 3 seconds) for quick testing
- •Test boundary conditions (9, 10, 11 seconds for color changes)
- •Add
triggerGameOver()command for testing
Pattern:
# Set timer to 5 seconds agent-browser eval "window.__TEST__.commands.setTimer(5)" # Fast forward timer agent-browser eval "window.__TEST__.commands.fastForwardTimer(10)" # Trigger game over agent-browser eval "window.__TEST__.commands.triggerGameOver()"
Movement Testing (Enhanced)
Phaser requires keydown/keyup pattern, not single press
Pattern:
# Movement requires keydown/keyup agent-browser keydown ArrowRight agent-browser wait 500 agent-browser keyup ArrowRight
Use test seam movePlayerTo(position) when available:
# Direct position setting (preferred) agent-browser eval "window.__TEST__.commands.movePlayerTo(100, 200)"
Test collision detection:
- •Test movement through open areas (should work)
- •Test movement into walls (should be blocked)
- •Verify position updates correctly
Common Test Seam Commands
- •
clickStartGame()- Navigate to game - •
movePlayerToExit()- Complete level - •
setTimer(seconds)- Manipulate timer - •
collectAnyCoin()- Test coin collection - •
gameState()- Access game state - •
triggerGameOver()- Force game over
See references/test-seam-commands.md for common commands catalog.
Bundled Resources
Read these when needed:
- •
references/agent-browser-cheatsheet.md: Detailed agent-browser CLI patterns - •
references/phaser-canvas-testing.md: Deterministic mode for Phaser games - •
references/flake-reduction.md: Flake classification and fixes - •
references/test-seam-commands.md: Common test seam commands catalog
Composite Test Functions
Create composite test functions for common flows to reduce command count:
// Add composite test functions to test seam
window.__TEST__.commands.testPlayAgainFlow = () => {
// Complete play again scenario in one call
this.scene.start('GameScene');
this.setTimer(5);
this.collectAnyCoin();
return this.gameState();
};
window.__TEST__.commands.testLevelCompleteFlow = () => {
// Navigate to level complete
this.movePlayerToExit();
return this.gameState();
};
window.__TEST__.commands.testSceneTransition = (from, to) => {
// Scene navigation testing
this.scene.start(to);
return { from, to, sceneKey: this.scene.key };
};
window.__TEST__.commands.testGameStateReset = () => {
// Verify state clearing
this.reset();
return this.gameState();
};
Usage in browser testing:
# Single composite function call instead of 10+ individual commands agent-browser eval "window.__TEST__.commands.testPlayAgainFlow()"
Test Seam Readiness Patterns
Use direct property checks instead of Promise polling:
// ❌ INEFFICIENT: Promise-based polling
agent-browser eval "
new Promise((resolve) => {
const check = () => {
if (window.__TEST__?.ready) {
resolve(true);
} else {
setTimeout(check, 100);
}
};
check();
})
"
// ✅ EFFICIENT: Direct property check
agent-browser eval "window.__TEST__?.ready || false"
Check window.TEST?.sceneKey directly:
# Direct check, no Promise polling agent-browser eval "window.__TEST__?.sceneKey === 'GameScene'"
Use Object.keys() for verification:
# Check command availability
agent-browser eval "Object.keys(window.__TEST__?.commands || {}).includes('clickStartGame')"
Error Scenario Testing
Add test seam commands for error injection:
// Force error conditions for testing
window.__TEST__.commands.forceMazeFailure = () => {
// Force maze generation to fail
this.mazeGenerator.forceFailure = true;
};
window.__TEST__.commands.restoreMazeGeneration = () => {
// Restore normal maze generation
this.mazeGenerator.forceFailure = false;
};
Test error handling paths:
# Test error scenario agent-browser eval "window.__TEST__.commands.forceMazeFailure()" agent-browser eval "window.__TEST__.commands.generateMaze()" # Verify fallback behavior agent-browser eval "window.__TEST__.commands.restoreMazeGeneration()"
Testing Phaser Scaling Configuration
CRITICAL: Verify games use Phaser Scale Manager, not manual JavaScript scaling
Scaling Configuration Verification
Test that the game uses Phaser's Scale Manager correctly:
# Verify Scale Manager is configured
agent-browser eval "
const game = window.__TEST__?.getCurrentScene()?.scene?.game;
if (game) {
const scale = game.scale;
JSON.stringify({
mode: scale.scaleMode,
gameSize: { width: scale.gameSize.width, height: scale.gameSize.height },
displaySize: { width: scale.displaySize.width, height: scale.displaySize.height },
autoCenter: scale.autoCenter,
usingScaleManager: scale.scaleMode !== Phaser.Scale.NONE
})
} else {
'Game not initialized'
}
"
Anti-Pattern Detection: Manual Scaling
Check for manual JavaScript/CSS scaling anti-patterns:
# Check for manual CSS transforms or width/height styles on canvas
agent-browser eval "
const canvas = document.querySelector('canvas');
if (canvas) {
const style = window.getComputedStyle(canvas);
JSON.stringify({
hasTransform: style.transform !== 'none',
hasWidthPercent: style.width.includes('%'),
hasHeightPercent: style.height.includes('%'),
hasManualScaling: style.transform !== 'none' ||
style.width.includes('%') ||
style.height.includes('%')
})
} else {
'Canvas not found'
}
"
Viewport Size Testing
Test game scaling across different viewport sizes:
# Test at different viewport sizes
agent-browser eval "window.innerWidth = 1280; window.innerHeight = 720; window.dispatchEvent(new Event('resize'))"
agent-browser wait 500
agent-browser eval "window.__TEST__?.getCurrentScene()?.scene?.game?.scale?.displaySize"
# Test at mobile size
agent-browser eval "window.innerWidth = 375; window.innerHeight = 667; window.dispatchEvent(new Event('resize'))"
agent-browser wait 500
agent-browser eval "window.__TEST__?.getCurrentScene()?.scene?.game?.scale?.displaySize"
Scaling Test Seam Commands
Add test seam commands for scaling verification:
// In your scene's test seam setup
window.__TEST__.commands.getScaleInfo = () => {
const game = this.scene.game;
return {
mode: game.scale.scaleMode,
gameSize: { width: game.scale.gameSize.width, height: game.scale.gameSize.height },
displaySize: { width: game.scale.displaySize.width, height: game.scale.displaySize.height },
scaleX: game.scale.displaySize.width / game.scale.gameSize.width,
scaleY: game.scale.displaySize.height / game.scale.gameSize.height,
};
};
window.__TEST__.commands.testResize = (width: number, height: number) => {
window.innerWidth = width;
window.innerHeight = height;
window.dispatchEvent(new Event('resize'));
return this.commands.getScaleInfo();
};
Expected Scaling Behavior
When testing scaling, verify:
- •Scale Manager is active:
scale.scaleMode !== Phaser.Scale.NONE - •Aspect ratio preserved: For
FITmode, game maintains aspect ratio - •Auto-centering works: Game is centered in viewport
- •Resize events handled: Window resize updates game size correctly
- •No manual CSS scaling: Canvas element has no transform or percentage width/height
Common Scaling Issues to Test
- •Game too small: Verify scale mode and base dimensions
- •Game distorted: Check aspect ratio preservation
- •Game off-center: Verify
autoCenterconfiguration - •Input misaligned: Indicates manual scaling breaking coordinate system
- •Resize not working: Check Scale Manager event handling
Browser Testing Optimization
Batch related commands:
// Batch independent checks in single eval
agent-browser eval "
const state = window.__TEST__.gameState();
JSON.stringify({
score: state.score,
timer: state.timer,
playerX: state.player?.x,
playerY: state.player?.y
})
"
Use parallel evaluation where possible:
// Use Promise.all() for parallel checks
agent-browser eval "
Promise.all([
Promise.resolve(window.__TEST__.gameState().score),
Promise.resolve(window.__TEST__.gameState().timer)
]).then(results => ({ score: results[0], timer: results[1] }))
"
Reduce wait times between commands:
# Use minimal waits (500ms for scene transitions)
agent-browser eval "window.__TEST__.commands.goToScene('GameScene')"
agent-browser wait 500 # Not 2000ms
Remember
You can make almost any frontend (including canvas/WebGL games) testable by adding a tiny, stable seam for readiness + state. One reliable smoke test is the foundation. Aim for tests that are boring to maintain: deterministic, explicit about readiness, and rich in failure evidence. The goal is confidence, not coverage numbers.