AI_UIAutomation/drivers/wda-driver.ts

242 lines
8.2 KiB
TypeScript

import { DeviceDriver, ElementLocator, Rect, Platform } from './types';
import { WDAHelper } from '../utils/wda-helper';
/**
* 把 Android UiSelector 串翻译成 iOS 定位,使全代码库里写死的 `findElementRaw('-android uiautomator', ...)`
* 在 iOS 上也能命中(免去逐个用例加 iOS 分支)。无 iOS 等价(resourceId 等)返回 null。
* .text("X") → name/label 精确
* .textContains("X") → name/label CONTAINS
* .textMatches("(?i)X") → 取核心词 CONTAINS
* .description(s)("X") → label/name CONTAINS
* className EditText → TextField/SecureTextField;Button → XCUIElementTypeButton
* resourceId/其它 → null(iOS 无等价)
*/
function androidSelectorToIOS(value: string): { using: string; value: string } | null {
let m: RegExpMatchArray | null;
if ((m = value.match(/\.textContains\("([^"]*)"\)/))) {
return { using: 'predicate string', value: `name CONTAINS "${m[1]}" OR label CONTAINS "${m[1]}"` };
}
if ((m = value.match(/\.textMatches\("(?:\(\?i\))?([^"]*)"\)/))) {
const core = (m[1].replace(/[.*+?^${}()|[\]\\]/g, ' ').trim().split(/\s+/)[0]) || m[1];
return { using: 'predicate string', value: `name CONTAINS "${core}" OR label CONTAINS "${core}"` };
}
if ((m = value.match(/\.text\("([^"]*)"\)/))) {
return { using: 'predicate string', value: `name == "${m[1]}" OR label == "${m[1]}"` };
}
if ((m = value.match(/\.description(?:Contains)?\("([^"]*)"\)/))) {
return { using: 'predicate string', value: `label CONTAINS "${m[1]}" OR name CONTAINS "${m[1]}"` };
}
if (/className\("android\.widget\.EditText"\)/.test(value)) {
return { using: 'predicate string', value: 'type == "XCUIElementTypeTextField" OR type == "XCUIElementTypeSecureTextField"' };
}
if (/className\("android\.widget\.Button"\)/.test(value)) {
return { using: 'class name', value: 'XCUIElementTypeButton' };
}
return null; // resourceId / CheckBox 等无 iOS 等价
}
export class WDADriver implements DeviceDriver {
readonly platform: Platform = 'ios';
private wda: WDAHelper;
constructor(host = 'localhost', port = 8100) {
this.wda = new WDAHelper(host, port);
}
async createSession(): Promise<void> {
await this.wda.createSession();
}
async destroySession(): Promise<void> {
await this.wda.destroySession();
}
async activateApp(appId: string): Promise<void> {
await this.wda.activateAppById(appId);
}
async findElement(locator: ElementLocator): Promise<string | null> {
if (!locator.ios) throw new Error(`Locator "${locator.name}" has no iOS strategy`);
return this.wda.findElement(locator.ios.using, locator.ios.value);
}
async findElements(locator: ElementLocator): Promise<string[]> {
if (!locator.ios) throw new Error(`Locator "${locator.name}" has no iOS strategy`);
return this.wda.findElements(locator.ios.using, locator.ios.value);
}
async findElementRaw(using: string, value: string): Promise<string | null> {
if (using === '-android uiautomator') {
const t = androidSelectorToIOS(value);
return t ? this.wda.findElement(t.using, t.value) : null;
}
return this.wda.findElement(using, value);
}
/** 硬复位:terminate+launch App 回干净首页(失败残留态兜底,防级联污染后续用例)。 */
async restartApp(): Promise<void> {
return this.wda.restartApp();
}
async findElementsRaw(using: string, value: string): Promise<string[]> {
if (using === '-android uiautomator') {
const t = androidSelectorToIOS(value);
return t ? this.wda.findElements(t.using, t.value) : [];
}
return this.wda.findElements(using, value);
}
async getElementRect(elementId: string): Promise<Rect> {
return this.wda.getElementRect(elementId);
}
async getElementAttribute(elementId: string, attr: string): Promise<string> {
return this.wda.getElementAttribute(elementId, attr);
}
async tap(x: number, y: number): Promise<void> {
return this.wda.tap(x, y);
}
async doubleTap(x: number, y: number): Promise<void> {
return this.wda.doubleTap(x, y);
}
async longPress(x: number, y: number, duration = 2): Promise<void> {
return this.wda.longPress(x, y, duration);
}
async tapElement(elementId: string): Promise<void> {
return this.wda.tapElement(elementId);
}
async clickElement(elementId: string): Promise<void> {
return this.wda.clickElement(elementId);
}
async typeText(elementId: string, text: string): Promise<void> {
return this.wda.typeText(elementId, text);
}
/** PickerWheel 整串设值(时间轮等)。见 WDAHelper.setPickerValue。 */
async setPickerWheelValue(elementId: string, value: string): Promise<void> {
return this.wda.setPickerValue(elementId, value);
}
/** 向聚焦输入框发送键盘按键(iOS 自绘输入框用)。 */
async sendKeys(keys: string[]): Promise<void> {
return this.wda.sendKeys(keys);
}
async clearText(elementId: string): Promise<void> {
return this.wda.clearText(elementId);
}
async swipe(fromX: number, fromY: number, toX: number, toY: number, duration = 0.5): Promise<void> {
return this.wda.swipe(fromX, fromY, toX, toY, duration);
}
async scrollDown(distance = 300): Promise<void> {
return this.wda.scrollDown(distance);
}
/** WDA 原生元素滚动(不产生触点点击,根治列表误点)。见 WDAHelper.scrollElement。 */
async scrollElement(elementId: string, body: Record<string, unknown>): Promise<void> {
return this.wda.scrollElement(elementId, body);
}
async scrollUp(distance = 300): Promise<void> {
return this.wda.scrollUp(distance);
}
async goBack(): Promise<void> {
await this.wda.tap(33, 69);
}
async getSource(): Promise<string> {
return this.wda.getSource();
}
async getWindowSize(): Promise<{ width: number; height: number }> {
return this.wda.getWindowSize();
}
async screenshot(): Promise<string> {
return this.wda.screenshot();
}
async tapByLocator(locator: ElementLocator): Promise<boolean> {
const elemId = await this.findElement(locator);
if (!elemId) return false;
await this.tapElement(elemId);
return true;
}
async waitForElement(locator: ElementLocator, timeoutMs = 10000): Promise<string | null> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const elemId = await this.findElement(locator);
if (elemId) return elemId;
await new Promise((r) => setTimeout(r, 500));
}
return null;
}
async isElementVisible(locator: ElementLocator): Promise<boolean> {
const elemId = await this.findElement(locator);
if (!elemId) return false;
const visible = await this.getElementAttribute(elemId, 'visible');
return visible === 'true' || visible === '1';
}
async findBotCard(): Promise<string | null> {
return this.findDeviceCard('Bot');
}
async findDeviceCard(deviceName: string): Promise<string | null> {
const predicates = [
`name CONTAINS "${deviceName}" AND type == "XCUIElementTypeCell"`,
];
for (const pred of predicates) {
const elems = await this.findElementsRaw('predicate string', pred);
if (elems.length > 0) return elems[0];
}
return null;
}
async isOnHomepage(): Promise<boolean> {
return this.wda.isOnHomepage();
}
async goBackToHomepage(): Promise<boolean> {
return this.wda.goBackToHomepage();
}
async dismissPopupIfPresent(): Promise<boolean> {
const source = await this.getSource();
if (source.includes('XCUIElementTypeAlert')) {
const dismissButtons = ['OK', 'Got it', 'Close', 'Cancel', 'Confirm', 'Allow', "Don't Allow", 'Later', 'Skip', 'Done'];
for (const btn of dismissButtons) {
const el = await this.findElementRaw('name', btn);
if (el) {
await this.tapElement(el);
await new Promise(r => setTimeout(r, 1000));
return true;
}
}
}
if (source.includes('Upgrade') || source.includes("What's New")) {
const dismissButtons = ['Close', 'Later', 'Skip', 'Not Now'];
for (const btn of dismissButtons) {
const el = await this.findElementRaw('name', btn);
if (el) {
await this.tapElement(el);
await new Promise(r => setTimeout(r, 1000));
return true;
}
}
}
return false;
}
}