| 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 |
Browser Testing for QA
"Validate implementations with E2E tests that become regression tests for the project."
Three.js / WebGL Testing Best Practices (2025-2026)
CRITICAL: Testing Three.js applications requires specific configuration for WebGL context support.
Playwright Configuration Requirements
The playwright.config.ts MUST include these GPU acceleration flags for headless WebGL support:
projects: [
{
name: 'chromium-webgl',
use: {
...devices['Desktop Chrome'],
channel: 'chrome',
launchOptions: {
args: [
'--use-gl=desktop',
'--enable-webgl',
'--enable-webgl2',
'--ignore-gpu-blocklist',
'--enable-gpu-rasterization',
'--enable-zero-copy',
'--disable-gpu-vsync',
],
},
},
},
]
Headless vs Headed Mode for WebGL
| 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):
npm run test:e2e -- --project=firefox-webgl
xvfb-run --auto-servernum npx playwright test --project=firefox-webgl
WebGL Console Error Filtering
Headless browsers may produce expected WebGL warnings. Always filter these out:
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')
);
Scene Readiness Pattern
Always wait for scene initialization using a data attribute:
useEffect(() => {
const canvas = canvasRef.current;
if (canvas) {
canvas.dataset.ready = '1';
}
}, []);
await page.locator('canvas[data-ready="1"]').waitFor({ timeout: 15000 });
GPU Acceleration Verification Test
Include this test to verify GPU acceleration is working:
test('GPU hardware acceleration is enabled', async ({ page }) => {
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);
});
Canvas-Only Screenshots
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');
await expect(canvas).toHaveScreenshot('canvas-render.png', {
animations: 'allow',
});
});
When to Use This Skill
Use for every validation after automated checks pass:
- Validating Developer implementation
- Verifying Tech Artist visual assets
- Testing gameplay mechanics
- Checking UI components
- Before marking PRD items as passed
Quick Start
netstat -an | grep LISTEN | grep -E ":(3000|3001|5174|8080)"
ls tests/e2e/{feature}-suite.spec.ts
npm run test:e2e
MANDATORY: Port Detection
⚠️ CRITICAL: Vite dev server may run on different ports (3000, 3001, 5174, etc.)
Before ANY browser interaction, ALWAYS detect the correct port:
netstat -an | grep LISTEN | grep -E ":(3000|3001|5174|8080)"
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"
E2E tests automatically detect the port from playwright.config.ts.
Manual MCP validation requires you to use the detected port.
Multi-Agent Playwright Considerations
⚠️ IMPORTANT: When multiple agents use Playwright MCP simultaneously
Standard @playwright/mcp shares a single browser instance. For parallel agent execution:
- Use
playwright-parallel-mcp - Isolated browser sessions per agent
- Configuration in MCP settings:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["playwright-parallel-mcp"],
"env": { "MAX_SESSIONS": "5" }
}
}
}
- Usage: Create session per agent, use sessionId in all calls
Reference: See qa-mcp-helpers skill for full details on parallel Playwright setup.
Core Principle: Run Tests, Don't Use MCP
❌ OLD APPROACH (Do NOT do this):
mcp__playwright__browser_navigate('http://localhost:3000');
mcp__playwright__browser_take_screenshot({ filename: 'validation.png' });
✅ NEW APPROACH (Do this):
npm run test:e2e -- tests/e2e/{feature}-suite.spec.ts
Validation Workflow
Level 0: Test Coverage Check (BEFORE Validation)
⚠️ CRITICAL: Ensure tests exist before validation
-
Check if E2E test exists for the validated feature:
ls tests/e2e/{feature}-suite.spec.ts
grep -r "taskId" tests/e2e/
-
If test is missing:
- Load
qa-e2e-test-creation skill
- Create test covering acceptance criteria
- Verify test runs successfully
Level 1: Run E2E Tests
npm run test:e2e
npm run test:e2e -- tests/e2e/{feature}-suite.spec.ts
npm run test:e2e -- -g "test-name"
npm run test:e2e -- --headed
npm run test:e2e -- --debug
Level 2: Verify Acceptance Criteria
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
Level 3: Report Results
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"
}
}
Test Categories
| 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 |
Creating Tests for Missing Coverage
When Developer/Tech Artist didn't create tests:
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('should meet acceptance criterion 2', async ({ page }) => {
});
});
Then verify:
npm run test:e2e -- tests/e2e/{feature}-suite.spec.ts
Common Test Patterns for Validation
Basic Load Test
test('page loads correctly', async ({ page }) => {
await page.goto('http://localhost:3000');
const canvas = page.locator('canvas');
await expect(canvas).toBeVisible();
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
await page.waitForTimeout(5000);
expect(errors).toHaveLength(0);
});
Input Testing
test('keyboard controls work', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.waitForSelector('canvas');
await page.click('canvas');
await page.keyboard.down('KeyW');
await page.waitForTimeout(500);
await page.screenshot({ path: 'test-results/after-w.png' });
await page.keyboard.up('KeyW');
});
Visual Comparison
test('visual appearance matches', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.waitForSelector('canvas');
await page.waitForTimeout(2000);
await expect(page).toHaveScreenshot('baseline.png', {
maxDiffPixelRatio: 0.01,
});
});
Pointer Lock Testing (FPS/TPS)
test('pointer lock activates on game start', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.waitForSelector('canvas');
await page.waitForTimeout(200);
const isLocked = await page.evaluate(() => {
return document.pointerLockElement === document.body;
});
expect(isLocked).toBe(true);
});
Performance Metrics
test('performance is acceptable', async ({ page }) => {
await page.goto('http://localhost:3000');
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);
});
Console Error Monitoring
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);
const filteredErrors = errors.filter((error) => {
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,
];
const gpuDriverPatterns = [
/ANGLE flag/,
/GPU process/,
/swiftshader/i,
];
return !webglContextPatterns.some((p) => p.test(error)) &&
!gpuDriverPatterns.some((p) => p.test(error));
});
expect(filteredErrors).toHaveLength(0);
if (warnings.length > 0) {
console.warn('Console warnings found:', warnings);
}
});
});
WebGL Error Filter Patterns
| 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 |
Load State Decision Tree
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:
- Fires when HTML is parsed and DOM is ready
- Much faster than waiting for all network requests
- Sufficient for most UI interactions (after waiting for specific elements)
networkidle can timeout on pages with continuous background activity
When to use load:
- Testing image loading
- Need fonts fully applied
- Media elements (video/audio)
- Critical styles depend on external resources
When to use networkidle:
- SPA with continuous background polling
- Analytics/tracking scripts running
- WebSocket connections active
- Rare - only when explicitly justified
Learned from bugfix-e2e-001 (2026-01-26):
- Changed
waitForLoadState('networkidle') to waitForLoadState('domcontentloaded')
- 23/23 accessibility tests now passing (was timing out before)
- Tests complete within 60 seconds (was timing out)
Load State Usage Examples
await page.goto('http://localhost:3000');
await page.waitForLoadState('domcontentloaded');
await page.waitForSelector('canvas', { state: 'attached' });
await page.waitForLoadState('load');
await page.waitForLoadState('networkidle');
E2E Server Lifecycle Management
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.
import { test, expect } from '@playwright/test';
let serverProcess: ReturnType<typeof spawn> | null = null;
const TEST_PORT = 2577;
test.beforeAll(async () => {
serverProcess = spawn('npm', ['run', 'server'], {
env: { ...process.env, PORT: String(TEST_PORT) },
stdio: 'pipe',
});
await waitForServerReady(TEST_PORT);
});
test.afterAll(async () => {
if (serverProcess) {
serverProcess.kill('SIGTERM');
serverProcess = null;
await waitForPortRelease(TEST_PORT);
}
});
Port Management Checklist:
Learned from bugfix-e2e-002 (2026-01-26):
- Fixed EADDRINUSE errors with proper port cleanup
- 65/65 E2E tests passing (100% success rate)
- Server availability detection added
Shader-Specific Error Detection
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();
const shaderErrorPatterns = [
/THREE\.WebGLProgram/i,
/shader error/i,
/program info log/i,
/WEBGL_WARNING/i,
/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');
await page.mouse.click(400, 300);
await page.waitForTimeout(2000);
expect(shaderErrors).toHaveLength(0);
});
});
Color Mode / Shader Task Validation Pattern
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) {
await page.evaluate((m) => {
localStorage.setItem(
'project-chroma-accessibility',
JSON.stringify({
hasCompletedFirstLaunch: true,
colorMode: m,
})
);
}, mode);
await page.reload();
await page.waitForTimeout(1000);
}
const shaderErrors = allErrors.filter((e) => /shader|THREE|TSL|WebGL/i.test(e));
expect(shaderErrors).toHaveLength(0);
});
});
Runtime Error Detection
Problem: Pre-existing Runtime Errors Block Validation
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.
Runtime Error Monitoring Pattern
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;
}> = [];
page.on('pageerror', (error) => {
runtimeErrors.push({
message: error.message,
stack: error.stack,
timestamp: Date.now(),
});
});
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);
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.`
);
}
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) {
console.log('All Runtime Errors:', allErrors);
}
});
});
Error Blocking Decision Tree
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)
Runtime Error Report Format
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"
}
Pre-Existing Error Detection
test.beforeEach(async ({ page }) => {
const baselineErrors: string[] = [];
page.on('pageerror', (error) => {
baselineErrors.push(error.message);
});
await page.goto('http://localhost:3000');
await page.waitForTimeout(2000);
(page as any).__baselineErrors = baselineErrors;
});
Validation Blocking Rules
| 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):
- "Cannot read properties of undefined" runtime error blocked browser validation for feat-tps-005
- Pre-existing errors need separate bugfix tasks, not to block current task indefinitely
- QA must distinguish between task-caused errors and pre-existing issues
Page Object Model Usage
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);
}
});
Cross-Browser Testing for WebGL
| 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 |
Running Tests by Browser
npm run test:e2e -- --project=chromium-webgl
npm run test:e2e -- --project=firefox-webgl --headed
xvfb-run --auto-servernum npm run test:e2e -- --project=firefox-webgl
npm run test:e2e
Firefox WebGL Testing Setup
Firefox doesn't support WebGL in headless mode. Configure project in playwright.config.ts:
{
name: 'firefox-webgl',
use: {
...devices['Desktop Firefox'],
headless: false,
},
}
For CI/CD with Firefox, use Xvfb (virtual display):
- 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
Hybrid Model: Tests Serve Dual Purpose
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
Decision Framework
| 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 |
Anti-Patterns
❌ DON'T:
- Use Playwright MCP directly for validation
- Skip E2E tests because automated checks passed
- Mark as passed without running tests
- Assume "it works on my machine"
✅ DO:
- Always run E2E tests for validation
- Create tests if missing
- Verify all acceptance criteria with tests
- Document failures with test output
Validation Checklist
For each validation:
Bug Report Format
When 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}
Server Management
⚠️ CRITICAL: Use shared-lifecycle skill for server management.
Server Detection (Before Any Browser Interaction)
⚠️ 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.CI
npm run server (port 2567) with reuseExistingServer: false (for multiplayer)
DO NOT manually start servers for E2E tests.
Decision Tree
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
Server Check Pattern
netstat -an | grep :3000 || lsof -i :3000
curl -s http://localhost:3000 | grep -q "vite" && echo "RUNNING" || echo "NOT_RUNNING"
E2E Test Path (Standard Validation)
npm run test:e2e
Manual MCP Validation Path (Only when explicitly needed)
if ! netstat -an | grep :3000; then
Bash(command="npm run dev", run_in_background=true)
fi
TaskStop(task_id="abc123")
MANDATORY CLEANUP after all tests complete (pass OR fail):
Use the cleanup patterns from shared-lifecycle skill to ensure:
- Dev server is stopped
- Ports are released
- No orphaned processes remain
See also: shared-lifecycle skill for complete process management patterns.
References
Camera Validation Pattern (feat-tps-004, 2026-01-27)
⚠️ CRITICAL: Camera validation requires E2E tests with exact value verification.
When validating camera features (TPS, orbital, etc.), use systematic E2E test coverage:
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');
const cameraState = await page.evaluate(() => {
return (window as any).__cameraTestState;
});
expect(cameraState.shoulderOffsetRight).toBe(0.75);
expect(cameraState.shoulderOffsetLeft).toBe(-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');
await page.screenshot({
path: 'test-results/camera/shoulder-offset.png',
});
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
- Solution: E2E tests verify EXACT numerical values from acceptance criteria
-
Missing look-at offset: Camera position was offset but looked at center
- Solution: E2E tests verify BOTH position and look-at are offset
-
Scene routing test: URL-based scene loading must be tested
- Solution: Test
?scene=controls actually loads correct scene
Camera E2E test checklist:
URL Scene Routing Pattern:
const [sceneParam, setSceneParam] = useState<string | null>(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const scene = params.get('scene');
if (scene) {
setSceneParam(scene);
gameStore.setPhase(scene as any);
}
}, []);