AI_UIAutomation/utils/common/scene.helper.ts

306 lines
12 KiB
TypeScript
Raw 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 } from '../../drivers/types';
import { sleep, waitForElement, waitForSource } from './element.helper';
import { ensureHomeTab, navigateToAddPage, navigateToManageScenes, dismissGuideOverlay } from './navigation.helper';
export interface SceneConfig {
name: string;
deviceKeyword: string;
action: string;
}
export async function createScene(driver: DeviceDriver, config: SceneConfig): Promise<boolean> {
const { name, deviceKeyword, action } = config;
const opened = await navigateToAddPage(driver, 'Scene');
if (!opened) return false;
await dismissGuideOverlay(driver); // 首进场景页可能有引导浮层,挡住 Add action
if (driver.platform === 'android') {
const addAction = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Add action")');
if (!addAction) { console.log('FAIL: no Add action'); return false; }
await driver.tapElement(addAction);
await sleep(2000);
const smartDevices = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Smart Devices")');
if (!smartDevices) { console.log('FAIL: no Smart Devices'); return false; }
await driver.tapElement(smartDevices);
let deviceEl: string | null = null;
for (let i = 0; i < 15; i++) {
await sleep(1000);
deviceEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${deviceKeyword}")`);
if (deviceEl) break;
}
if (!deviceEl) { console.log(`FAIL: no ${deviceKeyword} in device list`); return false; }
await driver.tapElement(deviceEl);
await sleep(2000);
let actionEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${action}")`);
if (!actionEl) {
actionEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Presses once")');
}
if (!actionEl) { console.log('FAIL: no action option'); return false; }
await driver.tapElement(actionEl);
await sleep(2000);
const editText = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Enter Scene name")');
if (editText) {
await driver.tapElement(editText);
await sleep(300);
await driver.typeText(editText, name);
await sleep(300);
await driver.goBack();
await sleep(500);
}
const saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
if (!saveEl) { console.log('FAIL: no Save'); return false; }
await driver.tapElement(saveEl);
await sleep(3000);
return true;
}
// iOS
const addAction = await driver.findElementRaw('predicate string', 'name BEGINSWITH "Add action" AND name CONTAINS "E.g."');
if (!addAction) { console.log('FAIL: no Add action'); return false; }
const aaRect = await driver.getElementRect(addAction);
await driver.tap(aaRect.x + aaRect.width / 2, aaRect.y + aaRect.height / 2);
const smartDevices = await waitForElement(driver, 'predicate string', 'name CONTAINS "Smart Devices"', 6000);
if (!smartDevices) { console.log('FAIL: no Smart Devices'); return false; }
await driver.tapElement(smartDevices);
const deviceEl = await waitForElement(driver, 'predicate string', `name CONTAINS "${deviceKeyword}"`, 6000);
if (!deviceEl) { console.log(`FAIL: no ${deviceKeyword}`); return false; }
await driver.tapElement(deviceEl);
const actionEl = await waitForElement(driver, 'name', action, 5000);
if (!actionEl) {
// Fallback: if Bot is in Press mode, "Turns off/on" won't exist
const pressEl = await driver.findElementRaw('name', 'Presses once');
if (!pressEl) { console.log('FAIL: no action ' + action); return false; }
await driver.tapElement(pressEl);
} else {
await driver.tapElement(actionEl);
}
await sleep(1500);
const tfs = await driver.findElementsRaw('class name', 'XCUIElementTypeTextField');
if (tfs.length > 0) {
await driver.tapElement(tfs[0]);
await sleep(300);
await driver.typeText(tfs[0], name);
await sleep(300);
const done = await driver.findElementRaw('name', 'Done');
if (done) { await driver.tapElement(done); }
else { await driver.tap(195, 400); }
await sleep(1000);
}
const saveEl = await driver.findElementRaw('name', 'Save');
if (!saveEl) { console.log('FAIL: no Save'); return false; }
await driver.tapElement(saveEl);
await sleep(3000);
return true;
}
export async function executeSceneFromHomepage(driver: DeviceDriver, sceneName: string): Promise<boolean> {
if (driver.platform === 'android') {
await driver.goBackToHomepage();
await sleep(500);
try {
const homeTab = await driver.findElementRaw('accessibility id', 'Home');
if (homeTab) { await driver.tapElement(homeTab); await sleep(1000); }
} catch { /* stale */ }
for (let i = 0; i < 5; i++) {
const sceneEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${sceneName}")`);
if (sceneEl) {
await driver.tapElement(sceneEl);
await sleep(1500);
return true;
}
await driver.scrollDown(300);
await sleep(800);
}
return false;
}
// iOS
const source = await driver.getSource();
if (!source.includes(sceneName)) {
await driver.scrollUp(200);
await sleep(800);
}
const sceneEl = await driver.findElementRaw('predicate string', `name CONTAINS "${sceneName}"`);
if (sceneEl) {
await driver.tapElement(sceneEl);
await sleep(1500);
return true;
}
return false;
}
export async function deleteScene(driver: DeviceDriver, sceneName: string): Promise<boolean> {
if (driver.platform === 'android') {
try {
const homeTab = await driver.findElementRaw('accessibility id', 'Home');
if (homeTab) { await driver.tapElement(homeTab); await sleep(1000); }
} catch { /* stale */ }
const moreBtn = await driver.findElementRaw('id', 'com.theswitchbot.switchbot:id/moreBto');
if (!moreBtn) return false;
await driver.tapElement(moreBtn);
await sleep(1500);
const manageEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Manage Scenes")');
if (!manageEl) return false;
await driver.tapElement(manageEl);
let sceneEl: string | null = null;
for (let i = 0; i < 15; i++) {
await sleep(1000);
sceneEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${sceneName}")`);
if (sceneEl) break;
}
if (!sceneEl) return false;
await driver.tapElement(sceneEl);
await sleep(2000);
let delEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
if (!delEl) {
await driver.scrollDown(300);
await sleep(500);
delEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
}
if (!delEl) return false;
await driver.tapElement(delEl);
await sleep(2000);
const confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
if (confirmEl) {
await driver.tapElement(confirmEl);
await sleep(1500);
}
return true;
}
// iOS
await driver.tap(347, 48);
await sleep(1500);
const manageEl = await driver.findElementRaw('name', 'Manage Scenes');
if (!manageEl) return false;
await driver.tapElement(manageEl);
await sleep(2000);
const source = await driver.getSource();
if (!source.includes(sceneName)) return false;
const myScenesEl = await driver.findElementRaw('name', 'My Scenes');
if (!myScenesEl) return false;
const msRect = await driver.getElementRect(myScenesEl);
await driver.tap(195, msRect.y + msRect.height + 40);
await sleep(2000);
const editSource = await driver.getSource();
if (!editSource.includes('Edit Scene')) return false;
const delEl = await driver.findElementRaw('name', 'Delete');
if (!delEl) return false;
await driver.tapElement(delEl);
await sleep(1500);
const delConfirm = await driver.findElementRaw('name', 'Delete');
if (delConfirm) {
await driver.tapElement(delConfirm);
await sleep(1500);
}
return true;
}
export async function verifySceneInLogs(driver: DeviceDriver, sceneName: string): Promise<boolean> {
if (driver.platform === 'android') {
const navigated = await navigateToManageScenes(driver);
if (!navigated) return false;
await sleep(1500);
// 场景日志入口:Manage Scenes 右上角有两个图标(左=Create,右=日志)。id 会命中 Create,故直接点最右图标坐标(1080×2280 约 1010,175)→ 菜单 "Scene Logs"。
await driver.tap(1010, 175);
await sleep(1500);
const logsEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Scene Logs")');
if (!logsEl) { console.log('未进入 Scene Logs 菜单'); return false; }
await driver.tapElement(logsEl);
await sleep(2500);
const source = await driver.getSource();
const has = source.includes(sceneName); // 日志页含该场景名 = 已执行(记录)
console.log(`Scene Logs 有执行记录(${sceneName}): ${has}`);
return has;
}
// iOS: navigate to Manage Scenes
await driver.tap(347, 48);
await sleep(1500);
const manageEl = await driver.findElementRaw('name', 'Manage Scenes');
if (!manageEl) return false;
await driver.tapElement(manageEl);
await sleep(2000);
// Find the scene and tap to enter detail
const sceneEl = await driver.findElementRaw('predicate string', `name CONTAINS "${sceneName}"`);
if (!sceneEl) return false;
await driver.tapElement(sceneEl);
await sleep(2000);
// Tap the top-right button to see execution history/status
const navBar = await driver.findElementRaw('class name', 'XCUIElementTypeNavigationBar');
if (navBar) {
const navRect = await driver.getElementRect(navBar);
// Top-right corner of navigation bar
await driver.tap(navRect.x + navRect.width - 30, navRect.y + navRect.height / 2);
await sleep(2000);
}
const source = await driver.getSource();
return source.includes('Success') || source.includes('success') || source.includes('Executed') || source.includes(sceneName);
}
/**
* 通用动作选择(场景/自动化共用):在"Add action 后的动作来源页"点 Smart Devices → 找设备 → 选动作。
* 兼容 Bot 两模式(按压=Presses once / 开关=Turns off/Turns on)与其它设备动作。
* 调用前:已点过 "Add action"(处于动作来源选择页)。
* @param actionPref 优先尝试的动作文本(如 'Turns off' / 'Presses once');找不到按通用顺序兜底。
*/
export async function selectSmartDeviceAction(
driver: DeviceDriver,
deviceKeyword: string,
actionPref?: string
): Promise<boolean> {
const tap = async (sel: string, val: string) => {
const el = await driver.findElementRaw(sel, val);
if (el) { await driver.tapElement(el); return true; }
return false;
};
if (!(await tap('-android uiautomator', 'new UiSelector().text("Smart Devices")'))) {
console.log('FAIL: no Smart Devices'); return false;
}
await sleep(2000);
// 找设备(滚动)
let dev: string | null = null;
for (let i = 0; i < 12 && !dev; i++) {
dev = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${deviceKeyword}")`);
if (!dev) { await driver.scrollDown(400); await sleep(700); }
}
if (!dev) { console.log(`FAIL: no device ${deviceKeyword}`); return false; }
await driver.tapElement(dev);
await sleep(2000);
// 选动作:优先 actionPref,再兜底 Presses once / Turns off / Turns on
const candidates = [actionPref, 'Presses once', 'Turns off', 'Turns on'].filter(Boolean) as string[];
for (const a of candidates) {
if (await tap('-android uiautomator', `new UiSelector().text("${a}")`)) {
console.log(`选动作: ${a}`);
await sleep(2000);
return true;
}
}
console.log('FAIL: no action option'); return false;
}