feat(webgl): Add WebGL GAL test infrastructure (Phase 1)

Add complete test infrastructure for WebGL GAL visual regression testing:

- scripts/test-gal-regression.sh: Master script that builds both backends,
  runs tests, and performs two-level comparison (native vs baseline,
  webgl vs native)
- scripts/build-gal-webgl-test.sh: WASM build using Makefile with em++
- tests/gal-regression/wasm/: WebGL test harness (stub WEBGL_GAL)
- tests/e2e/gal-webgl.spec.ts: Playwright test for screenshot capture

Fix Homebrew Emscripten environment in scripts/common/env.sh:
- Set EMSDK_PYTHON for Python 3.10+ (em++ reads this, not $PYTHON)
- Add bundled LLVM to PATH (Emscripten needs its clang with WASM backend)

Verified: Native vs Baseline passes (28/28), WebGL generates blank
screenshots as expected (WEBGL_GAL implementation is Phase 2).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Viktor Vaczi 2026-01-07 16:11:31 +01:00
commit a4f444fea8
10 changed files with 1401 additions and 0 deletions

View file

@ -0,0 +1,190 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GAL WebGL Test</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a26;
color: #fff;
font-family: monospace;
}
#canvas-container {
display: inline-block;
border: 2px solid #444;
}
#canvas {
display: block;
}
#controls {
margin-top: 20px;
}
#status {
margin-top: 10px;
padding: 10px;
background: #2a2a3a;
border-radius: 4px;
}
button {
padding: 8px 16px;
margin-right: 10px;
cursor: pointer;
}
select {
padding: 8px;
margin-right: 10px;
}
</style>
</head>
<body>
<h1>GAL WebGL Test</h1>
<div id="canvas-container">
<canvas id="canvas" width="800" height="600"></canvas>
</div>
<div id="controls">
<select id="scenario-select">
<option value="-1">Select scenario...</option>
</select>
<button id="run-btn" disabled>Run Scenario</button>
<button id="run-all-btn" disabled>Run All</button>
</div>
<div id="status">
<div id="status-text">Loading WASM module...</div>
</div>
<script>
// Scenario names (matching native test)
const SCENARIO_NAMES = [
'basic-lines', // 0
'line-widths', // 1
'circles', // 2
'arcs', // 3
'rectangles', // 4
'polygons', // 5
'alpha-blending', // 6
'transforms', // 7
'grid-cursor', // 8
'segments', // 9
'complex-scene', // 10
'bezier-curves', // 11
'arc-segments', // 12
'segment-chain', // 13
'group-caching', // 14
'polylines-multi', // 15
'hole-walls', // 16
'grid-native', // 17
'cursor-native', // 18
'render-targets', // 19
'screen-transform', // 20
'clear-colors', // 21
'depth-testing', // 22
'negative-mode', // 23
'text-attrs', // 24
'glyphs', // 25
'bitmap', // 26
'transform-api' // 27
];
let Module = null;
// Populate scenario dropdown
function populateScenarios() {
const select = document.getElementById('scenario-select');
SCENARIO_NAMES.forEach((name, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${index}: ${name}`;
select.appendChild(option);
});
}
// Run a single scenario
function runScenario(index) {
if (!Module) return;
const result = Module.ccall('runScenario', 'number', ['number'], [index]);
if (result === 0) {
setStatus(`Rendered scenario ${index}: ${SCENARIO_NAMES[index]}`);
} else {
setStatus(`ERROR: Failed to render scenario ${index}`);
}
}
// Run all scenarios (for automated testing)
async function runAllScenarios() {
if (!Module) return;
const total = Module.ccall('getTotalScenarios', 'number', [], []);
setStatus(`Running all ${total} scenarios...`);
for (let i = 0; i < total; i++) {
runScenario(i);
// Small delay to allow rendering
await new Promise(r => setTimeout(r, 100));
}
setStatus(`Completed all ${total} scenarios`);
}
// Update status display
function setStatus(text) {
document.getElementById('status-text').textContent = text;
console.log('[GAL Test]', text);
}
// Export for Playwright
window.galTest = {
runScenario,
runAllScenarios,
getScenarioName: (index) => SCENARIO_NAMES[index],
getTotalScenarios: () => SCENARIO_NAMES.length
};
// Setup UI after module loads
function onModuleReady() {
setStatus('WASM module loaded. Ready for testing.');
document.getElementById('run-btn').disabled = false;
document.getElementById('run-all-btn').disabled = false;
document.getElementById('run-btn').onclick = () => {
const select = document.getElementById('scenario-select');
const index = parseInt(select.value);
if (index >= 0) {
runScenario(index);
}
};
document.getElementById('run-all-btn').onclick = runAllScenarios;
document.getElementById('scenario-select').onchange = (e) => {
const index = parseInt(e.target.value);
if (index >= 0) {
runScenario(index);
}
};
// Dispatch custom event for Playwright
window.dispatchEvent(new CustomEvent('gal-test-ready'));
}
// Initialize
populateScenarios();
// Load WASM module
createGALTest().then(module => {
Module = module;
onModuleReady();
}).catch(err => {
setStatus('ERROR: Failed to load WASM module: ' + err.message);
console.error(err);
});
</script>
<script src="gal_webgl_test.js"></script>
</body>
</html>