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
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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue