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 { await this.wda.createSession(); } async destroySession(): Promise { await this.wda.destroySession(); } async activateApp(appId: string): Promise { await this.wda.activateAppById(appId); } async findElement(locator: ElementLocator): Promise { 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 { 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 { 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 { return this.wda.restartApp(); } async findElementsRaw(using: string, value: string): Promise { 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 { return this.wda.getElementRect(elementId); } async getElementAttribute(elementId: string, attr: string): Promise { return this.wda.getElementAttribute(elementId, attr); } async tap(x: number, y: number): Promise { return this.wda.tap(x, y); } async doubleTap(x: number, y: number): Promise { return this.wda.doubleTap(x, y); } async longPress(x: number, y: number, duration = 2): Promise { return this.wda.longPress(x, y, duration); } async tapElement(elementId: string): Promise { return this.wda.tapElement(elementId); } async clickElement(elementId: string): Promise { return this.wda.clickElement(elementId); } async typeText(elementId: string, text: string): Promise { return this.wda.typeText(elementId, text); } /** PickerWheel 整串设值(时间轮等)。见 WDAHelper.setPickerValue。 */ async setPickerWheelValue(elementId: string, value: string): Promise { return this.wda.setPickerValue(elementId, value); } /** 向聚焦输入框发送键盘按键(iOS 自绘输入框用)。 */ async sendKeys(keys: string[]): Promise { return this.wda.sendKeys(keys); } async clearText(elementId: string): Promise { return this.wda.clearText(elementId); } async swipe(fromX: number, fromY: number, toX: number, toY: number, duration = 0.5): Promise { return this.wda.swipe(fromX, fromY, toX, toY, duration); } async scrollDown(distance = 300): Promise { return this.wda.scrollDown(distance); } /** WDA 原生元素滚动(不产生触点点击,根治列表误点)。见 WDAHelper.scrollElement。 */ async scrollElement(elementId: string, body: Record): Promise { return this.wda.scrollElement(elementId, body); } async scrollUp(distance = 300): Promise { return this.wda.scrollUp(distance); } async goBack(): Promise { await this.wda.tap(33, 69); } async getSource(): Promise { return this.wda.getSource(); } async getWindowSize(): Promise<{ width: number; height: number }> { return this.wda.getWindowSize(); } async screenshot(): Promise { return this.wda.screenshot(); } async tapByLocator(locator: ElementLocator): Promise { const elemId = await this.findElement(locator); if (!elemId) return false; await this.tapElement(elemId); return true; } async waitForElement(locator: ElementLocator, timeoutMs = 10000): Promise { 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 { 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 { return this.findDeviceCard('Bot'); } async findDeviceCard(deviceName: string): Promise { 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 { return this.wda.isOnHomepage(); } async goBackToHomepage(): Promise { return this.wda.goBackToHomepage(); } async dismissPopupIfPresent(): Promise { 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; } }