Add element registry for semantic E2E test automation
Replace hardcoded pixel coordinates with semantic element lookups in tests. The element registry (added to wxWidgets) tracks all wxWindow instances, enabling tests to find buttons by label text instead of pixel positions. Changes: - Add element-tracker.ts with clickByLabel, findByLabel, findByType, etc. - Migrate clipboard, dialog, timer, filedialog, logerror tests to use registry - Update fixtures.ts to export element-tracker utilities - Update README with element registry documentation - Update wxwidgets submodule with registry implementation 🤖 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
9ccb07ad81
commit
0a093b918e
9 changed files with 431 additions and 104 deletions
|
|
@ -35,6 +35,7 @@ tests/
|
|||
├── e2e/ # Playwright test specs
|
||||
│ ├── utils/ # Shared test utilities
|
||||
│ │ ├── fixtures.ts # Playwright fixtures with auto-logging
|
||||
│ │ ├── element-tracker.ts # Element registry utilities (clickByLabel, etc.)
|
||||
│ │ └── test-utils.ts # Logging and helper functions
|
||||
│ ├── menu.spec.ts # wxMenuBar tests
|
||||
│ ├── timer.spec.ts # wxTimer tests
|
||||
|
|
@ -165,9 +166,78 @@ $LLVM_DIR/llvm-dwarfdump --debug-info apps/standalone/grid/grid_test.wasm
|
|||
$LLVM_DIR/llvm-objdump -d grid_test.wasm | head -200
|
||||
```
|
||||
|
||||
## Button Finder Utility
|
||||
## Element Registry (Recommended)
|
||||
|
||||
The wxWidgets WASM apps render to a canvas, so UI tests need to click at specific pixel coordinates. The button-finder utility scans a test app to find clickable button positions.
|
||||
The wxWidgets WASM port includes an element registry that tracks all wxWindow instances with their positions, labels, and types. This enables tests to find UI elements by semantic identifiers instead of hardcoded pixel coordinates.
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { waitForRegistry, clickByLabel, findByLabel, findByType } from './utils/fixtures';
|
||||
|
||||
// Wait for registry to be available
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Click buttons by label text
|
||||
await clickByLabel(page, 'Copy to Clipboard');
|
||||
await clickByLabel(page, 'Save File...');
|
||||
|
||||
// Find elements for inspection
|
||||
const button = await findByLabel(page, 'OK');
|
||||
if (button) {
|
||||
console.log(`Button at (${button.centerX}, ${button.centerY})`);
|
||||
}
|
||||
|
||||
// Find all elements of a type
|
||||
const buttons = await findByType(page, 'wxButton');
|
||||
```
|
||||
|
||||
### Available Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `waitForRegistry(page)` | Wait for element registry to initialize |
|
||||
| `findByLabel(page, label, options?)` | Find element by label text |
|
||||
| `findByName(page, name, options?)` | Find element by wxWindow name |
|
||||
| `findByType(page, typeName, options?)` | Find all elements of a type (e.g., 'wxButton') |
|
||||
| `clickByLabel(page, label, options?)` | Click element by label |
|
||||
| `clickByName(page, name, options?)` | Click element by name |
|
||||
|
||||
### Options
|
||||
|
||||
```typescript
|
||||
interface FindOptions {
|
||||
visible?: boolean; // Filter by visibility (default: true)
|
||||
enabled?: boolean; // Filter by enabled state
|
||||
exact?: boolean; // Exact label match (default: substring)
|
||||
type?: string; // Filter by type name
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
Use the element registry for tests that click on **wxButton** and other wxWindow-based controls. The registry tracks:
|
||||
- wxButton, wxTextCtrl, wxStaticText, wxPanel, wxFrame, etc.
|
||||
|
||||
**Not trackable** (use pixel coordinates instead):
|
||||
- wxToolBar tool items (rendered by toolbar)
|
||||
- wxMenuBar menu items (rendered by menu system)
|
||||
- wxAuiManager panel controls (title bars, close buttons)
|
||||
- wxGrid cells (rendered by grid)
|
||||
- wxSplitterWindow sash (rendered by splitter)
|
||||
|
||||
### Migrated Tests
|
||||
|
||||
These tests use the element registry:
|
||||
- `clipboard.spec.ts` - Copy, Paste, Check, Clear buttons
|
||||
- `dialog.spec.ts` - Info, Yes/No, Error, Custom dialog buttons
|
||||
- `timer.spec.ts` - Start, Stop, Reset buttons
|
||||
- `filedialog.spec.ts` - Open, Save, Open Multiple buttons
|
||||
- `logerror.spec.ts` - Trigger Error, Flush Log buttons
|
||||
|
||||
## Button Finder Utility (Legacy)
|
||||
|
||||
For elements not trackable by the registry, the button-finder utility scans a test app to find clickable positions by pixel coordinates.
|
||||
|
||||
**Note:** This utility is excluded from regular test runs (`npm test`). Use the dedicated config to run it.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
// wxClipboard Tests - Clipboard operations for KiCad copy/paste
|
||||
// Button positions found using button-finder utility
|
||||
import { test, expect, MAIN_CANVAS, tryLoadApp, getCanvasBox } from './utils/fixtures';
|
||||
|
||||
// Button positions (relative to canvas) - found using button-finder.spec.ts
|
||||
const BUTTONS = {
|
||||
COPY: { x: 352, y: 196 },
|
||||
PASTE: { x: 600, y: 196 },
|
||||
CHECK: { x: 700, y: 196 },
|
||||
CLEAR: { x: 808, y: 196 },
|
||||
};
|
||||
// Uses element registry for semantic element identification
|
||||
import { test, expect, tryLoadApp, waitForRegistry, clickByLabel } from './utils/fixtures';
|
||||
|
||||
test.describe('wxClipboard Tests', () => {
|
||||
|
||||
|
|
@ -34,10 +26,10 @@ test.describe('wxClipboard Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Click "Copy to Clipboard" button
|
||||
await page.mouse.click(box.x + BUTTONS.COPY.x, box.y + BUTTONS.COPY.y);
|
||||
await clickByLabel(page, 'Copy to Clipboard');
|
||||
await page.waitForTimeout(2500); // Wait for async clipboard operation + timeout
|
||||
|
||||
await page.screenshot({ path: 'test-results/clipboard-02-copy-clicked.png', fullPage: true });
|
||||
|
|
@ -62,14 +54,14 @@ test.describe('wxClipboard Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// First copy something
|
||||
await page.mouse.click(box.x + BUTTONS.COPY.x, box.y + BUTTONS.COPY.y);
|
||||
await clickByLabel(page, 'Copy to Clipboard');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
// Click "Paste from Clipboard" button
|
||||
await page.mouse.click(box.x + BUTTONS.PASTE.x, box.y + BUTTONS.PASTE.y);
|
||||
await clickByLabel(page, 'Paste from Clipboard');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/clipboard-03-paste-clicked.png', fullPage: true });
|
||||
|
|
@ -97,14 +89,14 @@ test.describe('wxClipboard Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// First copy something to ensure clipboard has content
|
||||
await page.mouse.click(box.x + BUTTONS.COPY.x, box.y + BUTTONS.COPY.y);
|
||||
await clickByLabel(page, 'Copy to Clipboard');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
// Click "Check Clipboard" button
|
||||
await page.mouse.click(box.x + BUTTONS.CHECK.x, box.y + BUTTONS.CHECK.y);
|
||||
await clickByLabel(page, 'Check Clipboard');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/clipboard-04-check-clicked.png', fullPage: true });
|
||||
|
|
@ -125,14 +117,14 @@ test.describe('wxClipboard Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// First copy something
|
||||
await page.mouse.click(box.x + BUTTONS.COPY.x, box.y + BUTTONS.COPY.y);
|
||||
await clickByLabel(page, 'Copy to Clipboard');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
// Click "Clear Clipboard" button
|
||||
await page.mouse.click(box.x + BUTTONS.CLEAR.x, box.y + BUTTONS.CLEAR.y);
|
||||
await clickByLabel(page, 'Clear Clipboard');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/clipboard-05-clear-clicked.png', fullPage: true });
|
||||
|
|
@ -156,10 +148,10 @@ test.describe('wxClipboard Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// 1. Copy
|
||||
await page.mouse.click(box.x + BUTTONS.COPY.x, box.y + BUTTONS.COPY.y);
|
||||
await clickByLabel(page, 'Copy to Clipboard');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
const hasCopyLog = testLogger.consoleLogs.some(l =>
|
||||
|
|
@ -168,7 +160,7 @@ test.describe('wxClipboard Tests', () => {
|
|||
expect(hasCopyLog, 'Copy should log activity').toBe(true);
|
||||
|
||||
// 2. Check
|
||||
await page.mouse.click(box.x + BUTTONS.CHECK.x, box.y + BUTTONS.CHECK.y);
|
||||
await clickByLabel(page, 'Check Clipboard');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
const hasCheckResult = testLogger.consoleLogs.some(l =>
|
||||
|
|
@ -177,7 +169,7 @@ test.describe('wxClipboard Tests', () => {
|
|||
expect(hasCheckResult, 'Check should report clipboard').toBe(true);
|
||||
|
||||
// 3. Paste
|
||||
await page.mouse.click(box.x + BUTTONS.PASTE.x, box.y + BUTTONS.PASTE.y);
|
||||
await clickByLabel(page, 'Paste from Clipboard');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
const hasPasteLog = testLogger.consoleLogs.some(l =>
|
||||
|
|
@ -186,7 +178,7 @@ test.describe('wxClipboard Tests', () => {
|
|||
expect(hasPasteLog, 'Paste should log activity').toBe(true);
|
||||
|
||||
// 4. Clear
|
||||
await page.mouse.click(box.x + BUTTONS.CLEAR.x, box.y + BUTTONS.CLEAR.y);
|
||||
await clickByLabel(page, 'Clear Clipboard');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
const hasClearLog = testLogger.consoleLogs.some(l =>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { test, expect, MAIN_CANVAS, tryLoadApp, getCanvasBox } from './utils/fixtures';
|
||||
// wxDialog/wxMessageBox Tests - Modal dialogs for KiCad confirmations, errors, properties
|
||||
// Uses element registry for semantic element identification
|
||||
import { test, expect, tryLoadApp, waitForRegistry, clickByLabel } from './utils/fixtures';
|
||||
|
||||
test.describe('wxDialog/wxMessageBox Tests', () => {
|
||||
test('Dialog test app loads successfully', async ({ page, testLogger }) => {
|
||||
|
|
@ -23,11 +25,10 @@ test.describe('wxDialog/wxMessageBox Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Info Dialog button is first in the wxMessageBox row
|
||||
const centerX = box.width / 2;
|
||||
await page.mouse.click(box.x + centerX - 110, box.y + 115);
|
||||
// Click "Info Dialog" button
|
||||
await clickByLabel(page, 'Info Dialog');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/dialog-02-info-clicked.png', fullPage: true });
|
||||
|
|
@ -47,11 +48,10 @@ test.describe('wxDialog/wxMessageBox Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Yes/No Dialog button is second (center) in the wxMessageBox row
|
||||
const centerX = box.width / 2;
|
||||
await page.mouse.click(box.x + centerX, box.y + 115);
|
||||
// Click "Yes/No Dialog" button
|
||||
await clickByLabel(page, 'Yes/No Dialog');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/dialog-03-yesno-clicked.png', fullPage: true });
|
||||
|
|
@ -70,11 +70,10 @@ test.describe('wxDialog/wxMessageBox Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Error Dialog button is third (rightmost) in the wxMessageBox row
|
||||
const centerX = box.width / 2;
|
||||
await page.mouse.click(box.x + centerX + 110, box.y + 115);
|
||||
// Click "Error Dialog" button
|
||||
await clickByLabel(page, 'Error Dialog');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/dialog-04-error-clicked.png', fullPage: true });
|
||||
|
|
@ -93,11 +92,10 @@ test.describe('wxDialog/wxMessageBox Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Custom Dialog button is first in the wxDialog row (row 2)
|
||||
const centerX = box.width / 2;
|
||||
await page.mouse.click(box.x + centerX - 60, box.y + 175);
|
||||
// Click "Custom Dialog" button
|
||||
await clickByLabel(page, 'Custom Dialog');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/dialog-05-custom-clicked.png', fullPage: true });
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// wxFileDialog Tests - File dialogs for KiCad open/save operations
|
||||
import { test, expect, MAIN_CANVAS, tryLoadApp, getCanvasBox } from './utils/fixtures';
|
||||
// Uses element registry for semantic element identification
|
||||
import { test, expect, tryLoadApp, waitForRegistry, clickByLabel } from './utils/fixtures';
|
||||
|
||||
test.describe('wxFileDialog Tests', () => {
|
||||
|
||||
|
|
@ -23,10 +24,10 @@ test.describe('wxFileDialog Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Click "Open File..." button
|
||||
await page.mouse.click(box.x + 100, box.y + 150);
|
||||
await clickByLabel(page, 'Open File...');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/filedialog-02-open-clicked.png', fullPage: true });
|
||||
|
|
@ -42,10 +43,10 @@ test.describe('wxFileDialog Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Click "Save File..." button
|
||||
await page.mouse.click(box.x + 220, box.y + 150);
|
||||
await clickByLabel(page, 'Save File...');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/filedialog-03-save-clicked.png', fullPage: true });
|
||||
|
|
@ -61,10 +62,10 @@ test.describe('wxFileDialog Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Click "Open Multiple..." button
|
||||
await page.mouse.click(box.x + 350, box.y + 150);
|
||||
await clickByLabel(page, 'Open Multiple...');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/filedialog-04-multiple-clicked.png', fullPage: true });
|
||||
|
|
@ -80,14 +81,14 @@ test.describe('wxFileDialog Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Try all three buttons
|
||||
await page.mouse.click(box.x + 100, box.y + 150);
|
||||
await clickByLabel(page, 'Open File...');
|
||||
await page.waitForTimeout(300);
|
||||
await page.mouse.click(box.x + 220, box.y + 150);
|
||||
await clickByLabel(page, 'Save File...');
|
||||
await page.waitForTimeout(300);
|
||||
await page.mouse.click(box.x + 350, box.y + 150);
|
||||
await clickByLabel(page, 'Open Multiple...');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await page.screenshot({ path: 'test-results/filedialog-05-all-buttons.png', fullPage: true });
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
import { test, expect } from './utils/fixtures';
|
||||
import { getCanvasBox } from './utils/test-utils';
|
||||
|
||||
// Button positions found via button-finder
|
||||
const BUTTONS = {
|
||||
triggerError: { x: 24, y: 138 },
|
||||
triggerMultiple: { x: 24, y: 226 },
|
||||
mixedLevels: { x: 24, y: 314 },
|
||||
flushLog: { x: 24, y: 398 },
|
||||
clearLog: { x: 216, y: 398 }
|
||||
};
|
||||
// wxLogError Dialog Tests - Tests wxLogDialog error handling for KiCad
|
||||
// Uses element registry for semantic element identification
|
||||
import { test, expect, waitForRegistry, clickByLabel } from './utils/fixtures';
|
||||
|
||||
test.describe('wxLogError Dialog Tests', () => {
|
||||
|
||||
|
|
@ -44,10 +36,7 @@ test.describe('wxLogError Dialog Tests', () => {
|
|||
return (document.querySelector('#canvas') as HTMLElement)?.style.display === 'block';
|
||||
}, { timeout: 30000 });
|
||||
|
||||
// Wait for UI to be ready
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Take screenshot before clicking
|
||||
await page.screenshot({
|
||||
|
|
@ -56,11 +45,11 @@ test.describe('wxLogError Dialog Tests', () => {
|
|||
});
|
||||
|
||||
// Click "Trigger Error" button
|
||||
await page.mouse.click(box.x + BUTTONS.triggerError.x, box.y + BUTTONS.triggerError.y);
|
||||
await clickByLabel(page, 'Trigger Error');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Click "Flush Log" to show the dialog
|
||||
await page.mouse.click(box.x + BUTTONS.flushLog.x, box.y + BUTTONS.flushLog.y);
|
||||
// Click "Flush Log (Show Dialog)" to show the dialog
|
||||
await clickByLabel(page, 'Flush Log (Show Dialog)');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Take screenshot showing the error dialog
|
||||
|
|
@ -89,17 +78,14 @@ test.describe('wxLogError Dialog Tests', () => {
|
|||
return (document.querySelector('#canvas') as HTMLElement)?.style.display === 'block';
|
||||
}, { timeout: 30000 });
|
||||
|
||||
// Wait for UI to be ready
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Click "Trigger Multiple" button
|
||||
await page.mouse.click(box.x + BUTTONS.triggerMultiple.x, box.y + BUTTONS.triggerMultiple.y);
|
||||
await clickByLabel(page, 'Trigger Multiple');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Click "Flush Log" to show the dialog with Details dropdown
|
||||
await page.mouse.click(box.x + BUTTONS.flushLog.x, box.y + BUTTONS.flushLog.y);
|
||||
// Click "Flush Log (Show Dialog)" to show the dialog with Details dropdown
|
||||
await clickByLabel(page, 'Flush Log (Show Dialog)');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Take screenshot showing the dialog with Details
|
||||
|
|
@ -125,12 +111,10 @@ test.describe('wxLogError Dialog Tests', () => {
|
|||
return (document.querySelector('#canvas') as HTMLElement)?.style.display === 'block';
|
||||
}, { timeout: 30000 });
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Click "Mixed Levels" button to log error, warning, and message
|
||||
await page.mouse.click(box.x + BUTTONS.mixedLevels.x, box.y + BUTTONS.mixedLevels.y);
|
||||
await clickByLabel(page, 'Mixed Levels');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Take screenshot
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { test, expect, tryLoadApp, getCanvasBox } from './utils/fixtures';
|
||||
// wxTimer Tests - Timer functionality for KiCad animations, auto-save, periodic updates
|
||||
// Uses element registry for semantic element identification
|
||||
import { test, expect, tryLoadApp, waitForRegistry, clickByLabel, findByLabel } from './utils/fixtures';
|
||||
|
||||
test.describe('wxTimer Tests', () => {
|
||||
|
||||
|
|
@ -20,11 +22,14 @@ test.describe('wxTimer Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
const centerX = box.width / 2;
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Click Start button for slow timer (left of center in button row)
|
||||
await page.mouse.click(box.x + centerX - 40, box.y + 125);
|
||||
// Click Start button for slow timer
|
||||
// Note: There are two "Start" buttons, we need the first one in "Slow Timer" section
|
||||
const startButton = await findByLabel(page, 'Start', { exact: true });
|
||||
if (startButton) {
|
||||
await page.mouse.click(startButton.centerX, startButton.centerY);
|
||||
}
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/timer-02-started.png', fullPage: true });
|
||||
|
|
@ -38,8 +43,11 @@ test.describe('wxTimer Tests', () => {
|
|||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: 'test-results/timer-03-ticked.png', fullPage: true });
|
||||
|
||||
// Click Stop button (right of center in button row)
|
||||
await page.mouse.click(box.x + centerX + 40, box.y + 125);
|
||||
// Click Stop button for slow timer
|
||||
const stopButton = await findByLabel(page, 'Stop', { exact: true });
|
||||
if (stopButton) {
|
||||
await page.mouse.click(stopButton.centerX, stopButton.centerY);
|
||||
}
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/timer-04-stopped.png', fullPage: true });
|
||||
|
|
@ -58,11 +66,10 @@ test.describe('wxTimer Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
const centerX = box.width / 2;
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Click Start Fast button (left of center in fast timer row)
|
||||
await page.mouse.click(box.x + centerX - 40, box.y + 265);
|
||||
// Click "Start Fast" button
|
||||
await clickByLabel(page, 'Start Fast');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/timer-05-fast-started.png', fullPage: true });
|
||||
|
|
@ -76,8 +83,8 @@ test.describe('wxTimer Tests', () => {
|
|||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: 'test-results/timer-06-fast-running.png', fullPage: true });
|
||||
|
||||
// Click Stop Fast button (right of center in fast timer row)
|
||||
await page.mouse.click(box.x + centerX + 40, box.y + 265);
|
||||
// Click "Stop Fast" button
|
||||
await clickByLabel(page, 'Stop Fast');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/timer-07-fast-stopped.png', fullPage: true });
|
||||
|
|
@ -96,15 +103,17 @@ test.describe('wxTimer Tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const box = await getCanvasBox(page);
|
||||
const centerX = box.width / 2;
|
||||
await waitForRegistry(page);
|
||||
|
||||
// Start slow timer briefly
|
||||
await page.mouse.click(box.x + centerX - 40, box.y + 125);
|
||||
const startButton = await findByLabel(page, 'Start', { exact: true });
|
||||
if (startButton) {
|
||||
await page.mouse.click(startButton.centerX, startButton.centerY);
|
||||
}
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Click Reset All Counters (centered button, below fast timer section)
|
||||
await page.mouse.click(box.x + centerX, box.y + 390);
|
||||
// Click "Reset All Counters" button
|
||||
await clickByLabel(page, 'Reset All Counters');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'test-results/timer-08-reset.png', fullPage: true });
|
||||
|
|
|
|||
270
tests/e2e/utils/element-tracker.ts
Normal file
270
tests/e2e/utils/element-tracker.ts
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
import { Page } from '@playwright/test';
|
||||
|
||||
export interface WxElement {
|
||||
id: string;
|
||||
label: string;
|
||||
name: string;
|
||||
typeName: string;
|
||||
screenX: number;
|
||||
screenY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
parentId: string | null;
|
||||
visible: boolean;
|
||||
enabled: boolean;
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
export interface FindOptions {
|
||||
visible?: boolean;
|
||||
enabled?: boolean;
|
||||
exact?: boolean;
|
||||
type?: string;
|
||||
parent?: string;
|
||||
}
|
||||
|
||||
export interface FindFilter extends FindOptions {
|
||||
label?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface RegistryStats {
|
||||
total: number;
|
||||
byType: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface WxElementRegistry {
|
||||
elements: Map<string, WxElement>;
|
||||
version: number;
|
||||
register(id: string, info: WxElement): void;
|
||||
update(id: string, updates: Partial<WxElement>): void;
|
||||
unregister(id: string): void;
|
||||
findByLabel(label: string, options?: FindOptions): WxElement[];
|
||||
findByName(name: string, options?: FindOptions): WxElement[];
|
||||
findByType(typeName: string, options?: FindOptions): WxElement[];
|
||||
findAll(filter?: FindFilter): WxElement[];
|
||||
getElement(id: string): WxElement | null;
|
||||
dump(): void;
|
||||
getStats(): RegistryStats;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
wxElementRegistry?: WxElementRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for element registry to be available
|
||||
*/
|
||||
export async function waitForRegistry(page: Page, timeout = 5000): Promise<boolean> {
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
() => typeof window.wxElementRegistry !== 'undefined',
|
||||
{ timeout }
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find element by label text
|
||||
*/
|
||||
export async function findByLabel(
|
||||
page: Page,
|
||||
label: string,
|
||||
options: FindOptions = {}
|
||||
): Promise<WxElement | null> {
|
||||
const elements = await page.evaluate(
|
||||
([label, opts]: [string, FindOptions]) => {
|
||||
const registry = window.wxElementRegistry;
|
||||
if (!registry) return [];
|
||||
return registry.findByLabel(label, opts);
|
||||
},
|
||||
[label, options] as [string, FindOptions]
|
||||
);
|
||||
return elements.length > 0 ? elements[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all elements by label text
|
||||
*/
|
||||
export async function findAllByLabel(
|
||||
page: Page,
|
||||
label: string,
|
||||
options: FindOptions = {}
|
||||
): Promise<WxElement[]> {
|
||||
return page.evaluate(
|
||||
([label, opts]: [string, FindOptions]) => {
|
||||
const registry = window.wxElementRegistry;
|
||||
if (!registry) return [];
|
||||
return registry.findByLabel(label, opts);
|
||||
},
|
||||
[label, options] as [string, FindOptions]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find element by name
|
||||
*/
|
||||
export async function findByName(
|
||||
page: Page,
|
||||
name: string,
|
||||
options: FindOptions = {}
|
||||
): Promise<WxElement | null> {
|
||||
const elements = await page.evaluate(
|
||||
([name, opts]: [string, FindOptions]) => {
|
||||
const registry = window.wxElementRegistry;
|
||||
if (!registry) return [];
|
||||
return registry.findByName(name, opts);
|
||||
},
|
||||
[name, options] as [string, FindOptions]
|
||||
);
|
||||
return elements.length > 0 ? elements[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find elements by type name (e.g., "wxButton", "wxTextCtrl")
|
||||
*/
|
||||
export async function findByType(
|
||||
page: Page,
|
||||
typeName: string,
|
||||
options: FindOptions = {}
|
||||
): Promise<WxElement[]> {
|
||||
return page.evaluate(
|
||||
([typeName, opts]: [string, FindOptions]) => {
|
||||
const registry = window.wxElementRegistry;
|
||||
if (!registry) return [];
|
||||
return registry.findByType(typeName, opts);
|
||||
},
|
||||
[typeName, options] as [string, FindOptions]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all elements matching filter
|
||||
*/
|
||||
export async function findAll(
|
||||
page: Page,
|
||||
filter: FindFilter = {}
|
||||
): Promise<WxElement[]> {
|
||||
return page.evaluate(
|
||||
(filter: FindFilter) => {
|
||||
const registry = window.wxElementRegistry;
|
||||
if (!registry) return [];
|
||||
return registry.findAll(filter);
|
||||
},
|
||||
filter
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on an element by label
|
||||
*/
|
||||
export async function clickByLabel(
|
||||
page: Page,
|
||||
label: string,
|
||||
options: FindOptions = {}
|
||||
): Promise<boolean> {
|
||||
const element = await findByLabel(page, label, options);
|
||||
if (!element) {
|
||||
console.warn(`Element with label "${label}" not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
await page.mouse.click(element.centerX, element.centerY);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on an element by name
|
||||
*/
|
||||
export async function clickByName(
|
||||
page: Page,
|
||||
name: string,
|
||||
options: FindOptions = {}
|
||||
): Promise<boolean> {
|
||||
const element = await findByName(page, name, options);
|
||||
if (!element) {
|
||||
console.warn(`Element with name "${name}" not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
await page.mouse.click(element.centerX, element.centerY);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element position for manual clicking (returns screen coords)
|
||||
*/
|
||||
export async function getElementPosition(
|
||||
page: Page,
|
||||
labelOrName: string,
|
||||
options: FindOptions = {}
|
||||
): Promise<{ x: number; y: number } | null> {
|
||||
// Try label first, then name
|
||||
let element = await findByLabel(page, labelOrName, options);
|
||||
if (!element) {
|
||||
element = await findByName(page, labelOrName, options);
|
||||
}
|
||||
if (!element) return null;
|
||||
|
||||
return { x: element.centerX, y: element.centerY };
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump all elements to console (for debugging)
|
||||
*/
|
||||
export async function dumpElements(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
const registry = window.wxElementRegistry;
|
||||
if (registry) {
|
||||
registry.dump();
|
||||
} else {
|
||||
console.log('[wxElementRegistry] Not initialized');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registry statistics
|
||||
*/
|
||||
export async function getRegistryStats(page: Page): Promise<RegistryStats | null> {
|
||||
return page.evaluate(() => {
|
||||
const registry = window.wxElementRegistry;
|
||||
if (!registry) return null;
|
||||
return registry.getStats();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an element to appear by label
|
||||
*/
|
||||
export async function waitForElement(
|
||||
page: Page,
|
||||
label: string,
|
||||
options: FindOptions & { timeout?: number } = {}
|
||||
): Promise<WxElement | null> {
|
||||
const timeout = options.timeout || 5000;
|
||||
const { timeout: _, ...findOptions } = options;
|
||||
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
([label, opts]: [string, FindOptions]) => {
|
||||
const registry = window.wxElementRegistry;
|
||||
if (!registry) return false;
|
||||
const elements = registry.findByLabel(label, opts);
|
||||
return elements.length > 0;
|
||||
},
|
||||
[label, findOptions] as [string, FindOptions],
|
||||
{ timeout }
|
||||
);
|
||||
return findByLabel(page, label, findOptions);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -24,3 +24,6 @@ export const test = base.extend<{
|
|||
|
||||
export { expect } from '@playwright/test';
|
||||
export { MAIN_CANVAS, waitForApp, tryLoadApp, getCanvasBox };
|
||||
|
||||
// Element tracking utilities for semantic element identification
|
||||
export * from './element-tracker';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit f17108fa84ce63a8936067d970e74fd37319ed7c
|
||||
Subproject commit f2f601b5aa8b60fcea1da0e1157e70ad96600fe0
|
||||
Loading…
Reference in a new issue