| 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:
- 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
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
{
"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:
class TestManager {
constructor() {
this.ready = false;
this.seed = null;
this.sceneKey = null;
this.scenes = new Map();
}
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 || [],
...(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);
}
}
};
}
window.__TEST__ = new TestManager();
BaseScene Pattern
Create a BaseScene class that automatically registers with TestManager:
export class BaseScene extends Phaser.Scene {
constructor(config) {
super(config);
}
create() {
if (window.__TEST__) {
window.__TEST__.registerScene(this.scene.key, this);
}
this.initScene();
}
initScene() {}
getTestState() {
return {};
}
}
Consistent window.__TEST__ Structure
All scenes should expose the same structure:
window.__TEST__ = {
sceneKey: null,
ready: false,
seed: null,
gameState: () => ({
scene: window.__TEST__.sceneKey,
score: gameState.score,
timer: gameState.timer,
inventory: gameState.inventory,
}),
commands: {
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
window.__TEST__ = { };
window.__TEST__ = { };
✅ Correct: Use TestManager + BaseScene for consistent structure
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 -i is 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,
seed: null,
sceneKey: null,
state: () => ({
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: {
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 errors or agent-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:
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:
python scripts/imgdiff.py baseline.png current.png --out diff.png
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
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 = {
scene: sceneParam
? [sceneParam]
: [BootScene, PreloaderScene, MenuScene, GameScene],
};
const game = new Phaser.Game(config);
if (isTestMode && seedParam) {
const seed = parseInt(seedParam);
seedRNG(seed);
window.__TEST__.seed = seed;
}
TestManager Integration
commands: {
goToScene: (key, data) => {
const game = this.getCurrentScene()?.scene?.game;
if (game) {
game.scene.start(key, data);
}
}
}
Agent Workflow
agent-browser open http://localhost:3000?scene=GameScene&test=1&seed=42
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
sceneKey may 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:
wait_for_test_seam() {
local max_attempts=5
local attempt=0
while [ $attempt -lt $max_attempts ]; do
local delay=$((2 ** $attempt))
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:
async function waitForTestSeam(maxAttempts = 5) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const delay = Math.pow(2, attempt) * 1000;
await sleep(delay);
const isReady = await checkTestSeamReady();
if (isReady) {
return true;
}
}
return false;
}
Retry Strategy Guidance
Retry patterns for test seam discovery:
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))
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__.commands in 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
agent-browser eval "window.__TEST__?.sceneKey === 'GameScene'"
agent-browser eval "window.__TEST__?.commands?.clickStartGame"
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:
agent-browser eval "window.__TEST__?.sceneKey || false"
agent-browser eval "Object.keys(window.__TEST__?.commands || {}).length > 0"
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:
check_test_seam_with_timeout() {
local max_wait=5
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:
sprite.x = 400;
ui.x = 400;
Camera Scroll Offset Handling
When calculating positions, account for camera scroll:
const worldX = 400;
sprite.x = worldX;
const cameraX = this.cameras.main.scrollX;
const worldX = 400;
sprite.x = cameraX + worldX;
For UI Elements (Screen Coordinates):
ui.x = 400;
Position Calculation Patterns
Pattern 1: Center Text on Screen
const textWidth = text.width;
const screenWidth = this.cameras.main.width;
const centerX = (screenWidth - textWidth) / 2;
text.x = centerX;
Pattern 2: Position Relative to Another Object
const textBottom = text.y + text.height;
const spacing = 20;
button.y = textBottom + spacing;
Pattern 3: Account for Origin
sprite.setOrigin(0.5, 0.5);
sprite.x = 400;
sprite.setOrigin(0, 0);
sprite.x = 400;
Common Gotchas About Position Calculations
Gotcha 1: Not Accounting for Text Width
text.x = 400;
const textWidth = text.width;
const centerX = (screenWidth - textWidth) / 2;
text.x = centerX;
Gotcha 2: Confusing World vs Screen Coordinates
ui.x = sprite.x;
ui.x = 400;
Gotcha 3: Not Accounting for Origin
sprite.x = 100;
sprite.setOrigin(0.5, 0.5);
sprite.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:
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:
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
sprite.setOrigin(0.5, 0.5);
sprite.setOrigin(0.4, 0.5);
sprite.setOrigin(0.6, 0.5);
Pattern 2: Adjust Position Instead
sprite.setOrigin(0.5, 0.5);
sprite.x = targetX + offsetX;
sprite.y = targetY + offsetY;
Pattern 3: Measure and Calculate
const visualWidth = sprite.width;
const visualHeight = sprite.height;
const offsetX = (textureWidth - visualWidth) / 2;
const offsetY = (textureHeight - visualHeight) / 2;
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:
sprite.setOrigin(0.4, 0.5);
sprite.setOrigin(0.5, 0.5);
sprite.x = targetX + 5;
Common Patterns
Pattern 1: Successful UI Layout Calculation
Example from real task:
const textWidth = this.add.text(0, 0, "Score: 100", style).width;
const screenWidth = this.cameras.main.width;
const centerX = (screenWidth - textWidth) / 2;
text.x = centerX;
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:
player.x = 400;
scoreText.x = 100;
const cameraX = this.cameras.main.scrollX;
enemy.x = cameraX + 500;
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:
sprite.setOrigin(0.5, 0.5);
sprite.x = 400;
sprite.y = 300;
if (sprite appears off-center) {
sprite.setOrigin(0.4, 0.5);
sprite.x += 5;
}
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:
ui.x = 100;
const cameraX = this.cameras.main.scrollX;
ui.x = cameraX + 100;
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:
const textWidth = text.width;
const screenWidth = this.cameras.main.width;
const centerX = (screenWidth - textWidth) / 2;
text.x = centerX;
text.setOrigin(0, 0.5);
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:
sprite.setOrigin(0.5, 0.5);
sprite.x = 400;
sprite.y = 300;
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 for window.__TEST__.commands definition
- 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:
agent-browser eval "window.__TEST__.commands.clickStartGame()"
agent-browser wait 2000
agent-browser console
agent-browser eval "window.__TEST__.commands.goToScene('GameScene', { level: 1 })"
Common Testing Scenario Templates:
Scenario 1: Test Game Flow
agent-browser open http://localhost:3000?scene=MainMenu&test=1&seed=42
agent-browser eval "window.__TEST__.commands.clickStartGame()"
agent-browser wait 2000
agent-browser eval "window.__TEST__.commands.setTimer(5)"
agent-browser eval "window.__TEST__.commands.collectAnyCoin()"
agent-browser eval "window.__TEST__.gameState().score"
Scenario 2: Test Game Over
agent-browser open http://localhost:3000?scene=GameScene&test=1&seed=42
agent-browser eval "window.__TEST__.commands.triggerGameOver()"
agent-browser wait 2000
agent-browser eval "window.__TEST__.sceneKey"
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
agent-browser eval "window.__TEST__.commands.goToScene('GameScene', { level: 1 })"
agent-browser wait 500
agent-browser eval "window.__TEST__.sceneKey === 'GameScene'"
Pattern 2: Navigation via UI Commands
agent-browser eval "window.__TEST__.commands.clickStartGame()"
agent-browser wait 500
agent-browser console
Pattern 3: Scene Navigation with Retry
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))
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:
agent-browser eval "window.__TEST__.commands.clickStartGame()"
agent-browser wait 500
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:
agent-browser eval "window.__TEST__.commands.setTimer(5)"
agent-browser eval "window.__TEST__.commands.fastForwardTimer(10)"
agent-browser eval "window.__TEST__.commands.triggerGameOver()"
Movement Testing (Enhanced)
Phaser requires keydown/keyup pattern, not single press
Pattern:
agent-browser keydown ArrowRight
agent-browser wait 500
agent-browser keyup ArrowRight
Use test seam movePlayerTo(position) when available:
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:
window.__TEST__.commands.testPlayAgainFlow = () => {
this.scene.start('GameScene');
this.setTimer(5);
this.collectAnyCoin();
return this.gameState();
};
window.__TEST__.commands.testLevelCompleteFlow = () => {
this.movePlayerToExit();
return this.gameState();
};
window.__TEST__.commands.testSceneTransition = (from, to) => {
this.scene.start(to);
return { from, to, sceneKey: this.scene.key };
};
window.__TEST__.commands.testGameStateReset = () => {
this.reset();
return this.gameState();
};
Usage in browser testing:
agent-browser eval "window.__TEST__.commands.testPlayAgainFlow()"
Test Seam Readiness Patterns
Use direct property checks instead of Promise polling:
agent-browser eval "
new Promise((resolve) => {
const check = () => {
if (window.__TEST__?.ready) {
resolve(true);
} else {
setTimeout(check, 100);
}
};
check();
})
"
agent-browser eval "window.__TEST__?.ready || false"
Check window.TEST?.sceneKey directly:
agent-browser eval "window.__TEST__?.sceneKey === 'GameScene'"
Use Object.keys() for verification:
agent-browser eval "Object.keys(window.__TEST__?.commands || {}).includes('clickStartGame')"
Error Scenario Testing
Add test seam commands for error injection:
window.__TEST__.commands.forceMazeFailure = () => {
this.mazeGenerator.forceFailure = true;
};
window.__TEST__.commands.restoreMazeGeneration = () => {
this.mazeGenerator.forceFailure = false;
};
Test error handling paths:
agent-browser eval "window.__TEST__.commands.forceMazeFailure()"
agent-browser eval "window.__TEST__.commands.generateMaze()"
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:
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:
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:
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"
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:
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
FIT mode, 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
autoCenter configuration
- Input misaligned: Indicates manual scaling breaking coordinate system
- Resize not working: Check Scale Manager event handling
Browser Testing Optimization
Batch related commands:
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:
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:
agent-browser eval "window.__TEST__.commands.goToScene('GameScene')"
agent-browser wait 500
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.