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; } export interface WxRenderedElement { id: string; parentId: string; elementType: 'tool' | 'menuitem' | 'sash' | 'auipart' | 'tab' | 'gridcell' | 'listitem' | 'datecell' | 'treeitem' | 'dataviewitem' | 'proprow' | 'listboxitem' | 'spinbutton' | 'slider' | 'slidertrack' | 'textctrl' | 'combobutton' | 'combotextarea' | 'searchctrl' | 'searchbutton' | 'columnheader' | 'styledtext'; subType: string; label: string; tooltip: string; screenX: number; screenY: number; width: number; height: number; centerX: number; centerY: number; enabled: boolean; index: number; lastUpdated: number; } export interface RenderedFindOptions { enabled?: boolean; elementType?: string; subType?: string; parentId?: string; exact?: boolean; } export interface WxElementRegistry { elements: Map; version: number; register(id: string, info: WxElement): void; update(id: string, updates: Partial): 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; // Rendered elements support renderedElements?: Map; renderedVersion?: number; findRenderedByLabel?(label: string, options?: RenderedFindOptions): WxRenderedElement[]; findRenderedByType?(elementType: string, options?: RenderedFindOptions): WxRenderedElement[]; findRenderedByParent?(parentId: string, options?: RenderedFindOptions): WxRenderedElement[]; findAllRendered?(filter?: RenderedFindOptions & { label?: string }): WxRenderedElement[]; dumpRendered?(): void; getRenderedStats?(): RegistryStats; } declare global { interface Window { wxElementRegistry?: WxElementRegistry; } } /** * Wait for element registry to be available */ export async function waitForRegistry(page: Page, timeout = 5000): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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; } } // ============================================================================ // Rendered Elements (toolbar tools, menu items, splitter sashes, AUI parts) // ============================================================================ /** * Find rendered element by label (e.g., toolbar tool "New", menu item "File") */ export async function findRenderedByLabel( page: Page, label: string, options: RenderedFindOptions = {} ): Promise { const elements = await page.evaluate( ([label, opts]: [string, RenderedFindOptions]) => { const registry = window.wxElementRegistry; if (!registry || !registry.findRenderedByLabel) return []; return registry.findRenderedByLabel(label, opts); }, [label, options] as [string, RenderedFindOptions] ); return elements.length > 0 ? elements[0] : null; } /** * Find all rendered elements by label */ export async function findAllRenderedByLabel( page: Page, label: string, options: RenderedFindOptions = {} ): Promise { return page.evaluate( ([label, opts]: [string, RenderedFindOptions]) => { const registry = window.wxElementRegistry; if (!registry || !registry.findRenderedByLabel) return []; return registry.findRenderedByLabel(label, opts); }, [label, options] as [string, RenderedFindOptions] ); } /** * Find all rendered elements of a type (tool, menuitem, sash, auipart) */ export async function findRenderedByType( page: Page, elementType: string, options: RenderedFindOptions = {} ): Promise { return page.evaluate( ([type, opts]: [string, RenderedFindOptions]) => { const registry = window.wxElementRegistry; if (!registry || !registry.findRenderedByType) return []; return registry.findRenderedByType(type, opts); }, [elementType, options] as [string, RenderedFindOptions] ); } /** * Find rendered element by tooltip */ export async function findByTooltip( page: Page, tooltip: string, options: RenderedFindOptions = {} ): Promise { const elements = await page.evaluate( ([tooltip, opts]: [string, RenderedFindOptions]) => { const registry = window.wxElementRegistry; if (!registry || !registry.findAllRendered) return []; // Find all rendered elements and filter by tooltip const all = registry.findAllRendered(opts); return all.filter(e => e.tooltip && e.tooltip.includes(tooltip)); }, [tooltip, options] as [string, RenderedFindOptions] ); return elements.length > 0 ? elements[0] : null; } /** * Click on a rendered element by tooltip */ export async function clickByTooltip( page: Page, tooltip: string, options: RenderedFindOptions = {} ): Promise { const element = await findByTooltip(page, tooltip, options); if (!element) { console.warn(`Element with tooltip "${tooltip}" not found`); return false; } if (!element.enabled) { console.warn(`Element with tooltip "${tooltip}" is disabled`); return false; } await page.mouse.click(element.centerX, element.centerY); return true; } /** * Click on a toolbar tool by label or tooltip */ export async function clickToolbarTool( page: Page, label: string ): Promise { const tool = await findRenderedByLabel(page, label, { elementType: 'tool' }); if (!tool) { console.warn(`Toolbar tool "${label}" not found`); return false; } if (!tool.enabled) { console.warn(`Toolbar tool "${label}" is disabled`); return false; } await page.mouse.click(tool.centerX, tool.centerY); return true; } /** * Click on a menu bar item by label */ export async function clickMenuBarItem( page: Page, label: string ): Promise { const menuItem = await findRenderedByLabel(page, label, { elementType: 'menuitem', subType: 'menubar' }); if (!menuItem) { console.warn(`Menu bar item "${label}" not found`); return false; } if (!menuItem.enabled) { console.warn(`Menu bar item "${label}" is disabled`); return false; } await page.mouse.click(menuItem.centerX, menuItem.centerY); return true; } /** * Click on a popup menu item by label */ export async function clickMenuItem( page: Page, label: string ): Promise { const menuItem = await findRenderedByLabel(page, label, { elementType: 'menuitem' }); if (!menuItem) { console.warn(`Menu item "${label}" not found`); return false; } if (!menuItem.enabled) { console.warn(`Menu item "${label}" is disabled`); return false; } await page.mouse.click(menuItem.centerX, menuItem.centerY); return true; } /** * Get splitter sash element */ export async function getSplitterSash( page: Page, parentId?: string ): Promise { const options: RenderedFindOptions = {}; if (parentId) options.parentId = parentId; const sashes = await findRenderedByType(page, 'sash', options); return sashes.length > 0 ? sashes[0] : null; } /** * Click on an AUI pane button (close, pin, maximize) */ export async function clickAuiButton( page: Page, buttonType: 'close' | 'pin' | 'maximize', paneCaption?: string ): Promise { const options: RenderedFindOptions = { elementType: 'auipart', subType: buttonType }; let button: WxRenderedElement | null = null; if (paneCaption) { // Find the specific pane first, then find the button by parent const caption = await findRenderedByLabel(page, paneCaption, { elementType: 'auipart', subType: 'caption' }); if (caption) { // Button index is based on pane index const paneIndex = caption.index; const buttons = await findRenderedByType(page, 'auipart', { subType: buttonType }); button = buttons.find(b => Math.floor(b.index / 10) === paneIndex) || null; } } else { // Just find the first button of this type const buttons = await findRenderedByType(page, 'auipart', options); button = buttons.length > 0 ? buttons[0] : null; } if (!button) { console.warn(`AUI ${buttonType} button not found`); return false; } await page.mouse.click(button.centerX, button.centerY); return true; } /** * Find an AUI pane content area by panel caption */ export async function findAuiPaneContent( page: Page, paneCaption: string ): Promise { const contents = await findRenderedByType(page, 'auipart', { subType: 'content' }); return contents.find(c => c.label === paneCaption) || null; } /** * Click on an AUI pane content area by panel caption */ export async function clickAuiPaneContent( page: Page, paneCaption: string ): Promise { const content = await findAuiPaneContent(page, paneCaption); if (!content) { console.warn(`AUI pane content "${paneCaption}" not found`); return false; } await page.mouse.click(content.centerX, content.centerY); return true; } /** * Get all AUI pane content areas */ export async function findAllAuiPaneContents(page: Page): Promise { return findRenderedByType(page, 'auipart', { subType: 'content' }); } /** * Click on a rendered element by label (searches all types) */ export async function clickRenderedByLabel( page: Page, label: string, options: RenderedFindOptions = {} ): Promise { const element = await findRenderedByLabel(page, label, options); if (!element) { console.warn(`Rendered element "${label}" not found`); return false; } if (!element.enabled) { console.warn(`Rendered element "${label}" is disabled`); return false; } await page.mouse.click(element.centerX, element.centerY); return true; } /** * Dump all rendered elements to console (for debugging) */ export async function dumpRenderedElements(page: Page): Promise { await page.evaluate(() => { const registry = window.wxElementRegistry; if (registry && registry.dumpRendered) { registry.dumpRendered(); } else { console.log('[wxElementRegistry] Rendered elements not available'); } }); } /** * Get rendered elements statistics */ export async function getRenderedStats(page: Page): Promise { return page.evaluate(() => { const registry = window.wxElementRegistry; if (!registry || !registry.getRenderedStats) return null; return registry.getRenderedStats(); }); } // ============================================================================ // Grid Cell Helpers // ============================================================================ /** * Find a grid cell by row and column */ export async function findGridCell( page: Page, row: number, col: number ): Promise { const elements = await page.evaluate( ([row, col]: [number, number]) => { const registry = window.wxElementRegistry; if (!registry || !registry.findAllRendered) return []; const allCells = registry.findAllRendered({}).filter( e => e.elementType === 'gridcell' && e.tooltip === `Row ${row}, Col ${col}` ); return allCells; }, [row, col] as [number, number] ); return elements.length > 0 ? elements[0] : null; } /** * Find a grid cell by its label/value */ export async function findGridCellByLabel( page: Page, label: string ): Promise { return findRenderedByLabel(page, label, { elementType: 'gridcell' }); } /** * Click on a grid cell by row and column */ export async function clickGridCell( page: Page, row: number, col: number ): Promise { const cell = await findGridCell(page, row, col); if (!cell) { console.warn(`Grid cell at row ${row}, col ${col} not found`); return false; } await page.mouse.click(cell.centerX, cell.centerY); return true; } /** * Click on a grid cell by its label/value */ export async function clickGridCellByLabel( page: Page, label: string ): Promise { const cell = await findGridCellByLabel(page, label); if (!cell) { console.warn(`Grid cell with label "${label}" not found`); return false; } await page.mouse.click(cell.centerX, cell.centerY); return true; } /** * Get all grid cells */ export async function findAllGridCells(page: Page): Promise { return findRenderedByType(page, 'gridcell'); } // ============================================================================ // List Item Helpers // ============================================================================ /** * Find a list item by its label */ export async function findListItem( page: Page, label: string ): Promise { return findRenderedByLabel(page, label, { elementType: 'listitem' }); } /** * Find a list item by row index */ export async function findListItemByIndex( page: Page, index: number ): Promise { const items = await findRenderedByType(page, 'listitem'); return items.find(item => item.index === index) || null; } /** * Click on a list item by label */ export async function clickListItem( page: Page, label: string ): Promise { const item = await findListItem(page, label); if (!item) { console.warn(`List item "${label}" not found`); return false; } await page.mouse.click(item.centerX, item.centerY); return true; } /** * Click on a list item by row index */ export async function clickListItemByIndex( page: Page, index: number ): Promise { const item = await findListItemByIndex(page, index); if (!item) { console.warn(`List item at index ${index} not found`); return false; } await page.mouse.click(item.centerX, item.centerY); return true; } /** * Get all list items */ export async function findAllListItems(page: Page): Promise { return findRenderedByType(page, 'listitem'); } // ============================================================================ // Tab Helpers // ============================================================================ /** * Find a tab by its label */ export async function findTab( page: Page, label: string ): Promise { return findRenderedByLabel(page, label, { elementType: 'tab' }); } /** * Click on a tab by label */ export async function clickTab( page: Page, label: string ): Promise { const tab = await findTab(page, label); if (!tab) { console.warn(`Tab "${label}" not found`); return false; } await page.mouse.click(tab.centerX, tab.centerY); return true; } /** * Get all tabs */ export async function findAllTabs(page: Page): Promise { return findRenderedByType(page, 'tab'); } /** * Get the currently selected tab */ export async function findSelectedTab(page: Page): Promise { const tabs = await page.evaluate(() => { const registry = window.wxElementRegistry; if (!registry || !registry.findAllRendered) return []; return registry.findAllRendered({ label: undefined }).filter( e => e.elementType === 'tab' && e.subType === 'selected' ); }); return tabs.length > 0 ? tabs[0] : null; } // ============================================================================ // Calendar Date Helpers // ============================================================================ /** * Find a calendar date by day number */ export async function findCalendarDate( page: Page, day: number ): Promise { return findRenderedByLabel(page, String(day), { elementType: 'datecell' }); } /** * Click on a calendar date by day number */ export async function clickCalendarDate( page: Page, day: number ): Promise { const dateCell = await findCalendarDate(page, day); if (!dateCell) { console.warn(`Calendar date ${day} not found`); return false; } if (!dateCell.enabled) { console.warn(`Calendar date ${day} is out of range`); return false; } await page.mouse.click(dateCell.centerX, dateCell.centerY); return true; } /** * Get all calendar date cells */ export async function findAllCalendarDates(page: Page): Promise { return findRenderedByType(page, 'datecell'); } /** * Get the currently selected calendar date */ export async function findSelectedCalendarDate(page: Page): Promise { const dates = await page.evaluate(() => { const registry = window.wxElementRegistry; if (!registry || !registry.findAllRendered) return []; return registry.findAllRendered({ label: undefined }).filter( e => e.elementType === 'datecell' && e.subType === 'selected' ); }); return dates.length > 0 ? dates[0] : null; } // ============================================================================ // Tree Item Helpers // ============================================================================ /** * Find a tree item by its label */ export async function findTreeItem( page: Page, label: string ): Promise { return findRenderedByLabel(page, label, { elementType: 'treeitem' }); } /** * Click on a tree item by label */ export async function clickTreeItem( page: Page, label: string ): Promise { const item = await findTreeItem(page, label); if (!item) { console.warn(`Tree item "${label}" not found`); return false; } await page.mouse.click(item.centerX, item.centerY); return true; } /** * Get all tree items */ export async function findAllTreeItems(page: Page): Promise { return findRenderedByType(page, 'treeitem'); } /** * Find tree items by state (expanded, collapsed, leaf) */ export async function findTreeItemsByState( page: Page, state: 'expanded' | 'collapsed' | 'leaf' ): Promise { return findRenderedByType(page, 'treeitem', { subType: state }); } // ============================================================================ // DataView Item Helpers // ============================================================================ /** * Find a dataview item by its label */ export async function findDataViewItem( page: Page, label: string ): Promise { return findRenderedByLabel(page, label, { elementType: 'dataviewitem' }); } /** * Find a dataview item by row index */ export async function findDataViewItemByIndex( page: Page, index: number ): Promise { const items = await findRenderedByType(page, 'dataviewitem'); return items.find(item => item.index === index) || null; } /** * Click on a dataview item by label */ export async function clickDataViewItem( page: Page, label: string ): Promise { const item = await findDataViewItem(page, label); if (!item) { console.warn(`DataView item "${label}" not found`); return false; } await page.mouse.click(item.centerX, item.centerY); return true; } /** * Click on a dataview item by row index */ export async function clickDataViewItemByIndex( page: Page, index: number ): Promise { const item = await findDataViewItemByIndex(page, index); if (!item) { console.warn(`DataView item at index ${index} not found`); return false; } await page.mouse.click(item.centerX, item.centerY); return true; } /** * Get all dataview items */ export async function findAllDataViewItems(page: Page): Promise { return findRenderedByType(page, 'dataviewitem'); } // ============================================================================ // Property Grid Helpers // ============================================================================ /** * Find a property row by its label */ export async function findPropertyRow( page: Page, label: string ): Promise { return findRenderedByLabel(page, label, { elementType: 'proprow' }); } /** * Click on a property row by label */ export async function clickPropertyRow( page: Page, label: string ): Promise { const row = await findPropertyRow(page, label); if (!row) { console.warn(`Property row "${label}" not found`); return false; } if (!row.enabled) { console.warn(`Property row "${label}" is disabled`); return false; } await page.mouse.click(row.centerX, row.centerY); return true; } /** * Get all property rows */ export async function findAllPropertyRows(page: Page): Promise { return findRenderedByType(page, 'proprow'); } /** * Find property rows by type (category, property, selected, disabled) */ export async function findPropertyRowsByType( page: Page, type: 'category' | 'property' | 'selected' | 'disabled' ): Promise { return findRenderedByType(page, 'proprow', { subType: type }); } // ============================================================================ // Listbox/Choice Item Helpers (for dropdown lists) // ============================================================================ /** * Find a listbox item by its label */ export async function findListboxItem( page: Page, label: string ): Promise { return findRenderedByLabel(page, label, { elementType: 'listboxitem' }); } /** * Find a listbox item by index */ export async function findListboxItemByIndex( page: Page, index: number ): Promise { const items = await findRenderedByType(page, 'listboxitem'); return items.find(item => item.index === index) || null; } /** * Click on a listbox item by label (for wxChoice/wxComboBox dropdowns) */ export async function clickListboxItem( page: Page, label: string ): Promise { const item = await findListboxItem(page, label); if (!item) { console.warn(`Listbox item "${label}" not found`); return false; } await page.mouse.click(item.centerX, item.centerY); return true; } /** * Click on a listbox item by index */ export async function clickListboxItemByIndex( page: Page, index: number ): Promise { const item = await findListboxItemByIndex(page, index); if (!item) { console.warn(`Listbox item at index ${index} not found`); return false; } await page.mouse.click(item.centerX, item.centerY); return true; } /** * Get all listbox items */ export async function findAllListboxItems(page: Page): Promise { return findRenderedByType(page, 'listboxitem'); } // ============================================================================ // Spin Button Helpers // ============================================================================ /** * Find a spin button arrow by type */ export async function findSpinButton( page: Page, arrowType: 'up' | 'down' | 'left' | 'right' ): Promise { const buttons = await findRenderedByType(page, 'spinbutton', { subType: arrowType }); return buttons.length > 0 ? buttons[0] : null; } /** * Click the spin button up arrow (or left for horizontal) */ export async function clickSpinUp(page: Page): Promise { // Try 'up' first (vertical), then 'left' (horizontal) let button = await findSpinButton(page, 'up'); if (!button) { button = await findSpinButton(page, 'left'); } if (!button) { console.warn('Spin up/left button not found'); return false; } if (!button.enabled) { console.warn('Spin up/left button is disabled'); return false; } await page.mouse.click(button.centerX, button.centerY); return true; } /** * Click the spin button down arrow (or right for horizontal) */ export async function clickSpinDown(page: Page): Promise { // Try 'down' first (vertical), then 'right' (horizontal) let button = await findSpinButton(page, 'down'); if (!button) { button = await findSpinButton(page, 'right'); } if (!button) { console.warn('Spin down/right button not found'); return false; } if (!button.enabled) { console.warn('Spin down/right button is disabled'); return false; } await page.mouse.click(button.centerX, button.centerY); return true; } /** * Get all spin buttons */ export async function findAllSpinButtons(page: Page): Promise { return findRenderedByType(page, 'spinbutton'); } // ============================================================================ // Slider Helpers // ============================================================================ /** * Find a slider thumb by name/label */ export async function findSlider( page: Page, name?: string ): Promise { if (name) { return findRenderedByLabel(page, name, { elementType: 'slider' }); } const sliders = await findRenderedByType(page, 'slider'); return sliders.length > 0 ? sliders[0] : null; } /** * Find a slider track for drag operations */ export async function findSliderTrack( page: Page, name?: string ): Promise { if (name) { return findRenderedByLabel(page, name, { elementType: 'slidertrack' }); } const tracks = await findRenderedByType(page, 'slidertrack'); return tracks.length > 0 ? tracks[0] : null; } /** * Click on a slider thumb */ export async function clickSlider( page: Page, name?: string ): Promise { const slider = await findSlider(page, name); if (!slider) { console.warn(`Slider${name ? ` "${name}"` : ''} not found`); return false; } if (!slider.enabled) { console.warn(`Slider${name ? ` "${name}"` : ''} is disabled`); return false; } await page.mouse.click(slider.centerX, slider.centerY); return true; } /** * Drag slider to a position within the track * @param position - value between 0 and 1 (0 = min, 1 = max) */ export async function dragSliderTo( page: Page, position: number, name?: string ): Promise { const slider = await findSlider(page, name); const track = await findSliderTrack(page, name); if (!slider || !track) { console.warn(`Slider or track${name ? ` "${name}"` : ''} not found`); return false; } if (!slider.enabled) { console.warn(`Slider${name ? ` "${name}"` : ''} is disabled`); return false; } // Clamp position to [0, 1] position = Math.max(0, Math.min(1, position)); // Determine if horizontal or vertical based on track dimensions const isHorizontal = track.width > track.height; let targetX: number, targetY: number; if (isHorizontal) { targetX = track.screenX + track.width * position; targetY = track.centerY; } else { targetX = track.centerX; // For vertical sliders, 0 is typically at bottom targetY = track.screenY + track.height * (1 - position); } // Drag from current slider position to target await page.mouse.move(slider.centerX, slider.centerY); await page.mouse.down(); await page.mouse.move(targetX, targetY); await page.mouse.up(); return true; } /** * Get all sliders */ export async function findAllSliders(page: Page): Promise { return findRenderedByType(page, 'slider'); } // ============================================================================ // Text Control Helpers // ============================================================================ /** * Find a text control by name */ export async function findTextCtrl( page: Page, name?: string ): Promise { if (name) { return findRenderedByLabel(page, name, { elementType: 'textctrl' }); } const ctrls = await findRenderedByType(page, 'textctrl'); return ctrls.length > 0 ? ctrls[0] : null; } /** * Find a singleline text control */ export async function findSingleLineTextCtrl( page: Page, name?: string ): Promise { if (name) { const ctrl = await findRenderedByLabel(page, name, { elementType: 'textctrl', subType: 'singleline' }); return ctrl; } const ctrls = await findRenderedByType(page, 'textctrl', { subType: 'singleline' }); return ctrls.length > 0 ? ctrls[0] : null; } /** * Find a multiline text control */ export async function findMultiLineTextCtrl( page: Page, name?: string ): Promise { if (name) { const ctrl = await findRenderedByLabel(page, name, { elementType: 'textctrl', subType: 'multiline' }); return ctrl; } const ctrls = await findRenderedByType(page, 'textctrl', { subType: 'multiline' }); return ctrls.length > 0 ? ctrls[0] : null; } /** * Click on a text control to focus it */ export async function clickTextCtrl( page: Page, name?: string ): Promise { const ctrl = await findTextCtrl(page, name); if (!ctrl) { console.warn(`TextCtrl${name ? ` "${name}"` : ''} not found`); return false; } if (!ctrl.enabled) { console.warn(`TextCtrl${name ? ` "${name}"` : ''} is disabled`); return false; } await page.mouse.click(ctrl.centerX, ctrl.centerY); return true; } /** * Get all text controls */ export async function findAllTextCtrls(page: Page): Promise { return findRenderedByType(page, 'textctrl'); } // ============================================================================ // Combo/Choice Button Helpers // ============================================================================ /** * Find a combo/choice dropdown button */ export async function findComboButton( page: Page, value?: string ): Promise { if (value) { return findRenderedByLabel(page, value, { elementType: 'combobutton' }); } const buttons = await findRenderedByType(page, 'combobutton'); return buttons.length > 0 ? buttons[0] : null; } /** * Click on a combo/choice dropdown button to open/close the dropdown */ export async function clickComboButton( page: Page, value?: string ): Promise { const button = await findComboButton(page, value); if (!button) { console.warn(`Combo button${value ? ` with value "${value}"` : ''} not found`); return false; } if (!button.enabled) { console.warn(`Combo button${value ? ` with value "${value}"` : ''} is disabled`); return false; } await page.mouse.click(button.centerX, button.centerY); return true; } /** * Open a combo/choice dropdown (clicks button if closed) */ export async function openComboDropdown( page: Page, value?: string ): Promise { const button = await findComboButton(page, value); if (!button) { console.warn(`Combo button${value ? ` with value "${value}"` : ''} not found`); return false; } // Only click if dropdown is closed if (button.subType === 'closed') { await page.mouse.click(button.centerX, button.centerY); } return true; } /** * Select an item from combo/choice dropdown by label * Opens the dropdown if needed, then clicks the item */ export async function selectComboItem( page: Page, itemLabel: string, comboValue?: string ): Promise { // Open the dropdown const opened = await openComboDropdown(page, comboValue); if (!opened) return false; // Wait a bit for dropdown to appear await page.waitForTimeout(100); // Click the listbox item return clickListboxItem(page, itemLabel); } /** * Get all combo buttons */ export async function findAllComboButtons(page: Page): Promise { return findRenderedByType(page, 'combobutton'); } // ============================================================================ // Search Control Helpers // ============================================================================ /** * Find a search control text field */ export async function findSearchCtrl( page: Page, hint?: string ): Promise { if (hint) { return findRenderedByLabel(page, hint, { elementType: 'searchctrl' }); } const ctrls = await findRenderedByType(page, 'searchctrl'); return ctrls.length > 0 ? ctrls[0] : null; } /** * Click on a search control to focus it */ export async function clickSearchCtrl( page: Page, hint?: string ): Promise { const ctrl = await findSearchCtrl(page, hint); if (!ctrl) { console.warn(`SearchCtrl${hint ? ` "${hint}"` : ''} not found`); return false; } if (!ctrl.enabled) { console.warn(`SearchCtrl${hint ? ` "${hint}"` : ''} is disabled`); return false; } await page.mouse.click(ctrl.centerX, ctrl.centerY); return true; } /** * Find the search button in a search control */ export async function findSearchButton( page: Page ): Promise { const buttons = await findRenderedByType(page, 'searchbutton', { subType: 'search' }); return buttons.length > 0 ? buttons[0] : null; } /** * Find the cancel button in a search control */ export async function findSearchCancelButton( page: Page ): Promise { const buttons = await findRenderedByType(page, 'searchbutton', { subType: 'cancel' }); return buttons.length > 0 ? buttons[0] : null; } /** * Click the search button */ export async function clickSearchButton(page: Page): Promise { const button = await findSearchButton(page); if (!button) { console.warn('Search button not found'); return false; } await page.mouse.click(button.centerX, button.centerY); return true; } /** * Click the cancel/clear button in a search control */ export async function clickSearchCancelButton(page: Page): Promise { const button = await findSearchCancelButton(page); if (!button) { console.warn('Search cancel button not found'); return false; } await page.mouse.click(button.centerX, button.centerY); return true; } /** * Get all search controls */ export async function findAllSearchCtrls(page: Page): Promise { return findRenderedByType(page, 'searchctrl'); } // ============================================================================ // Column Header Helpers (for wxDataViewCtrl, wxGrid, etc.) // ============================================================================ /** * Find a column header by its title */ export async function findColumnHeader( page: Page, title: string ): Promise { return findRenderedByLabel(page, title, { elementType: 'columnheader' }); } /** * Find a column header by index */ export async function findColumnHeaderByIndex( page: Page, index: number ): Promise { const headers = await findRenderedByType(page, 'columnheader'); return headers.find(h => h.index === index) || null; } /** * Click on a column header by title (for sorting) */ export async function clickColumnHeader( page: Page, title: string ): Promise { const header = await findColumnHeader(page, title); if (!header) { console.warn(`Column header "${title}" not found`); return false; } if (!header.enabled) { console.warn(`Column header "${title}" is disabled`); return false; } await page.mouse.click(header.centerX, header.centerY); return true; } /** * Click on a column header by index */ export async function clickColumnHeaderByIndex( page: Page, index: number ): Promise { const header = await findColumnHeaderByIndex(page, index); if (!header) { console.warn(`Column header at index ${index} not found`); return false; } if (!header.enabled) { console.warn(`Column header at index ${index} is disabled`); return false; } await page.mouse.click(header.centerX, header.centerY); return true; } /** * Get all column headers */ export async function findAllColumnHeaders(page: Page): Promise { return findRenderedByType(page, 'columnheader'); } /** * Find sortable column headers */ export async function findSortableColumnHeaders(page: Page): Promise { return findRenderedByType(page, 'columnheader', { subType: 'sortable' }); } // ============================================================================ // Styled Text Control (STC) Helpers // ============================================================================ /** * Find a styled text control (code editor) by name */ export async function findStyledTextCtrl( page: Page, name?: string ): Promise { if (name) { return findRenderedByLabel(page, name, { elementType: 'styledtext' }); } const ctrls = await findRenderedByType(page, 'styledtext'); return ctrls.length > 0 ? ctrls[0] : null; } /** * Click on a styled text control to focus it */ export async function clickStyledTextCtrl( page: Page, name?: string ): Promise { const ctrl = await findStyledTextCtrl(page, name); if (!ctrl) { console.warn(`StyledTextCtrl${name ? ` "${name}"` : ''} not found`); return false; } if (!ctrl.enabled) { console.warn(`StyledTextCtrl${name ? ` "${name}"` : ''} is read-only`); return false; } await page.mouse.click(ctrl.centerX, ctrl.centerY); return true; } /** * Get all styled text controls */ export async function findAllStyledTextCtrls(page: Page): Promise { return findRenderedByType(page, 'styledtext'); }