一键导入
qa-browser-testing
E2E test creation and execution for QA. Validates implementations using Playwright API tests that become persistent artifacts for regression.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
E2E test creation and execution for QA. Validates implementations using Playwright API tests that become persistent artifacts for regression.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Complete Developer workflow orchestration - task research sequence, implementation flow, validation gates, PRD synchronization, exit conditions.
Complete Game Designer workflow - skill invocation protocol, GDD creation, playtest flow with GDD review, design sessions. MUST load before starting assignments.
Complete PM Coordinator workflow - task assignment, project orchestration, PRD management, worker coordination. Use proactively when starting PM agent work.
Complete PM Coordinator workflow - task assignment, project orchestration, PRD management, worker coordination. Use proactively when starting PM agent work.
Complete QA Validator workflow orchestration. References specialized skills for each validation step. Load at session startup for full protocol.
Base instructions and guidelines for all agents in the system. This skill provides foundational behaviors and communication protocols that all agents should follow.
| name | qa-browser-testing |
| description | E2E test creation and execution for QA. Validates implementations using Playwright API tests that become persistent artifacts for regression. |
| category | validation |
"Validate implementations with E2E tests that become regression tests for the project."
CRITICAL: Testing Three.js applications requires specific configuration for WebGL context support.
The playwright.config.ts MUST include these GPU acceleration flags for headless WebGL support:
// playwright.config.ts - Required for Three.js testing
projects: [
{
name: 'chromium-webgl',
use: {
...devices['Desktop Chrome'],
channel: 'chrome',
launchOptions: {
args: [
'--use-gl=desktop', // Desktop OpenGL (Windows/macOS/Linux)
'--enable-webgl',
'--enable-webgl2',
'--ignore-gpu-blocklist',
'--enable-gpu-rasterization',
'--enable-zero-copy',
'--disable-gpu-vsync',
],
},
},
},
]
| Browser | Headless WebGL | Solution |
|---|---|---|
| Chromium | Yes (with flags) | Use --use-gl=desktop flags |
| Chrome | Yes (with flags) | Best WebGL support |
| Firefox | No | Set headless: false or use Xvfb in CI |
| WebKit | No | Set headless: false |
Firefox Testing Pattern (requires headed mode):
# Local development - use headed mode
npm run test:e2e -- --project=firefox-webgl
# CI environment - use Xvfb for virtual display
xvfb-run --auto-servernum npx playwright test --project=firefox-webgl
Headless browsers may produce expected WebGL warnings. Always filter these out:
// Filter out known headless WebGL errors
const filteredErrors = errors.filter(
(error) =>
!error.includes('WebGL2RenderingContext') &&
!error.includes('Error creating WebGL context') &&
!error.includes('WebGL context could not be created') &&
!error.includes('WEBGL_debug_renderer_info')
);
Always wait for scene initialization using a data attribute:
// In your app code (Scene.tsx or main component)
useEffect(() => {
// Mark scene as ready when Three.js has initialized
const canvas = canvasRef.current;
if (canvas) {
canvas.dataset.ready = '1';
}
}, []);
// In your test
await page.locator('canvas[data-ready="1"]').waitFor({ timeout: 15000 });
Include this test to verify GPU acceleration is working:
test('GPU hardware acceleration is enabled', async ({ page }) => {
// This test verifies WebGL context is properly initialized
await page.goto('/');
const canvas = await page.locator('canvas').first();
await expect(canvas).toBeVisible();
const webglInfo = await page.evaluate(() => {
const canvas = document.querySelector('canvas');
if (!canvas) return { hasContext: false };
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
if (!gl) return { hasContext: false };
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
return {
hasContext: true,
version: gl.getParameter(gl.VERSION),
vendor: debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : 'unknown',
renderer: debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : 'unknown',
};
});
expect(webglInfo.hasContext).toBe(true);
console.log('WebGL Info:', webglInfo);
});
For visual regression of WebGL scenes, screenshot only the canvas:
test('canvas visual regression', async ({ page }) => {
await page.goto('/');
await page.waitForSelector('canvas[data-ready="1"]');
const canvas = page.locator('canvas');
// Screenshot just the canvas element
await expect(canvas).toHaveScreenshot('canvas-render.png', {
animations: 'allow',
// Anti-aliasing tolerance is set in playwright.config.ts
// threshold: 0.2, maxDiffPixelRatio: 0.02
});
});
Use for every validation after automated checks pass:
# 1. MANDATORY: Detect dev server port (Vite may use 3000, 3001, 5174, etc.)
netstat -an | grep LISTEN | grep -E ":(3000|3001|5174|8080)"
# OR try: curl -s http://localhost:3000 | grep -q "vite" && echo "3000" || curl -s http://localhost:3001 | grep -q "vite" && echo "3001"
# 2. Check if E2E test exists for the feature
ls tests/e2e/{feature}-suite.spec.ts
# 3. If missing, create using qa-e2e-test-creation patterns
# Use Skill("qa-e2e-test-creation")
# 4. Run E2E tests to validate implementation
npm run test:e2e
# 5. Review test output for acceptance criteria verification
⚠️ CRITICAL: Vite dev server may run on different ports (3000, 3001, 5174, etc.)
Before ANY browser interaction, ALWAYS detect the correct port:
# Method 1: Check listening ports
netstat -an | grep LISTEN | grep -E ":(3000|3001|5174|8080)"
# Method 2: Try curl to detect Vite
curl -s http://localhost:3000 | grep -q "vite" && echo "PORT=3000" || \
curl -s http://localhost:3001 | grep -q "vite" && echo "PORT=3001" || \
curl -s http://localhost:5174 | grep -q "vite" && echo "PORT=5174"
# Method 3: Check Vite output when running `npm run dev`
# Look for "Local: http://localhost:XXXX" in the output
E2E tests automatically detect the port from playwright.config.ts.
Manual MCP validation requires you to use the detected port.
⚠️ IMPORTANT: When multiple agents use Playwright MCP simultaneously
Standard @playwright/mcp shares a single browser instance. For parallel agent execution:
playwright-parallel-mcp - Isolated browser sessions per agent{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["playwright-parallel-mcp"],
"env": { "MAX_SESSIONS": "5" }
}
}
}
Reference: See qa-mcp-helpers skill for full details on parallel Playwright setup.
❌ OLD APPROACH (Do NOT do this):
// Interactive MCP validation - NO!
mcp__playwright__browser_navigate('http://localhost:3000');
mcp__playwright__browser_take_screenshot({ filename: 'validation.png' });
✅ NEW APPROACH (Do this):
// Write or run E2E test - YES!
npm run test:e2e -- tests/e2e/{feature}-suite.spec.ts
⚠️ CRITICAL: Ensure tests exist before validation
Check if E2E test exists for the validated feature:
# Look for test file
ls tests/e2e/{feature}-suite.spec.ts
# Or search for task/feature in tests
grep -r "taskId" tests/e2e/
If test is missing:
qa-e2e-test-creation skill# Run all E2E tests
npm run test:e2e
# Run specific test file
npm run test:e2e -- tests/e2e/{feature}-suite.spec.ts
# Run specific test by name
npm run test:e2e -- -g "test-name"
# Run in headed mode (see browser)
npm run test:e2e -- --headed
# Run with debug mode
npm run test:e2e -- --debug
For each acceptance criterion in current-task-qa.json (acceptanceCriteria array):
## Acceptance Criteria Verification
### Criterion 1: "Feature does X"
- **Test**: `npm run test:e2e -- -g "feature does X"`
- **Result**: ✅ PASS / ❌ FAIL
- **Evidence**: Test output shows expected behavior
If ALL tests pass:
{
"id": "{taskId}",
"passes": true,
"status": "passed",
"validatedAt": "{ISO_TIMESTAMP}",
"testResults": {
"e2eTests": "passed",
"testFile": "tests/e2e/{feature}-suite.spec.ts"
}
}
If ANY test fails:
{
"id": "{taskId}",
"status": "needs_fixes",
"bugNotes": "Test failure details...",
"retryCount": 1,
"testResults": {
"e2eTests": "failed",
"failureReason": "Test output excerpt"
}
}
| Category | What to Check | Test Pattern |
|---|---|---|
| Load | Page loads, canvas renders | test('page loads', ...) |
| Console | No errors or warnings | Console listener test |
| Functional | Features work as specified | Acceptance criteria tests |
| Visual | UI appears correctly | Screenshot comparison |
| Performance | 60 FPS, no stuttering | FPS monitoring test |
| Input | Controls respond correctly | WASD/mouse tests |
When Developer/Tech Artist didn't create tests:
// tests/e2e/{feature}-suite.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Feature Name - {taskId}', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:3000');
});
test('should meet acceptance criterion 1', async ({ page }) => {
// Test implementation
});
test('should meet acceptance criterion 2', async ({ page }) => {
// Test implementation
});
});
Then verify:
npm run test:e2e -- tests/e2e/{feature}-suite.spec.ts
test('page loads correctly', async ({ page }) => {
await page.goto('http://localhost:3000');
// Wait for canvas
const canvas = page.locator('canvas');
await expect(canvas).toBeVisible();
// Check for console errors
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
await page.waitForTimeout(5000); // Wait for initial load
expect(errors).toHaveLength(0);
});
test('keyboard controls work', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.waitForSelector('canvas');
// Focus canvas
await page.click('canvas');
// Press WASD keys
await page.keyboard.down('KeyW');
await page.waitForTimeout(500);
await page.screenshot({ path: 'test-results/after-w.png' });
await page.keyboard.up('KeyW');
});
test('visual appearance matches', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.waitForSelector('canvas');
await page.waitForTimeout(2000); // Wait for scene to stabilize
// Compare with baseline
await expect(page).toHaveScreenshot('baseline.png', {
maxDiffPixelRatio: 0.01,
});
});
test('pointer lock activates on game start', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.waitForSelector('canvas');
// Wait for auto-lock timeout (typically 100ms)
await page.waitForTimeout(200);
// Check if pointer lock is active
const isLocked = await page.evaluate(() => {
return document.pointerLockElement === document.body;
});
expect(isLocked).toBe(true);
});
test('performance is acceptable', async ({ page }) => {
await page.goto('http://localhost:3000');
// Get performance metrics
const metrics = await page.evaluate(() => {
const entries = performance.getEntriesByType('navigation');
const nav = entries[0] as PerformanceNavigationTiming;
return {
loadTime: nav.loadEventEnd - nav.startTime,
domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
};
});
expect(metrics.loadTime).toBeLessThan(3000);
expect(metrics.domContentLoaded).toBeLessThan(2000);
});
Every validation should include console error checking with WebGL-specific filtering:
test.describe('Console Error Check', () => {
test('should have no application console errors', async ({ page }) => {
const errors: string[] = [];
const warnings: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
if (msg.type() === 'warning') warnings.push(msg.text());
});
await page.goto('http://localhost:3000');
await page.waitForSelector('canvas[data-ready="1"]');
await page.waitForTimeout(3000);
// Filter out known headless WebGL/browser errors that are not app bugs
const filteredErrors = errors.filter((error) => {
// WebGL context errors in headless mode (expected, not app bugs)
const webglContextPatterns = [
/WebGL2RenderingContext/i,
/Error creating WebGL context/i,
/WebGL context could not be created/i,
/Failed to create WebGL2RenderingContext/i,
/WEBGL_debug_renderer_info/i,
];
// ANGLE/GPU driver warnings (platform-specific, not app bugs)
const gpuDriverPatterns = [
/ANGLE flag/,
/GPU process/,
/swiftshader/i,
];
// Filter if matches any known non-bug pattern
return !webglContextPatterns.some((p) => p.test(error)) &&
!gpuDriverPatterns.some((p) => p.test(error));
});
// Report actual application errors
expect(filteredErrors).toHaveLength(0);
// Log warnings for review (non-failing)
if (warnings.length > 0) {
console.warn('Console warnings found:', warnings);
}
});
});
| Pattern | Type | Action |
|---|---|---|
WebGL2RenderingContext | Headless limitation | Filter out |
Error creating WebGL context | Headless limitation | Filter out |
Failed to create WebGL2RenderingContext | Headless limitation | Filter out |
WEBGL_debug_renderer_info | Extension not available | Filter out |
ANGLE flag | GPU driver info | Filter out |
swiftshader | Software renderer | Filter out |
THREE.WebGLProgram | Shader compilation | FAIL - This is a bug |
shader error | Shader compilation | FAIL - This is a bug |
program info log | Shader compilation | FAIL - This is a bug |
CRITICAL: Choose correct load state to avoid flaky timeouts
Based on retrospective findings (bugfix-e2e-001, 2026-01-26), domcontentloaded is more reliable than networkidle for most E2E tests.
What does your test need?
|
┌───────────────────┼───────────────────┐
│ │ │
HTML/DOM only? All resources? No network activity?
│ │ │
▼ ▼ ▼
domcontentloaded load networkidle (rare)
│ │
│ Use when: Use when:
│ - Images - SPA with
│ - Styles background
Use when: - Scripts polling
- Page structure - Fonts
- Element visibility - Media
- Fast test execution
Default Choice: domcontentloaded
Why domcontentloaded is preferred:
networkidle can timeout on pages with continuous background activityWhen to use load:
When to use networkidle:
Learned from bugfix-e2e-001 (2026-01-26):
waitForLoadState('networkidle') to waitForLoadState('domcontentloaded')// Default: domcontentloaded (fastest, most reliable)
await page.goto('http://localhost:3000');
await page.waitForLoadState('domcontentloaded');
// For element-specific waits (even better than load state)
await page.waitForSelector('canvas', { state: 'attached' });
// Only use load when you need all resources
await page.waitForLoadState('load'); // For images, fonts, styles
// Rarely use networkidle (only for background activity)
await page.waitForLoadState('networkidle'); // Last resort
CRITICAL: Multiplayer E2E tests require explicit port cleanup
Based on retrospective findings (bugfix-e2e-002, 2026-01-26), Colyseus server tests need proper lifecycle management to avoid EADDRINUSE errors.
// tests/e2e/multiplayer-suite.spec.ts
import { test, expect } from '@playwright/test';
let serverProcess: ReturnType<typeof spawn> | null = null;
const TEST_PORT = 2577; // Different from default 2567
test.beforeAll(async () => {
// Start server for E2E tests
serverProcess = spawn('npm', ['run', 'server'], {
env: { ...process.env, PORT: String(TEST_PORT) },
stdio: 'pipe',
});
// Wait for server to be ready
await waitForServerReady(TEST_PORT);
});
test.afterAll(async () => {
// EXPLICIT cleanup required
if (serverProcess) {
serverProcess.kill('SIGTERM');
serverProcess = null;
// Additional: verify port is released
await waitForPortRelease(TEST_PORT);
}
});
Port Management Checklist:
Learned from bugfix-e2e-002 (2026-01-26):
CRITICAL for Shader/TSL Tasks: Add pattern matching for shader errors:
test.describe('Shader Error Detection', () => {
test('should have no shader compilation errors', async ({ page }) => {
const shaderErrors: string[] = [];
page.on('console', (msg) => {
const text = msg.text();
// Three.js shader error patterns
const shaderErrorPatterns = [
/THREE\.WebGLProgram/i,
/shader error/i,
/program info log/i,
/WEBGL_WARNING/i,
// TSL-specific patterns
/Cannot read properties.*undefined.*replace/i,
/VaryingProperty/i,
/NodeBuilder/i,
/assign.*null/i,
];
if (shaderErrorPatterns.some((pattern) => pattern.test(text))) {
shaderErrors.push(text);
}
});
await page.goto('http://localhost:3000');
await page.waitForLoadState('networkidle');
// Trigger shader-heavy interactions
await page.mouse.click(400, 300);
await page.waitForTimeout(2000);
expect(shaderErrors).toHaveLength(0);
});
});
For P1-005 (Color Blind Modes) and similar shader tasks:
test.describe('Shader Task Validation Checklist', () => {
test('should validate all color modes without errors', async ({ page }) => {
const allErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') allErrors.push(msg.text());
});
const colorModes = ['default', 'protanopia', 'deuteranopia', 'tritanopia', 'high_contrast'];
for (const mode of colorModes) {
// Set mode via localStorage or UI
await page.evaluate((m) => {
localStorage.setItem(
'project-chroma-accessibility',
JSON.stringify({
hasCompletedFirstLaunch: true,
colorMode: m,
})
);
}, mode);
await page.reload();
await page.waitForTimeout(1000);
}
// Verify no shader errors across all modes
const shaderErrors = allErrors.filter((e) => /shader|THREE|TSL|WebGL/i.test(e));
expect(shaderErrors).toHaveLength(0);
});
});
Runtime TypeErrors like "Cannot read properties of undefined" can exist in the codebase before a task starts, blocking browser validation for unrelated features. QA needs to detect and report these blockers early.
// tests/e2e/runtime-error-check.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Runtime Error Detection', () => {
test('should have no runtime TypeErrors', async ({ page }) => {
const runtimeErrors: Array<{
message: string;
stack?: string;
timestamp: number;
}> = [];
// Capture all unhandled errors
page.on('pageerror', (error) => {
runtimeErrors.push({
message: error.message,
stack: error.stack,
timestamp: Date.now(),
});
});
// Also capture console errors
page.on('console', (msg) => {
if (msg.type() === 'error') {
runtimeErrors.push({
message: msg.text(),
timestamp: Date.now(),
});
}
});
await page.goto('http://localhost:3000');
await page.waitForSelector('canvas');
await page.waitForTimeout(5000); // Wait for initial load
// Check for specific runtime error patterns
const blockingErrors = runtimeErrors.filter((error) => {
const blockingPatterns = [
/Cannot read properties.*undefined/,
/Cannot read.*property.*undefined/,
/undefined is not.*object/,
/null is not.*object/,
/is not a function/,
/Unexpected token/,
];
return blockingPatterns.some((pattern) => pattern.test(error.message));
});
if (blockingErrors.length > 0) {
console.error('BLOCKING RUNTIME ERRORS FOUND:', blockingErrors);
throw new Error(
`Found ${blockingErrors.length} blocking runtime error(s):\n` +
blockingErrors.map((e) => ` - ${e.message}`).join('\n') +
`\n\nThese errors must be fixed before validation can proceed.`
);
}
// Also check for any runtime errors (not just blocking)
if (runtimeErrors.length > 0) {
console.warn('Non-blocking runtime errors:', runtimeErrors);
}
});
test('should report all runtime errors for debugging', async ({ page }) => {
const allErrors: string[] = [];
page.on('pageerror', (error) => {
allErrors.push(`[${error.name}] ${error.message}`);
});
await page.goto('http://localhost:3000');
await page.waitForSelector('canvas');
await page.waitForTimeout(3000);
if (allErrors.length > 0) {
// Log all errors for debugging, even if non-blocking
console.log('All Runtime Errors:', allErrors);
}
});
});
Runtime Error Found?
|
┌───────────────┴───────────────┐
│ │
Error in CHANGED files? Error in UNCHANGED files?
│ │
▼ ▼
RETURN to Developer CREATE BLOCKER TASK
(Task's code has bug) (Pre-existing issue)
When blocking runtime errors are found:
{
"status": "blocked",
"blocker": "Pre-existing runtime TypeError",
"errors": [
{
"message": "Cannot read properties of undefined (reading 'position')",
"location": "src/components/game/player/index.ts:42",
"isNew": false,
"relatedToTask": false
}
],
"action": "Create separate bugfix task for pre-existing error",
"recommendation": "Developer should fix pre-existing error before validating new features"
}
// Check if error exists before task changes
test.beforeEach(async ({ page }) => {
// Record baseline errors before any task interactions
const baselineErrors: string[] = [];
page.on('pageerror', (error) => {
baselineErrors.push(error.message);
});
await page.goto('http://localhost:3000');
await page.waitForTimeout(2000);
// Store baseline for comparison
(page as any).__baselineErrors = baselineErrors;
});
| Error Type | Is Blocking? | Action |
|---|---|---|
| TypeError in changed files | YES | Return to Developer |
| TypeError in unchanged files | YES | Create blocker task |
| ReferenceError | YES | Return to Developer |
| Console warnings | NO | Note in report |
| Asset load errors (404) | MAYBE | Check if task-related |
Learned from bugfix-tps-001 retrospective (2026-01-25):
For complex validations, use Page Objects from tests/pages/:
import { test, expect } from '@playwright/test';
import { GamePage } from '@/pages/game.page';
import { MultiplayerPage } from '@/pages/multiplayer.page';
test('complete gameplay loop', async ({ page }) => {
const gamePage = new GamePage(page);
await gamePage.goto();
await gamePage.selectCharacter('TestPlayer');
await gamePage.waitForLobby();
expect(await gamePage.isConnected()).toBe(true);
});
test('multiplayer state sync', async ({ browser }) => {
const multiplayerPage = new MultiplayerPage(page);
const players = await multiplayerPage.setupMultiPlayerTest(browser, 2);
try {
await multiplayerPage.connectPlayersToGame(players);
expect(await multiplayerPage.verifyAllConnected(players)).toBe(true);
} finally {
await multiplayerPage.cleanupPlayers(players);
}
});
| Browser | Headless WebGL | GPU Acceleration | Priority | Notes |
|---|---|---|---|---|
| Chrome | Yes (with flags) | Yes (with flags) | Primary | Best WebGL support |
| Chromium | Yes (with flags) | Yes (with flags) | Primary | CI/CD standard |
| Firefox | No | Yes (headed only) | Optional | Use headless: false or Xvfb |
| WebKit/Safari | No | Yes (headed only) | If iOS target | Use headless: false |
| Edge | Yes (with flags) | Yes (with flags) | Optional | Uses Chromium |
# Chrome/Chromium (primary - works in headless)
npm run test:e2e -- --project=chromium-webgl
# Firefox (requires headed mode or Xvfb)
npm run test:e2e -- --project=firefox-webgl --headed
# Firefox with Xvfb (CI/CD)
xvfb-run --auto-servernum npm run test:e2e -- --project=firefox-webgl
# All browsers
npm run test:e2e
Firefox doesn't support WebGL in headless mode. Configure project in playwright.config.ts:
{
name: 'firefox-webgl',
use: {
...devices['Desktop Firefox'],
headless: false, // Required for WebGL support
},
}
For CI/CD with Firefox, use Xvfb (virtual display):
# GitHub Actions example
- name: Install Xvfb
run: sudo apt-get install -y xvfb
- name: Run Firefox WebGL tests
run: xvfb-run --auto-servernum npx playwright test --project=firefox-webgl
New Feature Validation → Regression Tests
Developer/Tech Artist writes E2E test
↓
QA validates feature
↓
Test passes
↓
Feature merged to main
↓
Test becomes regression check in CI/CD
| Test Result | Action |
|---|---|
| All E2E tests pass | Mark as PASSED |
| Some tests fail | Mark as NEEDS_FIXES with bug notes |
| Console errors | Mark as NEEDS_FIXES |
| No test exists | Create test first, then validate |
❌ DON'T:
✅ DO:
For each validation:
tests/e2e/npm run test:e2e runs without errorsWhen tests fail, include in bug notes:
## Test Failure
**Test File**: tests/e2e/{feature}-suite.spec.ts
**Test Name**: "{test-name}"
**Error Message**: {error from test output}
**Steps to Reproduce**:
1. npm run test:e2e -- -g "{test-name}"
2. Observe failure
**Expected**: {expected behavior}
**Actual**: {actual behavior from test output}
⚠️ CRITICAL: Use shared-lifecycle skill for server management.
⚠️ IMPORTANT: Playwright's webServer config manages servers for E2E tests automatically.
When running npm run test:e2e, Playwright automatically starts:
npm run dev (port 3000) with reuseExistingServer: !process.env.CInpm run server (port 2567) with reuseExistingServer: false (for multiplayer)DO NOT manually start servers for E2E tests.
Running E2E tests?
|
┌───────────────┴───────────────┐
│ │
YES (npm run test:e2e) NO (Manual MCP validation)
│ │
▼ ▼
DO NOT start servers Check if servers running
Playwright manages them If YES: use existing
Cleanup is automatic If NO: start and track
# Check if dev server is running (port 3000)
netstat -an | grep :3000 || lsof -i :3000
# Alternative: Try curl to detect Vite
curl -s http://localhost:3000 | grep -q "vite" && echo "RUNNING" || echo "NOT_RUNNING"
# Playwright handles server lifecycle via webServer config
npm run test:e2e
# NO manual server start needed
# NO manual cleanup needed - Playwright handles it
# Only for manual MCP validation (NOT E2E tests)
# Check port 3000 first
if ! netstat -an | grep :3000; then
# Start server in background
Bash(command="npm run dev", run_in_background=true)
# Capture shell_id for cleanup: { shell_id: "abc123" }
fi
# After validation completes:
TaskStop(task_id="abc123") # MANDATORY cleanup
MANDATORY CLEANUP after all tests complete (pass OR fail):
Use the cleanup patterns from shared-lifecycle skill to ensure:
See also: shared-lifecycle skill for complete process management patterns.
⚠️ CRITICAL: Camera validation requires E2E tests with exact value verification.
When validating camera features (TPS, orbital, etc.), use systematic E2E test coverage:
// tests/e2e/camera-suite.spec.ts
test.describe('TPS Camera Validation - feat-tps-004', () => {
test('should validate shoulder offset values match acceptance criteria', async ({ page }) => {
await page.goto('http://localhost:3000/?scene=controls');
await page.waitForSelector('canvas');
// Access camera test state (exposed by test scene)
const cameraState = await page.evaluate(() => {
return (window as any).__cameraTestState;
});
// Verify EXACT values from acceptance criteria
expect(cameraState.shoulderOffsetRight).toBe(0.75);
expect(cameraState.shoulderOffsetLeft).toBe(-0.75);
// Common bug: wrong values (0.85 instead of 0.75)
if (cameraState.shoulderOffsetRight !== 0.75) {
throw new Error(
`shoulderOffsetRight mismatch: expected 0.75, got ${cameraState.shoulderOffsetRight}`
);
}
});
test('should validate look-at offset is applied', async ({ page }) => {
await page.goto('http://localhost:3000/?scene=controls');
await page.waitForSelector('canvas');
// Screenshot validation for visual composition
await page.screenshot({
path: 'test-results/camera/shoulder-offset.png',
});
// Verify look-at direction
const lookAtOffset = await page.evaluate(() => {
const camera = (window as any).__testCamera;
return camera ? camera.userData.lookAtOffset : null;
});
expect(lookAtOffset).toBeDefined();
expect(Math.abs(lookAtOffset)).toBe(0.75);
});
});
Learned from feat-tps-004 retrospective:
Value mismatch detection: Implementation had 0.85, acceptance criteria specified 0.75
Missing look-at offset: Camera position was offset but looked at center
Scene routing test: URL-based scene loading must be tested
?scene=controls actually loads correct sceneCamera E2E test checklist:
URL Scene Routing Pattern:
// main.tsx - useState/useEffect sync for URL-based routing
const [sceneParam, setSceneParam] = useState<string | null>(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const scene = params.get('scene');
if (scene) {
setSceneParam(scene);
// Sync to gameStore
gameStore.setPhase(scene as any);
}
}, []);