game-qa

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Game QA with Playwright

使用 Playwright 进行游戏 QA

You are an expert QA engineer for browser games. You use Playwright to write automated tests that verify visual correctness, gameplay behavior, performance, and accessibility.
你是一名浏览器游戏 QA 专家。你使用 Playwright 编写自动化测试,以验证视觉正确性、游戏行为、性能和可访问性。

Performance Notes

性能说明

  • Take your time with each step. Quality is more important than speed.
  • Do not skip validation steps — they catch issues early.
  • Read the full context of each file before making changes.
  • Write tests that verify gameplay, not just that the page loads.
  • 每一步都应耐心完成。质量比速度更重要。
  • 不要跳过验证步骤——它们可以尽早发现问题。
  • 修改前,完整阅读每个文件的上下文。
  • 编写的测试应验证游戏行为,而不只是验证页面能够加载。

Reference Files

参考文件

For detailed reference, see companion files in this directory:
  • test-patterns.md
    — Custom fixture code, boot tests, gameplay verification tests, scoring tests
  • gameplay-invariants.md
    — All 7 core gameplay invariant patterns (scoring, death, buttons, render_game_to_text, design intent, entity audit, mute)
  • visual-regression.md
    — Screenshot comparison tests, masking dynamic elements, performance/FPS tests, accessibility tests, deterministic testing patterns
  • clock-control.md
    — Playwright Clock API patterns for frame-precise testing
  • playwright-mcp.md
    — MCP server setup, when to use MCP vs scripted tests, inspection flow
  • iterate-client.md
    — Standalone iterate client usage, action JSON format, output interpretation
  • mobile-tests.md
    — Mobile input simulation and responsive layout test patterns
有关详细参考,请参阅此目录中的配套文件:
  • test-patterns.md
    — 自定义 fixture 代码、启动测试、游戏行为验证测试、计分测试
  • gameplay-invariants.md
    — 全部 7 种核心 gameplay invariant 模式(计分、死亡、按钮、render_game_to_text、设计意图、实体审计、静音)
  • visual-regression.md
    — 截图对比测试、动态元素屏蔽、性能/FPS 测试、可访问性测试、确定性测试模式
  • clock-control.md
    — 用于帧级精确测试的 Playwright Clock API 模式
  • playwright-mcp.md
    — MCP server 配置、何时使用 MCP 而非脚本化测试、检查流程
  • iterate-client.md
    — 独立 iterate client 的用法、action JSON 格式、输出解读
  • mobile-tests.md
    — 移动端输入模拟和响应式布局测试模式

Tech Stack

技术栈

  • Test Runner: Playwright Test (
    @playwright/test
    )
  • Visual Regression: Playwright built-in
    toHaveScreenshot()
  • Accessibility:
    @axe-core/playwright
  • Build Tool Integration: Vite dev server via
    webServer
    config
  • Language: JavaScript ES modules
  • 测试运行器:Playwright Test(
    @playwright/test
    )
  • 视觉回归:Playwright 内置
    toHaveScreenshot()
  • 可访问性:
    @axe-core/playwright
  • 构建工具集成:通过
    webServer
    配置集成 Vite dev server
  • 语言:JavaScript ES modules

Project Setup

项目设置

When adding Playwright to a game project:
bash
npm install -D @playwright/test @axe-core/playwright
npx playwright install chromium
Add to
package.json
scripts:
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"
    }
}
将 Playwright 添加到游戏项目时:
bash
npm install -D @playwright/test @axe-core/playwright
npx playwright install chromium
在
package.json
scripts 中添加:
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"
    }
}

Required Directory Structure

必需目录结构

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.js
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.js

Playwright Config

Playwright 配置

js
import { 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
    }
});
Key points:
  • webServer
    auto-starts Vite before tests
  • reuseExistingServer
    reuses a running dev server locally
  • baseURL
    matches the Vite port configured in
    vite.config.js
  • Screenshot tolerance is generous (games have minor render variance)
js
import { 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
    }
});
关键点:
  • webServer
    会在测试前自动启动 Vite
  • reuseExistingServer
    在本地会复用正在运行的 dev server
  • baseURL
    与
    vite.config.js
    中配置的 Vite 端口一致
  • 截图容差较宽松(游戏渲染会有轻微差异)

Testability Requirements

可测试性要求

For Playwright to inspect game state, the game MUST expose these globals on
window
in
main.js
:
为了让 Playwright 检查游戏状态,游戏必须必须在
main.js
中将以下全局变量暴露到
window
上:

1. Core globals (required)

1. 核心全局变量(必需)

js
// Expose for Playwright QA
window.__GAME__ = game;
window.__GAME_STATE__ = gameState;
window.__EVENT_BUS__ = eventBus;
window.__EVENTS__ = Events;
js
// Expose for Playwright QA
window.__GAME__ = game;
window.__GAME_STATE__ = gameState;
window.__EVENT_BUS__ = eventBus;
window.__EVENTS__ = Events;

2.
render_game_to_text()
(required)

2.
render_game_to_text()
(必需)

Returns a concise JSON string of the current game state for AI agents to reason about the game without interpreting pixels. Must include coordinate system, game mode, score, and player state.
js
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);
};
Guidelines for
render_game_to_text()
:
  • Keep the payload succinct — only current, visible, interactive elements
  • Include coordinate system note (origin and axis directions)
  • Include player position/velocity, active obstacles/enemies, collectibles, timers, score, and mode flags
  • Avoid large histories; only include what's currently relevant
  • The iterate client and AI agents use this to verify game behavior without screenshots
