pcbjam/tests/e2e/button-finder.spec.ts
Viktor Vaczi 802b1e871c Implement wxClipboard and add button finder utility for canvas testing
wxClipboard:
- Update wxwidgets submodule with full wxClipboard implementation
- Add clipboard async functions to ASYNCIFY_IMPORTS in Makefile.wasm
- Update clipboard tests with correct button positions
- Add clipboard permissions to playwright config
- Update WHATWORKS.md to mark wxClipboard as working

Button Finder Utility:
- Add parametric button-finder.spec.ts for scanning canvas apps
- Utility scans for clickable buttons by detecting console log responses
- Excluded from regular test runs via testIgnore
- Supports APP_URL, START_Y, END_Y, STEP environment variables
- Document usage in tests/README.md

All 91 tests pass.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 10:19:50 +01:00

258 lines
10 KiB
TypeScript

/**
* Button Finder Utility
*
* A utility for finding clickable button positions in wxWidgets WASM canvas apps.
* This is NOT a regular test - it's excluded from normal test runs via testIgnore.
*
* wxWidgets WASM renders everything to a canvas, so UI tests need specific pixel
* coordinates to click buttons. This utility scans the canvas and reports positions
* that trigger console log responses (indicating a button was clicked).
*
* Usage:
* cd tests
*
* # Scan clipboard test app
* APP_URL=/standalone/clipboard/clipboard_test.html npx playwright test button-finder --reporter=list
*
* # Scan dialog test app
* APP_URL=/standalone/dialog/dialog_test.html npx playwright test button-finder --reporter=list
*
* # Scan tree test app
* APP_URL=/standalone/tree/tree_test.html npx playwright test button-finder --reporter=list
*
* # Scan menu test app
* APP_URL=/standalone/menu/menu_test.html npx playwright test button-finder --reporter=list
*
* # Scan with custom region (faster - focus on likely button area)
* APP_URL=/standalone/dialog/dialog_test.html START_Y=150 END_Y=300 STEP=8 npx playwright test button-finder --reporter=list
*
* Environment Variables:
* APP_URL - URL path to scan (REQUIRED - no default to force explicit choice)
* STEP - Pixel step size (default: 10, smaller = more accurate but slower)
* START_X - X start coordinate (default: 0)
* END_X - X end coordinate (default: canvas width)
* START_Y - Y start coordinate (default: 0)
* END_Y - Y end coordinate (default: canvas height)
*
* Available Test Apps:
* /standalone/clipboard/clipboard_test.html - Copy, Paste, Check, Clear buttons
* /standalone/dialog/dialog_test.html - Info, Yes/No, Error, Custom dialog buttons
* /standalone/tree/tree_test.html - Expand All, Collapse All, etc.
* /standalone/menu/menu_test.html - Menu bar testing
* /standalone/grid/grid_test.html - Grid controls
* /standalone/aui/aui_test.html - AUI panel controls
* /standalone/toolbar/toolbar_test.html - Toolbar buttons
* /standalone/timer/timer_test.html - Timer controls
* /standalone/filedialog/filedialog_test.html - File dialog buttons
* /standalone/layout/layout_test.html - Layout controls
*
* Output:
* - Console output with button positions and labels
* - Generated test code snippets
* - JSON results at test-results/button-finder-results.json
*/
import { test, expect } from '@playwright/test';
import { tryLoadApp } from './utils/fixtures';
// Configuration from environment - APP_URL is required
const APP_URL = process.env.APP_URL;
const STEP = parseInt(process.env.STEP || '10');
const START_Y = process.env.START_Y ? parseInt(process.env.START_Y) : undefined;
const END_Y = process.env.END_Y ? parseInt(process.env.END_Y) : undefined;
const START_X = process.env.START_X ? parseInt(process.env.START_X) : undefined;
const END_X = process.env.END_X ? parseInt(process.env.END_X) : undefined;
test.describe('Button Finder Utility', () => {
// Long timeout for scanning
test.setTimeout(300000);
test('Scan for buttons', async ({ page }) => {
// Require APP_URL to be specified
if (!APP_URL) {
console.error('\n' + '='.repeat(70));
console.error('ERROR: APP_URL environment variable is required');
console.error('='.repeat(70));
console.error('\nUsage examples:');
console.error(' APP_URL=/standalone/clipboard/clipboard_test.html npx playwright test button-finder --reporter=list');
console.error(' APP_URL=/standalone/dialog/dialog_test.html npx playwright test button-finder --reporter=list');
console.error(' APP_URL=/standalone/tree/tree_test.html START_Y=100 END_Y=300 npx playwright test button-finder --reporter=list');
console.error('\nAvailable apps:');
console.error(' /standalone/clipboard/clipboard_test.html');
console.error(' /standalone/dialog/dialog_test.html');
console.error(' /standalone/tree/tree_test.html');
console.error(' /standalone/menu/menu_test.html');
console.error(' /standalone/grid/grid_test.html');
console.error(' /standalone/aui/aui_test.html');
console.error(' /standalone/toolbar/toolbar_test.html');
console.error(' /standalone/timer/timer_test.html');
console.error(' /standalone/filedialog/filedialog_test.html');
console.error(' /standalone/layout/layout_test.html');
console.error('');
test.skip();
return;
}
console.log(`\n${'='.repeat(70)}`);
console.log(`Button Finder - Scanning: ${APP_URL}`);
console.log(`Step size: ${STEP}px`);
console.log(`${'='.repeat(70)}\n`);
// Navigate and wait for app
await page.goto(APP_URL);
const loaded = await tryLoadApp(page);
expect(loaded, 'App should load').toBe(true);
// Get canvas bounds
const canvas = page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) {
console.log('ERROR: Canvas not found');
return;
}
const scanStartX = START_X ?? 0;
const scanEndX = END_X ?? box.width;
const scanStartY = START_Y ?? 0;
const scanEndY = END_Y ?? box.height;
console.log(`Canvas: ${box.width}x${box.height} at (${box.x}, ${box.y})`);
console.log(`Scan region: X[${scanStartX}-${scanEndX}] Y[${scanStartY}-${scanEndY}]`);
console.log(`Estimated clicks: ${Math.ceil((scanEndX - scanStartX) / STEP) * Math.ceil((scanEndY - scanStartY) / STEP)}\n`);
// Take before screenshot
await page.screenshot({ path: 'test-results/button-finder-before.png', fullPage: true });
// Collect console logs
const logs: string[] = [];
page.on('console', msg => logs.push(msg.text()));
// Track found buttons
const buttons: Array<{x: number, y: number, label: string, log: string}> = [];
const seenLogs = new Set<string>();
// Keywords that indicate a button click response
const buttonKeywords = [
'Attempting', 'SUCCESS', 'ERROR', 'WARNING',
'clicked', 'Clicked', 'EVT_BUTTON', 'EVT_MENU',
'OnButton', 'OnClick', 'pressed', 'Pressed',
'Copy', 'Paste', 'Clear', 'Check', 'Open', 'Close',
'selected', 'Selected', 'expand', 'collapse',
'Expand', 'Collapse', 'Add', 'Remove', 'Delete',
'Start', 'Stop', 'Reset', 'Save', 'Load'
];
// Label keywords for identification
const labelKeywords = [
'Copy', 'Paste', 'Clear', 'Check', 'Open', 'Close',
'OK', 'Cancel', 'Yes', 'No', 'Expand', 'Collapse',
'Add', 'Remove', 'Delete', 'Start', 'Stop', 'Reset',
'Save', 'Load', 'Info', 'Error', 'Warning', 'Custom'
];
// Scan the canvas
let lastProgressY = -100;
for (let y = scanStartY; y < scanEndY; y += STEP) {
// Progress indicator every 50px
if (y - lastProgressY >= 50) {
console.log(`Scanning row ${y}/${scanEndY}...`);
lastProgressY = y;
}
for (let x = scanStartX; x < scanEndX; x += STEP) {
const logCountBefore = logs.length;
// Click at this position
await page.mouse.click(box.x + x, box.y + y);
// Small wait to allow response
await page.waitForTimeout(20);
// Check for new logs
if (logs.length > logCountBefore) {
const newLogs = logs.slice(logCountBefore);
for (const log of newLogs) {
// Skip noise
if (log.includes('favicon') || log.includes('DevTools')) continue;
// Check if this looks like a button response
const isButtonLog = buttonKeywords.some(kw => log.includes(kw));
if (isButtonLog && !seenLogs.has(log)) {
seenLogs.add(log);
// Try to extract a label
let label = 'Button';
for (const kw of labelKeywords) {
if (log.toLowerCase().includes(kw.toLowerCase())) {
label = kw;
break;
}
}
buttons.push({
x,
y,
label,
log: log.substring(0, 80)
});
console.log(` FOUND: (${x}, ${y}) ${label} - "${log.substring(0, 60)}"`);
}
}
}
}
}
// Take after screenshot
await page.screenshot({ path: 'test-results/button-finder-after.png', fullPage: true });
// Output results
console.log(`\n${'='.repeat(70)}`);
console.log(`RESULTS: Found ${buttons.length} buttons`);
console.log(`${'='.repeat(70)}\n`);
if (buttons.length > 0) {
console.log('Button positions (relative to canvas):');
console.log('');
for (const btn of buttons) {
console.log(` ${btn.label.padEnd(12)} at (${String(btn.x).padStart(3)}, ${String(btn.y).padStart(3)})`);
console.log(` Log: ${btn.log}`);
console.log('');
}
console.log('\nGenerated test code:');
console.log('```typescript');
console.log('const box = await getCanvasBox(page);');
console.log('');
for (const btn of buttons) {
console.log(`// ${btn.label} button`);
console.log(`await page.mouse.click(box.x + ${btn.x}, box.y + ${btn.y});`);
console.log(`await page.waitForTimeout(500);`);
console.log('');
}
console.log('```');
} else {
console.log('No buttons found.');
console.log('\nPossible reasons:');
console.log(' - Buttons are outside the scan region (try adjusting START_Y/END_Y)');
console.log(' - Button clicks dont produce recognizable logs');
console.log(' - Step size is too large (try STEP=5)');
console.log('\nCaptured logs:');
logs.slice(0, 30).forEach(log => console.log(` ${log.substring(0, 80)}`));
}
// Write results to a JSON file for programmatic use
const results = {
app: APP_URL,
canvas: { x: box.x, y: box.y, width: box.width, height: box.height },
scanRegion: { startX: scanStartX, endX: scanEndX, startY: scanStartY, endY: scanEndY },
step: STEP,
buttons: buttons
};
const fs = await import('fs');
fs.writeFileSync('test-results/button-finder-results.json', JSON.stringify(results, null, 2));
console.log('\nResults saved to: test-results/button-finder-results.json');
});
});