Improve KiCad test workflow and separate from wxWidgets tests
- npm run test:kicad now auto-runs setup script first - Create separate playwright-kicad.config.ts for KiCad tests - Fix kicad spec to use web server instead of file:// paths - Remove duplicate pcbnew.html/wasm from tests/kicad/ (proper files are in tests/apps/kicad/) Test separation: - npm test: runs only wxWidgets tests (tests/e2e/) - npm run test:kicad: runs only KiCad tests (tests/kicad/) 🤖 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
98b523c2bc
commit
2ad6786198
4 changed files with 85 additions and 417 deletions
|
|
@ -1,318 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KiCad PCBnew - WASM</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #2c2c2c;
|
||||
color: #fff;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
background: #3c3c3c;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid #555;
|
||||
}
|
||||
|
||||
#toolbar h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 12px;
|
||||
background: #5c5c5c;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #6c6c6c;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
background: #4c4c4c;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#canvas-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 300px;
|
||||
background: #353535;
|
||||
border-left: 1px solid #555;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#status-bar {
|
||||
background: #3c3c3c;
|
||||
padding: 4px 16px;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid #555;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#loading-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 3px solid #333;
|
||||
border-top-color: #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
#loading-text {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
width: 300px;
|
||||
height: 4px;
|
||||
background: #333;
|
||||
border-radius: 2px;
|
||||
margin-top: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#progress-fill {
|
||||
height: 100%;
|
||||
background: #007bff;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
#log-panel {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 11px;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
}
|
||||
|
||||
.log-info { color: #a0a0a0; }
|
||||
.log-warn { color: #ffcc00; }
|
||||
.log-error { color: #ff6b6b; }
|
||||
.log-success { color: #69db7c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading-overlay">
|
||||
<div class="spinner"></div>
|
||||
<div id="loading-text">Loading PCBnew...</div>
|
||||
<div id="progress-bar">
|
||||
<div id="progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toolbar">
|
||||
<h1>KiCad PCBnew (WASM)</h1>
|
||||
<button class="btn" id="btn-new">New</button>
|
||||
<button class="btn" id="btn-open">Open</button>
|
||||
<button class="btn" id="btn-save">Save</button>
|
||||
<button class="btn" id="btn-zoom-in">Zoom +</button>
|
||||
<button class="btn" id="btn-zoom-out">Zoom -</button>
|
||||
<button class="btn" id="btn-fit">Fit</button>
|
||||
</div>
|
||||
|
||||
<div id="main-container">
|
||||
<div id="canvas-container">
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
<div id="sidebar">
|
||||
<div id="log-panel"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status-bar">
|
||||
<span id="status-coords">X: 0.00mm Y: 0.00mm</span>
|
||||
<span id="status-info">Ready</span>
|
||||
<span id="status-memory">Memory: --</span>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
// Logger
|
||||
const logPanel = document.getElementById('log-panel');
|
||||
const log = (msg, type = 'info') => {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry log-${type}`;
|
||||
entry.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
|
||||
logPanel.appendChild(entry);
|
||||
logPanel.scrollTop = logPanel.scrollHeight;
|
||||
console.log(`[${type}] ${msg}`);
|
||||
};
|
||||
|
||||
// Loading state
|
||||
const loadingOverlay = document.getElementById('loading-overlay');
|
||||
const loadingText = document.getElementById('loading-text');
|
||||
const progressFill = document.getElementById('progress-fill');
|
||||
|
||||
const updateProgress = (percent, text) => {
|
||||
progressFill.style.width = `${percent}%`;
|
||||
if (text) loadingText.textContent = text;
|
||||
};
|
||||
|
||||
// Memory monitoring
|
||||
const updateMemory = () => {
|
||||
if (performance.memory) {
|
||||
const used = (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(1);
|
||||
document.getElementById('status-memory').textContent = `Memory: ${used}MB`;
|
||||
}
|
||||
};
|
||||
setInterval(updateMemory, 1000);
|
||||
|
||||
// Main initialization
|
||||
async function init() {
|
||||
try {
|
||||
log('Starting PCBnew initialization...', 'info');
|
||||
updateProgress(10, 'Loading WebAssembly module...');
|
||||
|
||||
// Import the PCBnew module
|
||||
// In production, this would be the compiled KiCad WASM
|
||||
const moduleUrl = './pcbnew.js';
|
||||
|
||||
log(`Loading module from ${moduleUrl}`, 'info');
|
||||
updateProgress(30, 'Compiling WebAssembly...');
|
||||
|
||||
// For now, just show that we're ready to load
|
||||
// The actual module loading would happen here
|
||||
log('Note: PCBnew WASM module not yet built', 'warn');
|
||||
log('Run: ./scripts/build-pcbnew-wasm.sh', 'warn');
|
||||
|
||||
updateProgress(100, 'Ready (module not loaded)');
|
||||
|
||||
setTimeout(() => {
|
||||
loadingOverlay.classList.add('hidden');
|
||||
log('UI ready', 'success');
|
||||
}, 500);
|
||||
|
||||
} catch (error) {
|
||||
log(`Failed to initialize: ${error.message}`, 'error');
|
||||
loadingText.textContent = `Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Button handlers
|
||||
document.getElementById('btn-new').addEventListener('click', () => {
|
||||
log('New project', 'info');
|
||||
});
|
||||
|
||||
document.getElementById('btn-open').addEventListener('click', () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.kicad_pcb,.kicad_mod';
|
||||
input.onchange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
log(`Opening: ${file.name}`, 'info');
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
});
|
||||
|
||||
document.getElementById('btn-save').addEventListener('click', () => {
|
||||
log('Save project', 'info');
|
||||
});
|
||||
|
||||
document.getElementById('btn-zoom-in').addEventListener('click', () => {
|
||||
log('Zoom in', 'info');
|
||||
});
|
||||
|
||||
document.getElementById('btn-zoom-out').addEventListener('click', () => {
|
||||
log('Zoom out', 'info');
|
||||
});
|
||||
|
||||
document.getElementById('btn-fit').addEventListener('click', () => {
|
||||
log('Fit to view', 'info');
|
||||
});
|
||||
|
||||
// Canvas mouse tracking
|
||||
const canvas = document.getElementById('canvas');
|
||||
const statusCoords = document.getElementById('status-coords');
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) * 0.1).toFixed(2);
|
||||
const y = ((e.clientY - rect.top) * 0.1).toFixed(2);
|
||||
statusCoords.textContent = `X: ${x}mm Y: ${y}mm`;
|
||||
});
|
||||
|
||||
// Resize handling
|
||||
function resizeCanvas() {
|
||||
const container = document.getElementById('canvas-container');
|
||||
canvas.width = container.clientWidth;
|
||||
canvas.height = container.clientHeight;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
// Start initialization
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,115 +1,33 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* PCBnew WASM E2E Tests
|
||||
*
|
||||
* These tests verify that the KiCad PCBnew application runs correctly in the browser.
|
||||
* They test the basic UI functionality and WASM module loading.
|
||||
* The WASM files are served from apps/kicad/ via the web server.
|
||||
*/
|
||||
|
||||
test.describe('PCBnew WASM', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to PCBnew test page
|
||||
const htmlPath = path.join(__dirname, 'pcbnew.html');
|
||||
await page.goto(`file://${htmlPath}`);
|
||||
|
||||
// Wait for loading overlay to disappear
|
||||
await page.waitForSelector('#loading-overlay.hidden', { timeout: 30000 });
|
||||
// Navigate to PCBnew via web server (apps/kicad/pcbnew.html)
|
||||
await page.goto('/kicad/pcbnew.html');
|
||||
});
|
||||
|
||||
test('should load the PCBnew interface', async ({ page }) => {
|
||||
// Check that the toolbar is visible
|
||||
await expect(page.locator('#toolbar h1')).toHaveText('KiCad PCBnew (WASM)');
|
||||
|
||||
// Check that toolbar buttons exist
|
||||
await expect(page.locator('#btn-new')).toBeVisible();
|
||||
await expect(page.locator('#btn-open')).toBeVisible();
|
||||
await expect(page.locator('#btn-save')).toBeVisible();
|
||||
await expect(page.locator('#btn-zoom-in')).toBeVisible();
|
||||
await expect(page.locator('#btn-zoom-out')).toBeVisible();
|
||||
await expect(page.locator('#btn-fit')).toBeVisible();
|
||||
test('should load PCBnew WASM module', async ({ page }) => {
|
||||
// Wait for WASM to initialize (look for canvas or status indicator)
|
||||
// This is a basic smoke test - expand as PCBnew integration matures
|
||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60000 });
|
||||
});
|
||||
|
||||
test('should have a working canvas', async ({ page }) => {
|
||||
const canvas = page.locator('#canvas');
|
||||
await expect(canvas).toBeVisible();
|
||||
test('should render without JavaScript errors', async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', err => errors.push(err.message));
|
||||
|
||||
// Check that canvas has proper dimensions
|
||||
const box = await canvas.boundingBox();
|
||||
expect(box?.width).toBeGreaterThan(0);
|
||||
expect(box?.height).toBeGreaterThan(0);
|
||||
});
|
||||
await page.goto('/kicad/pcbnew.html');
|
||||
await page.waitForTimeout(5000); // Give time for WASM to load
|
||||
|
||||
test('should track mouse coordinates', async ({ page }) => {
|
||||
const canvas = page.locator('#canvas');
|
||||
const statusCoords = page.locator('#status-coords');
|
||||
|
||||
// Move mouse over canvas
|
||||
await canvas.hover({ position: { x: 100, y: 100 } });
|
||||
|
||||
// Check that coordinates are updated
|
||||
const coordText = await statusCoords.textContent();
|
||||
expect(coordText).toMatch(/X: \d+\.\d+mm Y: \d+\.\d+mm/);
|
||||
});
|
||||
|
||||
test('should log UI interactions', async ({ page }) => {
|
||||
const logPanel = page.locator('#log-panel');
|
||||
|
||||
// Click New button
|
||||
await page.click('#btn-new');
|
||||
|
||||
// Check that action was logged
|
||||
await expect(logPanel).toContainText('New project');
|
||||
});
|
||||
|
||||
test('should have status bar with info', async ({ page }) => {
|
||||
await expect(page.locator('#status-info')).toHaveText('Ready');
|
||||
});
|
||||
|
||||
test('should show sidebar with log panel', async ({ page }) => {
|
||||
const sidebar = page.locator('#sidebar');
|
||||
const logPanel = page.locator('#log-panel');
|
||||
|
||||
await expect(sidebar).toBeVisible();
|
||||
await expect(logPanel).toBeVisible();
|
||||
});
|
||||
|
||||
// Screenshot test for visual regression
|
||||
test('should match visual snapshot', async ({ page }) => {
|
||||
await page.screenshot({
|
||||
path: path.join(__dirname, 'screenshots', 'pcbnew-initial.png'),
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PCBnew File Operations', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const htmlPath = path.join(__dirname, 'pcbnew.html');
|
||||
await page.goto(`file://${htmlPath}`);
|
||||
await page.waitForSelector('#loading-overlay.hidden', { timeout: 30000 });
|
||||
});
|
||||
|
||||
test('should have file open button that triggers file picker', async ({ page }) => {
|
||||
const openBtn = page.locator('#btn-open');
|
||||
await expect(openBtn).toBeVisible();
|
||||
|
||||
// Click should trigger file input (we can't fully test file selection in Playwright)
|
||||
// but we can verify the button is clickable
|
||||
await openBtn.click();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PCBnew WASM Module', () => {
|
||||
test('WASM module loading (placeholder)', async ({ page }) => {
|
||||
// This test will be expanded once the WASM module is built
|
||||
const htmlPath = path.join(__dirname, 'pcbnew.html');
|
||||
await page.goto(`file://${htmlPath}`);
|
||||
|
||||
// Check for expected warning when module is not built
|
||||
const logPanel = page.locator('#log-panel');
|
||||
await page.waitForSelector('#loading-overlay.hidden', { timeout: 30000 });
|
||||
await expect(logPanel).toContainText('Note: PCBnew WASM module not yet built');
|
||||
// Filter out known acceptable errors if any
|
||||
const criticalErrors = errors.filter(e => !e.includes('ResizeObserver'));
|
||||
expect(criticalErrors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
"build-wasm": "cd apps && make -f Makefile.wasm",
|
||||
"serve": "npx serve apps -p 8080 -c ../serve.json",
|
||||
"setup:kicad": "./scripts/setup-kicad-wasm.sh",
|
||||
"test:kicad": "playwright test kicad/",
|
||||
"test:kicad:headed": "playwright test kicad/ --headed"
|
||||
"test:kicad": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts",
|
||||
"test:kicad:headed": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --headed"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
|
|
|
|||
68
tests/playwright-kicad.config.ts
Normal file
68
tests/playwright-kicad.config.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const PORT_FILE = path.join(__dirname, '.test-port');
|
||||
|
||||
// Get existing port from file or find a new one
|
||||
function getOrFindPort(): number {
|
||||
try {
|
||||
const stat = fs.statSync(PORT_FILE);
|
||||
const age = Date.now() - stat.mtimeMs;
|
||||
if (age < 60000) {
|
||||
const port = parseInt(fs.readFileSync(PORT_FILE, 'utf-8').trim());
|
||||
if (port > 0 && port < 65536) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist or can't be read
|
||||
}
|
||||
|
||||
const port = findFreePort();
|
||||
fs.writeFileSync(PORT_FILE, port.toString());
|
||||
return port;
|
||||
}
|
||||
|
||||
function findFreePort(): number {
|
||||
try {
|
||||
const result = execSync(
|
||||
'python3 -c "import socket; s=socket.socket(); s.bind((\'\',0)); print(s.getsockname()[1]); s.close()"',
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
return parseInt(result.trim());
|
||||
} catch {
|
||||
return 9000 + Math.floor(Math.random() * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
const port = getOrFindPort();
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './kicad',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
timeout: 120000, // KiCad WASM needs more time to load
|
||||
|
||||
use: {
|
||||
baseURL: `http://localhost:${port}`,
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: `npx serve apps -p ${port} -c ../serve.json`,
|
||||
port: port,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue