AI_UIAutomation/drivers/hubshow-driver.ts

257 lines
9.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { DeviceDriver, ElementLocator, Platform } from './types';
/**
* Hub Show Driver — 直连 AI Hub Show 设备屏幕
*
* 与 AndroidDriver 的区别:
* - 通过 udid 指定 Hub Show 设备(区分手机)
* - 不启动/激活任何 App测试设备本机固件 UI
* - 使用独立的 Appium 端口 (默认 4724)
*/
export class HubShowDriver implements DeviceDriver {
readonly platform: Platform = 'android';
private sessionId: string | null = null;
private baseUrl: string;
private udid: string;
constructor(
host = process.env.HUBSHOW_APPIUM_HOST || 'localhost',
port = Number(process.env.HUBSHOW_APPIUM_PORT) || 4724,
udid = process.env.HUBSHOW_UDID || ''
) {
this.baseUrl = `http://${host}:${port}`;
this.udid = udid;
}
private get sessionUrl(): string {
if (!this.sessionId) throw new Error('No active Appium session');
return `${this.baseUrl}/session/${this.sessionId}`;
}
private async request(method: string, path: string, body?: any): Promise<any> {
const url = path.startsWith('/') ? `${this.baseUrl}${path}` : path;
const opts: RequestInit = { method, headers: { 'Content-Type': 'application/json' } };
if (body !== undefined) opts.body = JSON.stringify(body);
const resp = await fetch(url, opts);
const json = await resp.json();
if (json.value && json.value.error) {
throw new Error(`Appium: ${json.value.message || json.value.error}`);
}
return json.value;
}
async createSession(): Promise<void> {
const caps: Record<string, any> = {
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:noReset': true,
'appium:autoLaunch': false,
'appium:newCommandTimeout': 300,
'appium:uiautomator2ServerInstallTimeout': 60000,
};
if (this.udid) caps['appium:udid'] = this.udid;
const result = await this.request('POST', '/session', { capabilities: { alwaysMatch: caps } });
this.sessionId = result.sessionId;
await new Promise(r => setTimeout(r, 2000));
}
async destroySession(): Promise<void> {
if (!this.sessionId) return;
await this.request('DELETE', `/session/${this.sessionId}`);
this.sessionId = null;
}
// Hub Show 直连设备固件 UI不管理 AppactivateApp 为空实现以满足接口。
async activateApp(_appId: string): Promise<void> {
/* no-op */
}
async findElement(locator: ElementLocator): Promise<string | null> {
if (!locator.android) return null;
return this.findElementRaw(locator.android.using, locator.android.value);
}
async findElements(locator: ElementLocator): Promise<string[]> {
if (!locator.android) return [];
return this.findElementsRaw(locator.android.using, locator.android.value);
}
private mapStrategy(using: string, value: string): { using: string; value: string } {
if (using === 'name' || using === 'text') {
return { using: '-android uiautomator', value: `new UiSelector().text("${value}")` };
}
if (using === 'accessibility id' || using === 'content-desc') {
return { using: 'accessibility id', value };
}
if (using === 'id') {
return { using: 'id', value };
}
if (using === 'predicate string') {
if (value.includes('textContains')) {
const match = value.match(/textContains\("([^"]+)"\)/);
if (match) return { using: '-android uiautomator', value: `new UiSelector().textContains("${match[1]}")` };
}
return { using: '-android uiautomator', value: `new UiSelector().textContains("${value}")` };
}
return { using, value };
}
async findElementRaw(using: string, value: string): Promise<string | null> {
const mapped = this.mapStrategy(using, value);
try {
const el = await this.request('POST', `${this.sessionUrl}/element`, mapped);
return el?.ELEMENT || el?.['element-6066-11e4-a52e-4f735466cecf'] || null;
} catch { return null; }
}
async findElementsRaw(using: string, value: string): Promise<string[]> {
const mapped = this.mapStrategy(using, value);
try {
const els = await this.request('POST', `${this.sessionUrl}/elements`, mapped);
if (!Array.isArray(els)) return [];
return els.map((e: any) => e.ELEMENT || e['element-6066-11e4-a52e-4f735466cecf']).filter(Boolean);
} catch { return []; }
}
async getElementRect(elementId: string): Promise<{ x: number; y: number; width: number; height: number }> {
return await this.request('GET', `${this.sessionUrl}/element/${elementId}/rect`);
}
async getElementAttribute(elementId: string, attr: string): Promise<string> {
return await this.request('GET', `${this.sessionUrl}/element/${elementId}/attribute/${attr}`) || '';
}
async tap(x: number, y: number): Promise<void> {
await this.request('POST', `${this.sessionUrl}/actions`, {
actions: [{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions: [
{ type: 'pointerMove', duration: 0, x: Math.round(x), y: Math.round(y) },
{ type: 'pointerDown', button: 0 },
{ type: 'pause', duration: 100 },
{ type: 'pointerUp', button: 0 },
]}]
});
}
async doubleTap(x: number, y: number): Promise<void> {
await this.tap(x, y);
await new Promise(r => setTimeout(r, 100));
await this.tap(x, y);
}
async longPress(x: number, y: number, duration = 2000): Promise<void> {
await this.request('POST', `${this.sessionUrl}/actions`, {
actions: [{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions: [
{ type: 'pointerMove', duration: 0, x: Math.round(x), y: Math.round(y) },
{ type: 'pointerDown', button: 0 },
{ type: 'pause', duration },
{ type: 'pointerUp', button: 0 },
]}]
});
}
async tapElement(elementId: string): Promise<void> {
await this.request('POST', `${this.sessionUrl}/element/${elementId}/click`, {});
}
async clickElement(elementId: string): Promise<void> {
await this.tapElement(elementId);
}
async typeText(elementId: string, text: string): Promise<void> {
await this.request('POST', `${this.sessionUrl}/element/${elementId}/value`, { text });
}
async clearText(elementId: string): Promise<void> {
await this.request('POST', `${this.sessionUrl}/element/${elementId}/clear`, {});
}
async swipe(fromX: number, fromY: number, toX: number, toY: number, duration = 0.5): Promise<void> {
const ms = Math.round(duration * 1000);
await this.request('POST', `${this.sessionUrl}/actions`, {
actions: [{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions: [
{ type: 'pointerMove', duration: 0, x: Math.round(fromX), y: Math.round(fromY) },
{ type: 'pointerDown', button: 0 },
{ type: 'pointerMove', duration: ms, x: Math.round(toX), y: Math.round(toY) },
{ type: 'pointerUp', button: 0 },
]}]
});
}
async scrollDown(distance = 300): Promise<void> {
const midX = 540;
await this.swipe(midX, 600, midX, 600 - distance, 0.5);
}
async scrollUp(distance = 300): Promise<void> {
const midX = 540;
await this.swipe(midX, 300, midX, 300 + distance, 0.5);
}
async goBack(): Promise<void> {
await this.request('POST', `${this.sessionUrl}/back`, {});
}
async getSource(): Promise<string> {
return await this.request('GET', `${this.sessionUrl}/source`);
}
async getWindowSize(): Promise<{ width: number; height: number }> {
const rect = await this.request('GET', `${this.sessionUrl}/window/rect`);
return { width: rect.width, height: rect.height };
}
async screenshot(): Promise<string> {
return await this.request('GET', `${this.sessionUrl}/screenshot`);
}
async tapByLocator(locator: ElementLocator): Promise<boolean> {
const el = await this.findElement(locator);
if (!el) return false;
await this.tapElement(el);
return true;
}
async waitForElement(locator: ElementLocator, timeoutMs = 10000): Promise<string | null> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const el = await this.findElement(locator);
if (el) return el;
await new Promise(r => setTimeout(r, 1000));
}
return null;
}
async isElementVisible(locator: ElementLocator): Promise<boolean> {
const el = await this.findElement(locator);
return el !== null;
}
async goBackToHomepage(): Promise<boolean> {
for (let i = 0; i < 5; i++) {
await this.goBack();
await new Promise(r => setTimeout(r, 1500));
const src = await this.getSource();
if (src.includes('安防') || src.includes('Security') || src.includes('主页')) return true;
}
return false;
}
async dismissPopupIfPresent(): Promise<boolean> {
const dismissTexts = ['Got it', 'OK', 'I know', '我知道了', 'Confirm', 'Allow'];
for (const text of dismissTexts) {
const el = await this.findElementRaw('-android uiautomator', `new UiSelector().text("${text}")`);
if (el) {
await this.tapElement(el);
await new Promise(r => setTimeout(r, 1000));
return true;
}
}
return false;
}
}
export function createHubShowDriver(): HubShowDriver {
return new HubShowDriver();
}