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:
Viktor Vaczi 2025-12-29 12:11:33 +01:00
commit 0a093b918e
9 changed files with 431 additions and 104 deletions

View file

@ -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.

View file

@ -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 =>

View file

@ -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 });

View file

@ -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 });

View file

@ -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

View file

@ -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 });

View 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;
}
}

View file

@ -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