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:
parent
344cf3c432
commit
a4f444fea8
10 changed files with 1401 additions and 0 deletions
190
tests/apps/gal-webgl/gal_webgl_test.html
Normal file
190
tests/apps/gal-webgl/gal_webgl_test.html
Normal 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>
|
||||
142
tests/e2e/gal-webgl.spec.ts
Normal file
142
tests/e2e/gal-webgl.spec.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* GAL WebGL Regression Test
|
||||
*
|
||||
* Runs all 28 GAL test scenarios in WebGL and captures screenshots
|
||||
* for comparison against native OpenGL rendering.
|
||||
*/
|
||||
|
||||
import { test, expect } from './utils/fixtures';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Scenario names (must match 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
|
||||
];
|
||||
|
||||
// Output directory for WebGL screenshots
|
||||
const OUTPUT_DIR = path.join(__dirname, '../gal-regression/output/webgl');
|
||||
|
||||
test.describe('GAL WebGL Regression Tests', () => {
|
||||
test.beforeAll(async () => {
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('Load GAL WebGL test module', async ({ page, testLogger }) => {
|
||||
await page.goto('/gal-webgl/gal_webgl_test.html');
|
||||
|
||||
// Wait for the custom event indicating module is ready
|
||||
await page.waitForFunction(() => {
|
||||
return (window as any).galTest !== undefined;
|
||||
}, { timeout: 60000 });
|
||||
|
||||
// Verify module loaded
|
||||
const totalScenarios = await page.evaluate(() => {
|
||||
return (window as any).galTest.getTotalScenarios();
|
||||
});
|
||||
|
||||
expect(totalScenarios).toBe(28);
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(OUTPUT_DIR, 'gal-module-loaded.png'),
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
console.log(`GAL WebGL test module loaded with ${totalScenarios} scenarios`);
|
||||
});
|
||||
|
||||
// Generate a test for each scenario
|
||||
for (let i = 0; i < SCENARIO_NAMES.length; i++) {
|
||||
const scenarioName = SCENARIO_NAMES[i];
|
||||
const scenarioIndex = i;
|
||||
|
||||
test(`Scenario ${scenarioIndex}: ${scenarioName}`, async ({ page, testLogger }) => {
|
||||
await page.goto('/gal-webgl/gal_webgl_test.html');
|
||||
|
||||
// Wait for module to be ready
|
||||
await page.waitForFunction(() => {
|
||||
return (window as any).galTest !== undefined;
|
||||
}, { timeout: 60000 });
|
||||
|
||||
// Run the scenario
|
||||
await page.evaluate((index) => {
|
||||
(window as any).galTest.runScenario(index);
|
||||
}, scenarioIndex);
|
||||
|
||||
// Wait for rendering to complete
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Get the canvas element and take a screenshot of just the canvas
|
||||
const canvas = await page.locator('#canvas');
|
||||
await expect(canvas).toBeVisible();
|
||||
|
||||
// Screenshot the canvas (matching native 800x600 output)
|
||||
const screenshotPath = path.join(OUTPUT_DIR, `gal-${scenarioName}.png`);
|
||||
await canvas.screenshot({ path: screenshotPath });
|
||||
|
||||
console.log(`Saved: ${screenshotPath}`);
|
||||
});
|
||||
}
|
||||
|
||||
test('Run all scenarios sequentially', async ({ page, testLogger }) => {
|
||||
await page.goto('/gal-webgl/gal_webgl_test.html');
|
||||
|
||||
// Wait for module to be ready
|
||||
await page.waitForFunction(() => {
|
||||
return (window as any).galTest !== undefined;
|
||||
}, { timeout: 60000 });
|
||||
|
||||
console.log('Running all 28 scenarios...');
|
||||
|
||||
for (let i = 0; i < SCENARIO_NAMES.length; i++) {
|
||||
const scenarioName = SCENARIO_NAMES[i];
|
||||
|
||||
// Run scenario
|
||||
await page.evaluate((index) => {
|
||||
(window as any).galTest.runScenario(index);
|
||||
}, i);
|
||||
|
||||
// Wait for rendering
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
// Screenshot the canvas
|
||||
const canvas = await page.locator('#canvas');
|
||||
const screenshotPath = path.join(OUTPUT_DIR, `gal-${scenarioName}.png`);
|
||||
await canvas.screenshot({ path: screenshotPath });
|
||||
|
||||
console.log(`[${i + 1}/28] ${scenarioName}`);
|
||||
}
|
||||
|
||||
console.log('All scenarios completed');
|
||||
});
|
||||
});
|
||||
62
tests/gal-regression/wasm/Makefile
Normal file
62
tests/gal-regression/wasm/Makefile
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Makefile for GAL WebGL Test (WASM)
|
||||
#
|
||||
# Uses direct em++ calls like tests/apps/Makefile.wasm
|
||||
# Avoids emcmake/cmake which require Python 3.10+
|
||||
#
|
||||
# Usage:
|
||||
# make # Build
|
||||
# make clean # Clean build artifacts
|
||||
# make DEBUG=1 # Debug build with source maps
|
||||
|
||||
CXX = em++
|
||||
|
||||
# Output directory
|
||||
OUTPUT_DIR = ../../apps/gal-webgl
|
||||
|
||||
# Debug or Release build
|
||||
ifdef DEBUG
|
||||
CXXFLAGS = -g -O0
|
||||
DEBUG_LDFLAGS = -g -gsource-map
|
||||
else
|
||||
CXXFLAGS = -O2
|
||||
DEBUG_LDFLAGS =
|
||||
endif
|
||||
|
||||
# Emscripten flags for WebGL 2.0
|
||||
EM_FLAGS = -sUSE_WEBGL2=1 \
|
||||
-sFULL_ES3=1 \
|
||||
-sALLOW_MEMORY_GROWTH=1 \
|
||||
-sEXPORTED_FUNCTIONS=['_main','_runScenario','_getTotalScenarios','_getCurrentScenario','_getCanvasWidth','_getCanvasHeight'] \
|
||||
-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap'] \
|
||||
-sMODULARIZE=1 \
|
||||
-sEXPORT_NAME='createGALTest' \
|
||||
-sENVIRONMENT=web
|
||||
|
||||
LDFLAGS = $(DEBUG_LDFLAGS) $(EM_FLAGS)
|
||||
|
||||
# Source files
|
||||
SRCS = gal_webgl_test.cpp
|
||||
OBJS = $(SRCS:.cpp=.o)
|
||||
|
||||
# Target
|
||||
TARGET = $(OUTPUT_DIR)/gal_webgl_test.js
|
||||
|
||||
all: $(OUTPUT_DIR) $(TARGET)
|
||||
|
||||
$(OUTPUT_DIR):
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
|
||||
%.o: %.cpp
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
||||
$(TARGET): $(OBJS)
|
||||
$(CXX) $(OBJS) $(LDFLAGS) -o $@
|
||||
cp gal_webgl_test.html $(OUTPUT_DIR)/
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS)
|
||||
rm -f $(OUTPUT_DIR)/gal_webgl_test.js
|
||||
rm -f $(OUTPUT_DIR)/gal_webgl_test.wasm
|
||||
rm -f $(OUTPUT_DIR)/gal_webgl_test.html
|
||||
|
||||
.PHONY: all clean
|
||||
147
tests/gal-regression/wasm/gal_webgl_test.cpp
Normal file
147
tests/gal-regression/wasm/gal_webgl_test.cpp
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* GAL WebGL Test - WASM Entry Point
|
||||
*
|
||||
* This is a test harness that renders GAL test scenarios using WEBGL_GAL
|
||||
* and allows Playwright to capture screenshots for comparison against native.
|
||||
*
|
||||
* Phase 1: Stub that initializes WebGL and renders a test pattern
|
||||
* Phase 2: Full WEBGL_GAL implementation with all scenarios
|
||||
*/
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
// Canvas dimensions (matching native test)
|
||||
static const int CANVAS_WIDTH = 800;
|
||||
static const int CANVAS_HEIGHT = 600;
|
||||
|
||||
// Current scenario index
|
||||
static int g_currentScenario = -1;
|
||||
static int g_totalScenarios = 28;
|
||||
|
||||
// WebGL context
|
||||
static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE g_glContext = 0;
|
||||
|
||||
/**
|
||||
* Initialize WebGL context
|
||||
*/
|
||||
bool initWebGL() {
|
||||
EmscriptenWebGLContextAttributes attrs;
|
||||
emscripten_webgl_init_context_attributes(&attrs);
|
||||
|
||||
attrs.majorVersion = 2; // WebGL 2.0
|
||||
attrs.minorVersion = 0;
|
||||
attrs.alpha = true;
|
||||
attrs.depth = true;
|
||||
attrs.stencil = true;
|
||||
attrs.antialias = false; // We handle AA ourselves
|
||||
attrs.premultipliedAlpha = false;
|
||||
attrs.preserveDrawingBuffer = true; // Needed for screenshots
|
||||
|
||||
g_glContext = emscripten_webgl_create_context("#canvas", &attrs);
|
||||
if (g_glContext <= 0) {
|
||||
printf("ERROR: Failed to create WebGL 2.0 context: %d\n", g_glContext);
|
||||
return false;
|
||||
}
|
||||
|
||||
emscripten_webgl_make_context_current(g_glContext);
|
||||
|
||||
printf("WebGL 2.0 context created successfully\n");
|
||||
printf(" GL_VENDOR: %s\n", glGetString(GL_VENDOR));
|
||||
printf(" GL_RENDERER: %s\n", glGetString(GL_RENDERER));
|
||||
printf(" GL_VERSION: %s\n", glGetString(GL_VERSION));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a test pattern (stub for Phase 1)
|
||||
* In Phase 2, this will be replaced with actual WEBGL_GAL rendering
|
||||
*/
|
||||
void renderTestPattern(int scenarioIndex) {
|
||||
// Set viewport
|
||||
glViewport(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
// Clear with KiCad-like dark background
|
||||
glClearColor(0.102f, 0.102f, 0.149f, 1.0f); // RGB(26, 26, 38)
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// TODO Phase 2: Replace with actual WEBGL_GAL rendering
|
||||
// For now, just render a different colored rectangle for each scenario
|
||||
// to verify the pipeline works
|
||||
|
||||
// This is a placeholder - in Phase 2 we'll call:
|
||||
// GALTest::RenderScenario(webglGal, scenarioIndex, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
printf("Rendered scenario %d (stub)\n", scenarioIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a specific scenario
|
||||
* Called from JavaScript: Module.ccall('runScenario', 'number', ['number'], [index])
|
||||
*/
|
||||
extern "C" {
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int runScenario(int scenarioIndex) {
|
||||
if (scenarioIndex < 0 || scenarioIndex >= g_totalScenarios) {
|
||||
printf("ERROR: Invalid scenario index %d (valid: 0-%d)\n",
|
||||
scenarioIndex, g_totalScenarios - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_currentScenario = scenarioIndex;
|
||||
renderTestPattern(scenarioIndex);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int getTotalScenarios() {
|
||||
return g_totalScenarios;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int getCurrentScenario() {
|
||||
return g_currentScenario;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int getCanvasWidth() {
|
||||
return CANVAS_WIDTH;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int getCanvasHeight() {
|
||||
return CANVAS_HEIGHT;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
int main() {
|
||||
printf("GAL WebGL Test - Phase 1 Stub\n");
|
||||
printf("============================\n\n");
|
||||
|
||||
// Set canvas size
|
||||
emscripten_set_canvas_element_size("#canvas", CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
// Initialize WebGL
|
||||
if (!initWebGL()) {
|
||||
printf("Failed to initialize WebGL\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("\nReady for scenarios. Total: %d\n", g_totalScenarios);
|
||||
printf("Call runScenario(index) from JavaScript to render.\n");
|
||||
|
||||
// Don't exit - keep runtime alive for JavaScript calls
|
||||
emscripten_exit_with_live_runtime();
|
||||
|
||||
return 0;
|
||||
}
|
||||
190
tests/gal-regression/wasm/gal_webgl_test.html
Normal file
190
tests/gal-regression/wasm/gal_webgl_test.html
Normal 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>
|
||||
Loading…
Reference in a new issue