Add Playwright test infrastructure for wxWidgets WASM port
- Add tests/ directory with Playwright e2e tests - Add minimal wxWidgets WASM test application - Add build script for test app (scripts/build-wasm-test.sh) - Update wxWidgets submodule with WASM build fixes - Update .gitignore for test artifacts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0ac29130ca
commit
3f67fed173
10 changed files with 1506 additions and 2 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -29,4 +29,12 @@ cmake-build-*/
|
|||
.emscripten_cache/
|
||||
|
||||
# Clean clone for reproducible builds (generated by build-wxwidgets-wasm-clean.sh)
|
||||
wxwidgets-clean/
|
||||
wxwidgets-clean/
|
||||
|
||||
# Test artifacts
|
||||
/tests/node_modules/
|
||||
/tests/playwright-report/
|
||||
/tests/test-results/
|
||||
/tests/wasm-app/*.js
|
||||
/tests/wasm-app/*.wasm
|
||||
/temp/
|
||||
|
|
|
|||
58
scripts/build-wasm-test.sh
Executable file
58
scripts/build-wasm-test.sh
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
#!/bin/bash
|
||||
# Build the minimal wxWidgets WASM test application
|
||||
# This script creates library symlinks and builds the test app
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
BUILD_DIR="$PROJECT_ROOT/build-wasm/wxwidgets-universal"
|
||||
TESTS_DIR="$PROJECT_ROOT/tests"
|
||||
WASM_APP_DIR="$TESTS_DIR/wasm-app"
|
||||
|
||||
echo "=== Building wxWidgets WASM Test Application ==="
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
echo "wxWidgets build: $BUILD_DIR"
|
||||
echo "Test app dir: $WASM_APP_DIR"
|
||||
|
||||
# Verify wxWidgets is built
|
||||
if [ ! -f "$BUILD_DIR/wx-config" ]; then
|
||||
echo "ERROR: wxWidgets not built. Run build-wxuniversal-wasm.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create library symlinks
|
||||
# wx-config outputs paths like libwx_baseu-3.2.a but files are named libwx_baseu-3.2-emscripten.a
|
||||
echo ""
|
||||
echo "=== Creating library symlinks ==="
|
||||
cd "$BUILD_DIR/lib"
|
||||
for f in libwx_*-emscripten.a; do
|
||||
if [ -f "$f" ]; then
|
||||
link="${f%-emscripten.a}.a"
|
||||
if [ ! -e "$link" ]; then
|
||||
ln -s "$f" "$link"
|
||||
echo "Created: $link -> $f"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Build the test app
|
||||
echo ""
|
||||
echo "=== Building test application ==="
|
||||
cd "$WASM_APP_DIR"
|
||||
|
||||
# Clean any previous build
|
||||
make -f Makefile.wasm clean 2>/dev/null || true
|
||||
|
||||
# Build
|
||||
make -f Makefile.wasm
|
||||
|
||||
echo ""
|
||||
echo "=== Build complete ==="
|
||||
ls -lh "$WASM_APP_DIR"/minimal_test.* 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "To run the tests:"
|
||||
echo " cd $TESTS_DIR"
|
||||
echo " npm install"
|
||||
echo " npm test"
|
||||
43
tests/README.md
Normal file
43
tests/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# KiCad WASM Tests
|
||||
|
||||
Playwright tests for verifying the wxWidgets WASM port.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- Emscripten SDK (for building)
|
||||
|
||||
## Building the Test App
|
||||
|
||||
```bash
|
||||
../scripts/build-wasm-test.sh
|
||||
```
|
||||
|
||||
This builds `wasm-app/minimal_test.{html,js,wasm}`.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm test
|
||||
```
|
||||
|
||||
## Viewing the App Directly
|
||||
|
||||
Start a local server in the wasm-app directory:
|
||||
|
||||
```bash
|
||||
cd wasm-app
|
||||
npx serve .
|
||||
```
|
||||
|
||||
Then open http://localhost:3000/minimal_test.html in your browser.
|
||||
|
||||
Alternative using Python:
|
||||
|
||||
```bash
|
||||
cd wasm-app
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
Then open http://localhost:8000/minimal_test.html
|
||||
70
tests/e2e/wxwidgets.spec.ts
Normal file
70
tests/e2e/wxwidgets.spec.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('wxWidgets WASM', () => {
|
||||
test('app loads without JavaScript errors', async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', err => errors.push(err.message));
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/minimal_test.html');
|
||||
|
||||
// Wait for WASM to initialize (canvas becomes visible)
|
||||
await page.waitForSelector('canvas', { state: 'visible', timeout: 30000 });
|
||||
|
||||
// Give it a moment to settle
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Filter out known non-critical errors
|
||||
const criticalErrors = errors.filter(e =>
|
||||
!e.includes('SharedArrayBuffer') && // COOP/COEP warning
|
||||
!e.includes('cross-origin')
|
||||
);
|
||||
|
||||
expect(criticalErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('canvas element is rendered with dimensions', async ({ page }) => {
|
||||
await page.goto('/minimal_test.html');
|
||||
|
||||
const canvas = page.locator('canvas');
|
||||
await expect(canvas).toBeVisible({ timeout: 30000 });
|
||||
|
||||
// Canvas should have non-zero dimensions
|
||||
const box = await canvas.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
expect(box?.width).toBeGreaterThan(0);
|
||||
expect(box?.height).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('loading progress completes', async ({ page }) => {
|
||||
await page.goto('/minimal_test.html');
|
||||
|
||||
// Progress text should eventually show completion or disappear
|
||||
await page.waitForFunction(() => {
|
||||
const status = document.getElementById('progress-text');
|
||||
if (!status) return true;
|
||||
const text = status.textContent || '';
|
||||
return text.toLowerCase().includes('complete') ||
|
||||
text === '' ||
|
||||
status.style.display === 'none';
|
||||
}, { timeout: 30000 });
|
||||
});
|
||||
|
||||
test('WASM module initializes successfully', async ({ page }) => {
|
||||
await page.goto('/minimal_test.html');
|
||||
|
||||
// Wait for canvas to be visible (indicates WASM loaded)
|
||||
await page.waitForSelector('canvas', { state: 'visible', timeout: 30000 });
|
||||
|
||||
// Check that the Module object exists and is initialized
|
||||
const moduleExists = await page.evaluate(() => {
|
||||
return typeof (window as any).Module !== 'undefined';
|
||||
});
|
||||
|
||||
expect(moduleExists).toBe(true);
|
||||
});
|
||||
});
|
||||
1124
tests/package-lock.json
generated
Normal file
1124
tests/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
16
tests/package.json
Normal file
16
tests/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "kicad-wasm-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "Playwright tests for wxWidgets WASM port",
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:ui": "playwright test --ui",
|
||||
"test:headed": "playwright test --headed",
|
||||
"build-wasm": "cd wasm-app && make -f Makefile.wasm",
|
||||
"serve": "npx serve wasm-app -p 8080"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
"serve": "^14.2.0"
|
||||
}
|
||||
}
|
||||
29
tests/playwright.config.ts
Normal file
29
tests/playwright.config.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
timeout: 60000, // WASM can be slow to load
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'npx serve wasm-app -p 8080',
|
||||
port: 8080,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
42
tests/wasm-app/minimal_test.cpp
Normal file
42
tests/wasm-app/minimal_test.cpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Minimal wxWidgets WASM Test Application
|
||||
// Purpose: Verify wxWidgets WASM port is working correctly
|
||||
|
||||
#include "wx/wxprec.h"
|
||||
|
||||
#ifndef WX_PRECOMP
|
||||
#include "wx/wx.h"
|
||||
#endif
|
||||
|
||||
class TestApp : public wxApp
|
||||
{
|
||||
public:
|
||||
virtual bool OnInit() wxOVERRIDE;
|
||||
};
|
||||
|
||||
class TestFrame : public wxFrame
|
||||
{
|
||||
public:
|
||||
TestFrame(const wxString& title);
|
||||
};
|
||||
|
||||
wxIMPLEMENT_APP(TestApp);
|
||||
|
||||
bool TestApp::OnInit()
|
||||
{
|
||||
if (!wxApp::OnInit())
|
||||
return false;
|
||||
|
||||
TestFrame *frame = new TestFrame("wxWidgets WASM Test");
|
||||
frame->Show(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TestFrame::TestFrame(const wxString& title)
|
||||
: wxFrame(NULL, wxID_ANY, title, wxDefaultPosition, wxSize(640, 480))
|
||||
{
|
||||
#if wxUSE_STATUSBAR
|
||||
CreateStatusBar();
|
||||
SetStatusText("wxWidgets WASM is working!");
|
||||
#endif
|
||||
}
|
||||
114
tests/wasm-app/minimal_test.html
Normal file
114
tests/wasm-app/minimal_test.html
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title></title>
|
||||
<style>
|
||||
.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
|
||||
div.emscripten { text-align: center; }
|
||||
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
|
||||
canvas.emscripten { border: 0px none; }
|
||||
|
||||
.window {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
background-color: black;
|
||||
overflow: hidden;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.window-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden">
|
||||
<div id="main-window"></div>
|
||||
|
||||
<div id="progress-container" class="centered">
|
||||
<div id="progress-text">Loading...</div>
|
||||
<div id="progress-bar">
|
||||
<div id="progress-bar-position">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="window-container"></div>
|
||||
|
||||
<script>
|
||||
var mainWindow = document.getElementById('main-window');
|
||||
var statusElement = document.getElementById('progress-text');
|
||||
var progressElement = document.getElementById('progress-bar-position');
|
||||
|
||||
var createCanvas = function () {
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.id = 'canvas';
|
||||
canvas.style.display = 'none';
|
||||
canvas.style.width = window.innerWidth + 'px';
|
||||
canvas.style.height = window.innerHeight + 'px';
|
||||
canvas.oncontextmenu = function () { event.preventDefault(); }
|
||||
canvas.addEventListener("webglcontextlost", function (e) {
|
||||
showError('WebGL context lost. You will need to reload the page.');
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
|
||||
mainWindow.appendChild(canvas);
|
||||
|
||||
Module.canvas = canvas;
|
||||
};
|
||||
|
||||
var onRuntimeInitialized = function () {
|
||||
var canvas = Module.canvas;
|
||||
canvas.style.display = 'block';
|
||||
};
|
||||
|
||||
var Module = {
|
||||
preRun: [createCanvas],
|
||||
postRun: [],
|
||||
print: function(text) {
|
||||
if (arguments.length > 1)
|
||||
text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.log(text);
|
||||
},
|
||||
printErr: function(text) {
|
||||
if (arguments.length > 1)
|
||||
text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.error(text);
|
||||
},
|
||||
setStatus: function(text) {
|
||||
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
|
||||
if (text === Module.setStatus.text) return;
|
||||
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
|
||||
if (m) {
|
||||
text = m[1];
|
||||
progressElement.style.width = m[2] / m[4] + '%';
|
||||
progressElement.hidden = false;
|
||||
} else {
|
||||
progressElement.hidden = true;
|
||||
}
|
||||
statusElement.innerHTML = text;
|
||||
},
|
||||
totalDependencies: 0,
|
||||
monitorRunDependencies: function(left) {
|
||||
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
|
||||
},
|
||||
onRuntimeInitialized: onRuntimeInitialized
|
||||
};
|
||||
Module.setStatus('Downloading...');
|
||||
window.onerror = function() {
|
||||
showError('Exception thrown, see JavaScript console');
|
||||
Module.setStatus = function(text) {
|
||||
if (text) Module.printErr('[post-exception status] ' + text);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<script async type="text/javascript" src="minimal_test.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 4ab6d13cf0ae7469809236b24b81d658738a4ffa
|
||||
Subproject commit 5ca6237800627c7d5e49a60ac2317ac71fa1e5e3
|
||||
Loading…
Reference in a new issue