Loading...
Loading...
Game QA testing with Playwright — visual regression, gameplay verification, performance, and accessibility for browser games. Use when writing or running game tests, debugging test failures, or building QA infrastructure. This is the reference skill — use qa-game for the user-facing command.
npx skill4agent add hungify/playables-skills game-qatest-patterns.mdgameplay-invariants.mdvisual-regression.mdclock-control.mdplaywright-mcp.mditerate-client.mdmobile-tests.md@playwright/testtoHaveScreenshot()@axe-core/playwrightwebServernpm install -D @playwright/test @axe-core/playwright
npx playwright install chromiumpackage.json{
"scripts": {
"test": "npx playwright test",
"test:ui": "npx playwright test --ui",
"test:headed": "npx playwright test --headed",
"test:update-snapshots": "npx playwright test --update-snapshots"
}
}tests/
├── e2e/
│ ├── game.spec.js # Core game tests (boot, scenes, input, score)
│ ├── visual.spec.js # Visual regression screenshots
│ └── perf.spec.js # Performance and FPS tests
├── fixtures/
│ ├── game-test.js # Custom test fixture with game helpers
│ └── screenshot.css # CSS to mask dynamic elements for visual tests
├── helpers/
│ └── seed-random.js # Seeded PRNG for deterministic game behavior
playwright.config.jsimport { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [['html', { open: 'never' }], ['list']],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
expect: {
toHaveScreenshot: {
maxDiffPixels: 200,
threshold: 0.3
}
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } }
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 30000
}
});webServerreuseExistingServerbaseURLvite.config.jswindowmain.js// Expose for Playwright QA
window.__GAME__ = game;
window.__GAME_STATE__ = gameState;
window.__EVENT_BUS__ = eventBus;
window.__EVENTS__ = Events;render_game_to_text()window.render_game_to_text = () => {
if (!game || !gameState) return JSON.stringify({ error: 'not_ready' });
const activeScenes = game.scene.getScenes(true).map((s) => s.scene.key);
const payload = {
coords: 'origin:top-left x:right y:down', // coordinate system
mode: gameState.gameOver ? 'game_over' : 'playing',
scene: activeScenes[0] || null,
score: gameState.score,
bestScore: gameState.bestScore
};
// Add player info when in gameplay
const gameScene = game.scene.getScene('GameScene');
if (gameState.started && gameScene?.player?.sprite) {
const s = gameScene.player.sprite;
const body = s.body;
payload.player = {
x: Math.round(s.x),
y: Math.round(s.y),
vx: Math.round(body.velocity.x),
vy: Math.round(body.velocity.y),
onGround: body.blocked.down
};
}
// Extend with visible entities as you add them:
// payload.entities = obstacles.map(o => ({ x: o.x, y: o.y, type: o.type }));
return JSON.stringify(payload);
};render_game_to_text()advanceTime(ms)window.advanceTime = (ms) => {
return new Promise((resolve) => {
const start = performance.now();
function step() {
if (performance.now() - start >= ms) return resolve();
requestAnimationFrame(step);
}
requestAnimationFrame(step);
});
};@playwright/testpage.clock.install()page.clock.runFor()advanceTimescripts/iterate-client.jsGametest-patterns.mdgameplay-invariants.mdnpm install -D @playwright/test @axe-core/playwright && npx playwright install chromiumplaywright.config.jswindow.__GAME__window.__GAME_STATE__window.__EVENT_BUS__main.jstests/fixtures/game-test.jsgamePagetests/helpers/seed-random.jstests/e2e/game.spec.jsvisual.spec.jsperf.spec.jstesttest:uitest:headedtest:update-snapshotsnpm run test:update-snapshotspage.route()