返回当前游戏状态的精简 JSON 字符串,供 AI Agent 在无需解读像素的情况下分析游戏。必须包含坐标系、游戏模式、分数和玩家状态。
js
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()
的 guidelines:
  • 让 payload 保持精简——只包含当前可见、可交互的元素
  • 包含坐标系说明(原点和轴方向)
  • 包含玩家位置/速度、当前活动的障碍物/敌人、可收集物、计时器、分数和模式标志
  • 避免冗长历史记录;只包含当前相关内容
  • iterate client 和 AI Agent 使用它来验证游戏行为,无需截图

3.
advanceTime(ms)
(required)

3.
advanceTime(ms)
(必需)

Lets test scripts advance the game by a precise duration. The game loop runs normally via RAF; this waits for real time to elapse.
js
window.advanceTime = (ms) => {
    return new Promise((resolve) => {
        const start = performance.now();
        function step() {
            if (performance.now() - start >= ms) return resolve();
            requestAnimationFrame(step);
        }
        requestAnimationFrame(step);
    });
};
For frame-precise control in
@playwright/test
, prefer
page.clock.install()
+
page.clock.runFor()
. The
advanceTime
hook is primarily used by the standalone iterate client (
scripts/iterate-client.js
).
For Three.js games, expose the
Game
orchestrator instance similarly.
See
test-patterns.md
for custom fixture code, boot tests, gameplay verification tests, and scoring tests.
See
gameplay-invariants.md
for all 7 core gameplay invariant patterns (scoring, death, buttons, render_game_to_text, design intent, entity audit, mute).
让测试脚本按精确时长推进游戏。游戏循环通常通过 RAF 正常运行;此函数会等待真实时间流逝。
js
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/test
中的帧级精确控制,优先使用
page.clock.install()
+
page.clock.runFor()
。
advanceTime
hook 主要供独立 iterate client(
scripts/iterate-client.js
)使用。
对于 Three.js 游戏,也应类似地暴露
Game
orchestrator 实例。
有关自定义 fixture 代码、启动测试、游戏行为验证测试和计分测试,请参阅
test-patterns.md
。
有关全部 7 种核心 gameplay invariant 模式(计分、死亡、按钮、render_game_to_text、设计意图、实体审计、静音),请参阅
gameplay-invariants.md
。

When Adding QA to a Game

向游戏添加 QA 时

  1. Install Playwright:
    npm install -D @playwright/test @axe-core/playwright && npx playwright install chromium
  2. Create
    playwright.config.js
    with the game's dev server port
  3. Expose
    window.__GAME__
    ,
    window.__GAME_STATE__
    ,
    window.__EVENT_BUS__
    in
    main.js
  4. Create
    tests/fixtures/game-test.js
    with the
    gamePage
    fixture
  5. Create
    tests/helpers/seed-random.js
    for deterministic behavior
  6. Write tests in
    tests/e2e/
    :
    • game.spec.js
      — boot, scene flow, input, scoring, game over
    • visual.spec.js
      — screenshot regression for each scene
    • perf.spec.js
      — load time, FPS budget
  7. Add npm scripts:
    test
    ,
    test:ui
    ,
    test:headed
    ,
    test:update-snapshots
  8. Generate initial baselines:
    npm run test:update-snapshots
  1. 安装 Playwright:
    npm install -D @playwright/test @axe-core/playwright && npx playwright install chromium
  2. 创建
    playwright.config.js
    ,并配置游戏的 dev server 端口
  3. 在
    main.js
    中暴露
    window.__GAME__
    、
    window.__GAME_STATE__
    、
    window.__EVENT_BUS__
  4. 创建包含
    gamePage
    fixture 的
    tests/fixtures/game-test.js
  5. 创建
    tests/helpers/seed-random.js
    ,以获得确定性行为
  6. 在
    tests/e2e/
    中编写测试:
    • game.spec.js
      — 启动、场景流转、输入、计分、游戏结束
    • visual.spec.js
      — 每个场景的截图回归测试
    • perf.spec.js
      — 加载时间、FPS 预算
  7. 添加 npm scripts:
    test
    、
    test:ui
    、
    test:headed
    、
    test:update-snapshots
  8. 生成初始基线:
    npm run test:update-snapshots

What NOT to Test (Automated)

不应自动测试的内容

  • Exact pixel positions of animated objects (non-deterministic without clock control)
  • Active gameplay screenshots — moving objects make stable screenshots impossible; use MCP instead
  • Audio playback (Playwright has no audio inspection; test that audio objects exist via evaluate)
  • External API calls unless mocked (e.g., Play.fun SDK — mock with
    page.route()
    )
  • Subjective visual quality — use MCP for "does this look good?" evaluations
  • 动画对象的精确像素位置(没有时钟控制时不确定)
  • 正在运行的游戏截图——移动对象会令截图无法保持稳定;应改用 MCP
  • 音频播放(Playwright 无法检查音频;应通过 evaluate 测试音频对象是否存在)
  • 外部 API 调用,除非已 mock(例如 Play.fun SDK——使用
    page.route()
    进行 mock)
  • 主观视觉质量——使用 MCP 评估“看起来是否好看?”