AI_UIAutomation/tests/aihub/aihub_aicam.test.ts

1126 lines
42 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 { describe, it, beforeAll, afterAll, beforeEach, afterEach, expect } from 'vitest';
import { DeviceDriver } from '../../drivers/types';
import { createDriver } from '../../drivers/factory';
import { AICAM_LOCATORS } from '../../locators/aicam-locators';
import { TestReporter } from '../../utils/test-reporter';
import { sleep } from '../../utils/common';
import * as dotenv from 'dotenv';
import * as path from 'path';
import { robustBeforeAll, robustBeforeEach } from './aihub-setup.helper';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const AIHUB_NAME = process.env.AIHUB_NAME || 'AI Hub 6C';
describe('AI Hub 功能页 - 全主功能覆盖', () => {
let driver: DeviceDriver;
let reporter: TestReporter;
// 平台自适应坐标
const isAndroid = () => driver.platform === 'android';
const BACK_BTN = () => isAndroid() ? { x: 0, y: 0 } : { x: 39, y: 70 }; // Android uses goBack()
const BOTTOM_LEFT_BTN = () => isAndroid() ? { x: 318, y: 2059 } : { x: 115, y: 784 };
const BOTTOM_RIGHT_BTN = () => isAndroid() ? { x: 763, y: 2059 } : { x: 275, y: 784 };
const SETTINGS_ICON = () => isAndroid() ? { x: 999, y: 175 } : { x: 361, y: 70 };
const SEARCH_BAR = () => isAndroid() ? { x: 540, y: 326 } : { x: 195, y: 121 };
const FILTER_ICON = () => isAndroid() ? { x: 905, y: 189 } : { x: 327, y: 70 };
const MORE_ICON = () => isAndroid() ? { x: 991, y: 189 } : { x: 358, y: 70 };
const EVENT_TAG_Y = () => isAndroid() ? 621 : 230;
const SWIPE_CENTER_X = () => isAndroid() ? 540 : 195;
beforeAll(async () => {
driver = createDriver();
await driver.createSession();
await robustBeforeAll(driver);
reporter = new TestReporter('AIHub_FunctionPage', driver.platform.toUpperCase());
});
afterAll(async () => {
reporter.generate();
await driver.destroySession();
});
beforeEach(async () => {
await robustBeforeEach(driver);
});
afterEach(async () => {
const timeout = (ms: number, fn: () => Promise<void>) =>
Promise.race([fn(), sleep(ms)]);
try {
await timeout(15000, async () => {
const source = await driver.getSource();
if (source.includes('Try OpenClaw') || source.includes('AI Routines')) return;
if (source.includes('Manage Cameras') || source.includes('Add New Device')) {
await goBackFromSubpage();
return;
}
if (source.includes('Today') && source.includes('AI Events')) return;
const isHome = (source.includes('Add') && source.includes('More') && source.includes('Home'))
|| source.includes('主页');
if (isHome) return;
for (let i = 0; i < 3; i++) {
await goBackFromSubpage();
const s = await driver.getSource();
if (s.includes('Try OpenClaw') || s.includes('AI Routines')) return;
const onHome = (s.includes('Add') && s.includes('More') && s.includes('Home'))
|| s.includes('主页');
if (onHome) return;
}
});
} catch {}
});
async function captureScreenshot(): Promise<string | undefined> {
try { return await driver.screenshot(); } catch { return undefined; }
}
async function dismissPopup(): Promise<void> {
await driver.dismissPopupIfPresent();
const gotIt = await driver.findElement(AICAM_LOCATORS.gotItButton);
if (gotIt) {
await driver.tapElement(gotIt);
await sleep(1000);
}
}
async function navigateToHubFromHome(): Promise<void> {
// Step 1: Get to homepage using driver's robust goBackToHomepage
const source = await driver.getSource();
if (source.includes('Try OpenClaw') || source.includes('AI Routines')) {
await dismissPopup();
return;
}
const isHome = (source.includes('Add') && source.includes('More') && source.includes('Home'))
|| source.includes('主页') || source.includes('自动化');
if (!isHome) {
await driver.goBackToHomepage();
await sleep(1000);
}
await dismissPopup();
// Step 2: Find and tap Hub card
if (isAndroid()) {
// Use findDeviceCard which leverages UiScrollable.scrollIntoView
const hubEl = await (driver as any).findDeviceCard(AIHUB_NAME);
if (!hubEl) throw new Error(`找不到或无法点击 ${AIHUB_NAME} 卡片`);
// Tap near the element's text (avoid camera stream overlay)
const rect = await driver.getElementRect(hubEl);
await driver.tap(rect.x + 100, rect.y + 30);
await sleep(6000);
let s = await driver.getSource();
if (s.includes('Try OpenClaw') || (s.includes('Cameras') && s.includes('AI Events'))) {
await dismissPopup();
return;
}
// Retry with tapElement
await driver.tapElement(hubEl);
await sleep(5000);
s = await driver.getSource();
if (s.includes('Try OpenClaw') || (s.includes('Cameras') && s.includes('AI Events'))) {
await dismissPopup();
return;
}
throw new Error(`找不到或无法点击 ${AIHUB_NAME} 卡片`);
}
// iOS path: manual scroll loop
const maxScroll = 5;
for (let i = 0; i <= maxScroll; i++) {
let hubEl: string | null = null;
hubEl = await driver.findElementRaw('predicate string', `name CONTAINS "${AIHUB_NAME}" AND type == "XCUIElementTypeCell"`);
if (!hubEl) {
hubEl = await driver.findElementRaw('predicate string', `label CONTAINS "${AIHUB_NAME}"`);
}
if (hubEl) {
const rect = await driver.getElementRect(hubEl);
await driver.tap(rect.x + rect.width / 2, rect.y + rect.height / 4);
await sleep(5000);
let s = await driver.getSource();
if (s.includes('Try OpenClaw') || (s.includes('Cameras') && s.includes('AI Events'))) {
await dismissPopup();
return;
}
await driver.clickElement(hubEl);
await sleep(5000);
s = await driver.getSource();
if (s.includes('Try OpenClaw') || (s.includes('Cameras') && s.includes('AI Events'))) {
await dismissPopup();
return;
}
if (i < maxScroll) {
await driver.swipe(195, 650, 195, 450, 0.3);
await sleep(1500);
}
continue;
}
if (i < maxScroll) {
await driver.swipe(195, 650, 195, 300, 0.5);
await sleep(1500);
}
}
throw new Error(`找不到或无法点击 ${AIHUB_NAME} 卡片`);
}
async function ensureOnHubPage(): Promise<void> {
const source = await driver.getSource();
if (source.includes('Try OpenClaw') || (source.includes('Cameras') && source.includes('AI Events'))) {
return;
}
// If on AI Events, just go back once
if (source.includes('Today') && source.includes('AI Events')) {
await goBackFromSubpage();
const s = await driver.getSource();
if (s.includes('Try OpenClaw') || s.includes('Cameras')) return;
}
// Use goBackToHomepage then navigate to Hub
await driver.goBackToHomepage();
await sleep(1000);
await navigateToHubFromHome();
}
async function findCameraCardRect(): Promise<{ x: number; y: number; width: number; height: number } | null> {
let el: string | null = null;
if (driver.platform === 'ios') {
el = await driver.findElementRaw('predicate string', 'label CONTAINS "摄像机" OR label CONTAINS "Plus 2K"');
} else {
el = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Plus 2K")');
if (!el) {
el = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("摄像机")');
}
if (!el) {
el = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Cam")');
}
}
if (!el) return null;
return driver.getElementRect(el);
}
async function findViewToggleBtn(): Promise<void> {
let camerasEl: string | null = null;
if (driver.platform === 'ios') {
camerasEl = await driver.findElementRaw('predicate string', 'label == "Cameras" AND type == "XCUIElementTypeStaticText"');
} else {
camerasEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Cameras")');
}
if (camerasEl) {
const rect = await driver.getElementRect(camerasEl);
if (isAndroid()) {
// Toggle button is at the far right of the Cameras row
await driver.tap(1010, rect.y + Math.floor(rect.height / 2));
} else {
await driver.tap(rect.x + 340, rect.y + Math.floor(rect.height / 2));
}
} else {
if (isAndroid()) {
await driver.tap(1010, 810);
} else {
await driver.tap(365, 299);
}
}
}
async function goBackFromSubpage(): Promise<void> {
if (isAndroid()) {
await driver.goBack();
} else {
await driver.tap(BACK_BTN().x, BACK_BTN().y);
}
await sleep(3000);
}
async function enterAIEvents(): Promise<void> {
await ensureOnHubPage();
// Scroll down if needed to find AI Events
for (let attempt = 0; attempt < 3; attempt++) {
const aiEventsEl = await driver.findElement(AICAM_LOCATORS.aiEvents);
if (aiEventsEl) {
await driver.tapElement(aiEventsEl);
await sleep(5000);
const source = await driver.getSource();
if (source.includes('Today') || source.includes('AI Events')) return;
}
// Platform-specific fallback
let el: string | null = null;
if (isAndroid()) {
el = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("AI Events")');
} else {
el = await driver.findElementRaw('predicate string', 'label == "AI Events"');
}
if (el) {
await driver.tapElement(el);
await sleep(5000);
const source = await driver.getSource();
if (source.includes('Today') || source.includes('AI Events')) return;
}
await driver.scrollDown(200);
await sleep(1000);
}
throw new Error('未能进入 AI Events 页面');
}
async function ensureOnAIEvents(): Promise<void> {
const source = await driver.getSource();
if (source.includes('Today') && source.includes('AI Events')) return;
// 可能有弹出菜单遮挡,先尝试关闭
if (source.includes('Change View') || source.includes('Delete All')) {
await driver.tap(SWIPE_CENTER_X(), isAndroid() ? 1350 : 500);
await sleep(1500);
const s = await driver.getSource();
if (s.includes('Today') && s.includes('AI Events')) return;
}
// 如果在Hub功能页直接进入AI Events
if (source.includes('Try OpenClaw') || source.includes('AI Routines')) {
await enterAIEvents();
return;
}
// 其他页面:先回首页再导航
await driver.goBackToHomepage();
await sleep(1000);
await enterAIEvents();
}
// ============================================================
// 一、功能页入口 & 布局验证
// ============================================================
it('1.1 从首页进入AI Hub功能页', { timeout: 90000 }, async () => {
const start = Date.now();
try {
await navigateToHubFromHome();
const source = await driver.getSource();
expect(source).toContain('Try OpenClaw');
expect(source).toContain('AI Events');
expect(source).toContain('Cameras');
reporter.record('从首页进入AI Hub功能页', 'PASS', Date.now() - start, '成功进入功能页');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('从首页进入AI Hub功能页', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('1.2 功能页展示所有主要入口', async () => {
const start = Date.now();
try {
await ensureOnHubPage();
const source = await driver.getSource();
expect(source).toContain('Try OpenClaw');
expect(source).toContain('AI Routines');
expect(source).toContain('AI Events');
expect(source).toContain('Cameras');
reporter.record('功能页展示所有主要入口', 'PASS', Date.now() - start, 'OpenClaw/AI Routines/AI Events/Cameras 均可见');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('功能页展示所有主要入口', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// ============================================================
// 二、Camera 卡片交互(大卡片模式)
// ============================================================
it('2.1 Camera卡片展示摄像头实时画面', async () => {
const start = Date.now();
try {
await ensureOnHubPage();
const source = await driver.getSource();
const hasCameraInfo = source.includes('摄像机') ||
source.includes('Plus 2K') ||
source.includes('Cam');
expect(hasCameraInfo).toBe(true);
reporter.record('Camera卡片展示摄像头实时画面', 'PASS', Date.now() - start, '摄像头画面卡片显示正常');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('Camera卡片展示摄像头实时画面', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('2.2 点击Camera卡片中部跳转回放页', async () => {
const start = Date.now();
try {
await ensureOnHubPage();
const rect = await findCameraCardRect();
if (!rect) throw new Error('找不到Camera卡片元素');
// 点击卡片中间偏左区域(避开右下角按钮)
await driver.tap(rect.x + Math.floor(rect.width / 3), rect.y + Math.floor(rect.height / 2));
await sleep(5000);
const source = await driver.getSource();
const isPlaybackPage = (source.includes('No playbacks') ||
source.includes('quiet today') ||
source.includes('Today')) &&
!source.includes('Try OpenClaw');
expect(isPlaybackPage).toBe(true);
await goBackFromSubpage();
await sleep(2000);
const backSource = await driver.getSource();
expect(backSource).toContain('Cameras');
reporter.record('点击Camera卡片中部跳转回放页', 'PASS', Date.now() - start, '跳转回放页成功');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('点击Camera卡片中部跳转回放页', 'FAIL', Date.now() - start, e.message, ss);
await goBackFromSubpage();
throw e;
}
});
it('2.3 点击Camera右下角按钮进入Camera功能页验证拉流', async () => {
const start = Date.now();
try {
await ensureOnHubPage();
const rect = await findCameraCardRect();
if (!rect) throw new Error('找不到Camera卡片元素');
// 点击卡片右下角区域
await driver.tap(rect.x + rect.width - 30, rect.y + rect.height - 20);
await sleep(5000);
const source = await driver.getSource();
const leftHub = !source.includes('Try OpenClaw');
const hasDeviceOnline = source.includes('Device online') || source.includes('online');
const hasCameraTitle = source.includes('摄像机') || source.includes('Plus 2K');
expect(leftHub).toBe(true);
expect(hasCameraTitle).toBe(true);
expect(hasDeviceOnline).toBe(true);
await goBackFromSubpage();
await sleep(2000);
const backSource = await driver.getSource();
expect(backSource).toContain('Cameras');
reporter.record('点击Camera右下角按钮进入Camera功能页验证拉流', 'PASS', Date.now() - start, 'Camera功能页拉流正常(Device online)');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('点击Camera右下角按钮进入Camera功能页验证拉流', 'FAIL', Date.now() - start, e.message, ss);
await goBackFromSubpage();
throw e;
}
});
// ============================================================
// 三、Camera 视图切换 & 网格模式交互
// ============================================================
it('3.1 Cameras视图切换按钮切换到网格模式', async () => {
const start = Date.now();
try {
await ensureOnHubPage();
const beforeSource = await driver.getSource();
await findViewToggleBtn();
await sleep(2000);
const afterSource = await driver.getSource();
expect(afterSource).toContain('Cameras');
expect(afterSource).toContain('Try OpenClaw');
expect(beforeSource !== afterSource).toBe(true);
reporter.record('Cameras视图切换按钮切换到网格模式', 'PASS', Date.now() - start, '视图切换到网格模式成功');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('Cameras视图切换按钮切换到网格模式', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('3.2 网格模式点击Camera卡片进入回放页', async () => {
const start = Date.now();
try {
const source = await driver.getSource();
if (!source.includes('Try OpenClaw')) {
await ensureOnHubPage();
await findViewToggleBtn();
await sleep(2000);
}
const rect = await findCameraCardRect();
if (!rect) throw new Error('网格模式找不到Camera卡片');
// 点击卡片中心偏左(避开右侧按钮)
await driver.tap(rect.x + Math.floor(rect.width / 3), rect.y + Math.floor(rect.height / 2));
await sleep(5000);
const navSource = await driver.getSource();
const isPlaybackPage = (navSource.includes('No playbacks') ||
navSource.includes('quiet today') ||
navSource.includes('Today')) &&
!navSource.includes('Try OpenClaw');
expect(isPlaybackPage).toBe(true);
await goBackFromSubpage();
await sleep(2000);
reporter.record('网格模式点击Camera卡片进入回放页', 'PASS', Date.now() - start, '网格模式回放页跳转成功');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('网格模式点击Camera卡片进入回放页', 'FAIL', Date.now() - start, e.message, ss);
await goBackFromSubpage();
throw e;
}
});
it('3.3 网格模式点击Camera右上角进入创建自动化', async () => {
const start = Date.now();
try {
const source = await driver.getSource();
if (!source.includes('Try OpenClaw')) {
await ensureOnHubPage();
await findViewToggleBtn();
await sleep(2000);
}
const rect = await findCameraCardRect();
if (!rect) throw new Error('网格模式找不到Camera卡片');
// 点击卡片右上角(创建自动化图标)
await driver.tap(rect.x + rect.width - 25, rect.y + 25);
await sleep(3000);
const navSource = await driver.getSource();
const isAutomationPage = navSource.includes('Add condition') ||
navSource.includes('Detects') ||
navSource.includes('condition') ||
navSource.includes('Automation');
expect(isAutomationPage).toBe(true);
await goBackFromSubpage();
await sleep(2000);
reporter.record('网格模式点击Camera右上角进入创建自动化', 'PASS', Date.now() - start, '创建自动化页面(Add condition)打开成功');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('网格模式点击Camera右上角进入创建自动化', 'FAIL', Date.now() - start, e.message, ss);
await goBackFromSubpage();
throw e;
}
});
it('3.4 网格模式点击Camera右下角进入Camera功能页验证拉流', async () => {
const start = Date.now();
try {
const source = await driver.getSource();
if (!source.includes('Try OpenClaw')) {
await ensureOnHubPage();
await findViewToggleBtn();
await sleep(2000);
}
const rect = await findCameraCardRect();
if (!rect) throw new Error('网格模式找不到Camera卡片');
// 点击卡片右下角
await driver.tap(rect.x + rect.width - 20, rect.y + rect.height - 20);
await sleep(5000);
const navSource = await driver.getSource();
const leftHub = !navSource.includes('Try OpenClaw');
const hasDeviceOnline = navSource.includes('Device online') || navSource.includes('online');
const hasCameraTitle = navSource.includes('摄像机') || navSource.includes('Plus 2K');
expect(leftHub).toBe(true);
expect(hasCameraTitle).toBe(true);
expect(hasDeviceOnline).toBe(true);
await goBackFromSubpage();
await sleep(2000);
reporter.record('网格模式点击Camera右下角进入Camera功能页验证拉流', 'PASS', Date.now() - start, '网格模式Camera功能页拉流正常');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('网格模式点击Camera右下角进入Camera功能页验证拉流', 'FAIL', Date.now() - start, e.message, ss);
await goBackFromSubpage();
throw e;
}
});
it('3.5 网格模式切换回大卡片模式', async () => {
const start = Date.now();
try {
const source = await driver.getSource();
if (!source.includes('Try OpenClaw')) {
await ensureOnHubPage();
}
await findViewToggleBtn();
await sleep(2000);
const afterSource = await driver.getSource();
expect(afterSource).toContain('Cameras');
expect(afterSource).toContain('Try OpenClaw');
reporter.record('网格模式切换回大卡片模式', 'PASS', Date.now() - start, '切换回大卡片模式成功');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('网格模式切换回大卡片模式', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// ============================================================
// 四、创建自动化 & 左滑删除
// ============================================================
it('4.1 Camera创建AI自动化', { timeout: 120000 }, async () => {
const start = Date.now();
try {
await ensureOnHubPage();
// 切换到网格模式
await findViewToggleBtn();
await sleep(2000);
// 找到camera卡片并点击右上角创建自动化
const rect = await findCameraCardRect();
if (!rect) throw new Error('找不到Camera卡片');
await driver.tap(rect.x + rect.width - 25, rect.y + 25);
await sleep(3000);
let source = await driver.getSource();
if (!source.includes('Detects') && !source.includes('condition')) {
throw new Error('未能进入创建自动化页面');
}
// Step 1: 选择触发条件 "Detects objects (AI Hub)"
let detectObj: string | null = null;
if (driver.platform === 'ios') {
detectObj = await driver.findElementRaw('predicate string', 'label CONTAINS "Detects objects"');
if (!detectObj) {
detectObj = await driver.findElementRaw('predicate string', 'label CONTAINS "Detects"');
}
} else {
detectObj = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Detects")');
}
if (!detectObj) throw new Error('找不到 Detects objects 条件选项');
await driver.tapElement(detectObj);
await sleep(3000);
// Step 2: 选择检测对象类型Person/Pet/Package/Vehicle
source = await driver.getSource();
if (source.includes('Person') || source.includes('Pet') || source.includes('Package') || source.includes('Vehicle')) {
// 选择 Person 作为检测对象
let personEl: string | null = null;
if (driver.platform === 'ios') {
personEl = await driver.findElementRaw('predicate string', 'label == "Person" OR label CONTAINS "Person"');
} else {
personEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Person")');
}
if (personEl) {
await driver.tapElement(personEl);
await sleep(1500);
}
}
// Step 3: 点击 Next/Done 完成条件配置
source = await driver.getSource();
let nextBtn: string | null = null;
if (driver.platform === 'ios') {
nextBtn = await driver.findElementRaw('predicate string',
'label == "Next" OR label == "Done" OR label == "Confirm" OR label == "Save"');
} else {
for (const text of ['Next', 'Done', 'Confirm', 'Save']) {
nextBtn = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${text}")`);
if (nextBtn) break;
}
}
if (nextBtn) {
await driver.tapElement(nextBtn);
await sleep(3000);
}
// Step 4: 如果进入动作配置页Add action选择通知或跳过
source = await driver.getSource();
if (source.includes('Add action') || source.includes('Action') || source.includes('Notification')) {
// 尝试选择 Send notification 作为动作
let notifyEl: string | null = null;
if (driver.platform === 'ios') {
notifyEl = await driver.findElementRaw('predicate string',
'label CONTAINS "Notification" OR label CONTAINS "Send"');
} else {
notifyEl = await driver.findElementRaw('-android uiautomator',
'new UiSelector().textContains("Notification")');
}
if (notifyEl) {
await driver.tapElement(notifyEl);
await sleep(2000);
}
// 点击 Save/Done 完成动作配置
let saveAction: string | null = null;
if (driver.platform === 'ios') {
saveAction = await driver.findElementRaw('predicate string',
'label == "Save" OR label == "Done" OR label == "Next"');
} else {
for (const text of ['Save', 'Done', 'Next']) {
saveAction = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${text}")`);
if (saveAction) break;
}
}
if (saveAction) {
await driver.tapElement(saveAction);
await sleep(3000);
}
}
// Step 5: 最终保存自动化(可能还有一层 Save 确认)
source = await driver.getSource();
if (source.includes('Save') && !source.includes('Try OpenClaw')) {
let finalSave: string | null = null;
if (driver.platform === 'ios') {
finalSave = await driver.findElementRaw('predicate string', 'label == "Save"');
} else {
finalSave = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
}
if (finalSave) {
await driver.tapElement(finalSave);
await sleep(3000);
}
}
// 验证回到Hub功能页或AI Routines中能看到新建的自动化
source = await driver.getSource();
const created = source.includes('AI Routines') || source.includes('Detect')
|| source.includes('Person') || source.includes('Try OpenClaw')
|| source.includes('Routine');
expect(created).toBe(true);
reporter.record('Camera创建AI自动化', 'PASS', Date.now() - start, '自动化创建流程完成(Detects objects → Person)');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('Camera创建AI自动化', 'FAIL', Date.now() - start, e.message, ss);
// 确保回到可恢复状态
for (let i = 0; i < 5; i++) {
await goBackFromSubpage();
const s = await driver.getSource();
if (s.includes('Try OpenClaw') || (s.includes('Add') && s.includes('More'))) break;
}
throw e;
}
});
it('4.2 AI Routines左滑删除自动化', { timeout: 90000 }, async () => {
const start = Date.now();
try {
await ensureOnHubPage();
// 进入 AI Routines 列表
let routinesEl: string | null = null;
if (driver.platform === 'ios') {
routinesEl = await driver.findElementRaw('predicate string', 'label == "AI Routines"');
} else {
routinesEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("AI Routines")');
}
if (!routinesEl) {
await driver.scrollDown(200);
await sleep(1000);
if (driver.platform === 'ios') {
routinesEl = await driver.findElementRaw('predicate string', 'label == "AI Routines"');
} else {
routinesEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("AI Routines")');
}
}
if (!routinesEl) throw new Error('找不到 AI Routines 入口');
await driver.tapElement(routinesEl);
await sleep(4000);
let source = await driver.getSource();
const hasRoutine = source.includes('Detect') || source.includes('Person')
|| source.includes('Motion') || source.includes('Routine')
|| source.includes('routine');
if (!hasRoutine) {
console.log('AI Routines列表为空跳过删除');
reporter.record('AI Routines左滑删除', 'PASS', Date.now() - start, 'Routines列表为空(无需删除)');
await goBackFromSubpage();
return;
}
// 左滑第一条 routine
await driver.swipe(isAndroid() ? 970 : 350, isAndroid() ? 1080 : 400, isAndroid() ? 140 : 50, isAndroid() ? 1080 : 400, 0.3);
await sleep(2000);
// 点击 Delete 按钮
let deleteBtn: string | null = null;
if (driver.platform === 'ios') {
deleteBtn = await driver.findElementRaw('predicate string', 'label == "Delete" OR label == "删除"');
} else {
deleteBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
}
if (deleteBtn) {
await driver.tapElement(deleteBtn);
await sleep(2000);
// 可能有确认弹窗
source = await driver.getSource();
if (source.includes('Confirm') || source.includes('OK') || source.includes('Yes')) {
let confirmBtn: string | null = null;
if (driver.platform === 'ios') {
confirmBtn = await driver.findElementRaw('predicate string',
'label == "Confirm" OR label == "OK" OR label == "Yes" OR label == "Delete"');
} else {
for (const text of ['Confirm', 'OK', 'Yes', 'Delete']) {
confirmBtn = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${text}")`);
if (confirmBtn) break;
}
}
if (confirmBtn) {
await driver.tapElement(confirmBtn);
await sleep(2000);
}
}
reporter.record('AI Routines左滑删除', 'PASS', Date.now() - start, '自动化左滑删除成功');
} else {
reporter.record('AI Routines左滑删除', 'FAIL', Date.now() - start, '左滑后未出现Delete按钮');
throw new Error('左滑后未出现Delete按钮');
}
await goBackFromSubpage();
await sleep(2000);
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('AI Routines左滑删除', 'FAIL', Date.now() - start, e.message, ss);
await goBackFromSubpage();
throw e;
}
});
// ============================================================
// 五、底部按钮 - 摄像头管理 & 事件回放
// ============================================================
it('5.1 底部左按钮进入摄像头管理', { timeout: 120000 }, async () => {
const start = Date.now();
try {
await ensureOnHubPage();
await driver.tap(BOTTOM_LEFT_BTN().x, BOTTOM_LEFT_BTN().y);
await sleep(5000);
const source = await driver.getSource();
const isCameraManagement = source.includes('Manage Cameras') ||
source.includes('Add New Device') ||
source.includes('Add Third-party Camera');
expect(isCameraManagement).toBe(true);
await goBackFromSubpage();
await sleep(2000);
const backSource = await driver.getSource();
expect(backSource).toContain('Cameras');
reporter.record('底部左按钮进入摄像头管理', 'PASS', Date.now() - start, '进入Manage Cameras页成功');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('底部左按钮进入摄像头管理', 'FAIL', Date.now() - start, e.message, ss);
await goBackFromSubpage();
throw e;
}
});
it('5.2 底部右按钮进入事件回放', { timeout: 120000 }, async () => {
const start = Date.now();
try {
await ensureOnHubPage();
await driver.tap(BOTTOM_RIGHT_BTN().x, BOTTOM_RIGHT_BTN().y);
await sleep(5000);
const source = await driver.getSource();
const isPlaybackPage = (source.includes('No playbacks') ||
source.includes('quiet today') ||
source.includes('Today')) &&
!source.includes('Try OpenClaw');
expect(isPlaybackPage).toBe(true);
await goBackFromSubpage();
await sleep(2000);
const backSource = await driver.getSource();
expect(backSource).toContain('Cameras');
reporter.record('底部右按钮进入事件回放', 'PASS', Date.now() - start, '进入事件回放页成功');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('底部右按钮进入事件回放', 'FAIL', Date.now() - start, e.message, ss);
await goBackFromSubpage();
throw e;
}
});
// ============================================================
// 六、AI EventsAI事件分析
// ============================================================
it('6.1 进入AI Events页面', { timeout: 120000 }, async () => {
const start = Date.now();
try {
await enterAIEvents();
const source = await driver.getSource();
expect(source).toContain('AI Events');
expect(source).toContain('Today');
reporter.record('进入AI Events页面', 'PASS', Date.now() - start, 'AI Events页面正常展示');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('进入AI Events页面', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('6.2 AI Events展示事件类型标签和计数', async () => {
const start = Date.now();
try {
await ensureOnAIEvents();
const source = await driver.getSource();
expect(source).toContain('Today');
expect(source).toContain('0');
reporter.record('AI Events展示事件类型标签和计数', 'PASS', Date.now() - start, '事件类型标签栏正常');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('AI Events展示事件类型标签和计数', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('6.3 点击搜索栏激活AI搜索', { timeout: 90000 }, async () => {
const start = Date.now();
try {
await ensureOnAIEvents();
// Try element-based tap first (more reliable than coordinate)
const searchEl = await driver.findElement(AICAM_LOCATORS.searchBar);
if (searchEl) {
await driver.tapElement(searchEl);
} else {
// Fallback: search bar is typically below nav bar + event tags area
await driver.tap(SEARCH_BAR().x, SEARCH_BAR().y);
}
await sleep(3000);
const source = await driver.getSource();
const hasSearchActive = source.includes('search') ||
source.includes('Search') ||
source.includes('Cancel') ||
source.includes('XCUIElementTypeTextField') ||
source.includes('keyboard');
expect(hasSearchActive).toBe(true);
// Dismiss keyboard/search - tap Cancel if visible, otherwise tap back
if (source.includes('Cancel')) {
let cancelEl: string | null = null;
if (isAndroid()) {
cancelEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Cancel")');
} else {
cancelEl = await driver.findElementRaw('predicate string', 'label == "Cancel"');
}
if (cancelEl) {
await driver.tapElement(cancelEl);
await sleep(1500);
} else {
await goBackFromSubpage();
}
} else {
await goBackFromSubpage();
}
await sleep(1000);
reporter.record('点击搜索栏激活AI搜索', 'PASS', Date.now() - start, 'AI搜索栏激活成功');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('点击搜索栏激活AI搜索', 'FAIL', Date.now() - start, e.message, ss);
// Try to dismiss any active search state
try {
let cancelEl: string | null = null;
if (isAndroid()) {
cancelEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Cancel")');
} else {
cancelEl = await driver.findElementRaw('predicate string', 'label == "Cancel"');
}
if (cancelEl) await driver.tapElement(cancelEl);
else await goBackFromSubpage();
} catch { await goBackFromSubpage(); }
throw e;
}
});
it('6.4 点击筛选按钮打开筛选页', async () => {
const start = Date.now();
try {
await ensureOnAIEvents();
await driver.tap(FILTER_ICON().x, FILTER_ICON().y);
await sleep(3000);
const source = await driver.getSource();
const hasFilterContent = source.includes('Filter') ||
source.includes('Start time') ||
source.includes('End time') ||
source.includes('Profile') ||
source.includes('Event') ||
source.includes('Save');
expect(hasFilterContent).toBe(true);
await goBackFromSubpage();
await sleep(1000);
reporter.record('点击筛选按钮打开筛选页', 'PASS', Date.now() - start, '筛选页面打开成功');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('点击筛选按钮打开筛选页', 'FAIL', Date.now() - start, e.message, ss);
await goBackFromSubpage();
throw e;
}
});
it('6.5 点击更多按钮弹出菜单', async () => {
const start = Date.now();
try {
await ensureOnAIEvents();
await driver.tap(MORE_ICON().x, MORE_ICON().y);
await sleep(3000);
const source = await driver.getSource();
const hasMenu = source.includes('Change View') ||
source.includes('Delete') ||
source.includes('View');
expect(hasMenu).toBe(true);
// 关闭菜单 - 点击页面中间空白区域
await driver.tap(SWIPE_CENTER_X(), isAndroid() ? 1350 : 500);
await sleep(2000);
// 验证菜单已关闭
const afterSource = await driver.getSource();
if (afterSource.includes('Change View') || afterSource.includes('Delete')) {
await driver.tap(SWIPE_CENTER_X(), isAndroid() ? 1080 : 400);
await sleep(1000);
}
reporter.record('点击更多按钮弹出菜单', 'PASS', Date.now() - start, '更多菜单弹出成功(Change View/Delete)');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('点击更多按钮弹出菜单', 'FAIL', Date.now() - start, e.message, ss);
await driver.tap(SWIPE_CENTER_X(), isAndroid() ? 1350 : 500);
throw e;
}
});
it('6.6 事件类型标签横向滑动', { timeout: 90000 }, async () => {
const start = Date.now();
try {
await ensureOnAIEvents();
// Swipe left on event type tags (shorter, centered swipe to avoid edge gestures)
const tagY = EVENT_TAG_Y();
const swipeRight = isAndroid() ? 830 : 300;
const swipeLeft = isAndroid() ? 270 : 100;
await driver.swipe(swipeRight, tagY, swipeLeft, tagY, 0.4);
await sleep(2000);
let source = await driver.getSource();
// After horizontal swipe on tags, page should still be AI Events
let stillOnPage = source.includes('AI Events') || source.includes('Today');
if (!stillOnPage) {
// Swipe may have navigated away, try to go back
await goBackFromSubpage();
await sleep(2000);
source = await driver.getSource();
stillOnPage = source.includes('AI Events') || source.includes('Today');
}
expect(stillOnPage).toBe(true);
// Swipe back (right)
await driver.swipe(swipeLeft, tagY, swipeRight, tagY, 0.4);
await sleep(1500);
reporter.record('事件类型标签横向滑动', 'PASS', Date.now() - start, '标签栏横向滑动正常');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('事件类型标签横向滑动', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('6.7 事件列表下拉刷新', { timeout: 60000 }, async () => {
const start = Date.now();
try {
await ensureOnAIEvents();
// Pull down to refresh
await driver.swipe(SWIPE_CENTER_X(), isAndroid() ? 1080 : 400, SWIPE_CENTER_X(), isAndroid() ? 1800 : 650, 0.5);
await sleep(4000);
const source = await driver.getSource();
const stillOnPage = source.includes('AI Events') || source.includes('Today');
expect(stillOnPage).toBe(true);
reporter.record('事件列表下拉刷新', 'PASS', Date.now() - start, '下拉刷新正常');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('事件列表下拉刷新', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('6.8 从AI Events返回Hub功能页', { timeout: 60000 }, async () => {
const start = Date.now();
try {
await ensureOnAIEvents();
await goBackFromSubpage();
await sleep(2000);
const source = await driver.getSource();
const backToHub = source.includes('Try OpenClaw') || source.includes('Cameras') || source.includes('AI Routines');
expect(backToHub).toBe(true);
reporter.record('从AI Events返回Hub功能页', 'PASS', Date.now() - start, '返回功能页成功');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('从AI Events返回Hub功能页', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// ============================================================
// 七、设置入口
// ============================================================
it('7.1 右上角设置按钮进入设置页', { timeout: 60000 }, async () => {
const start = Date.now();
try {
await ensureOnHubPage();
await driver.tap(SETTINGS_ICON().x, SETTINGS_ICON().y);
await sleep(3000);
const source = await driver.getSource();
const leftHubPage = !source.includes('Try OpenClaw') && !source.includes('AI Routines');
expect(leftHubPage).toBe(true);
await goBackFromSubpage();
await sleep(2000);
const backSource = await driver.getSource();
expect(backSource).toContain('Cameras');
reporter.record('右上角设置按钮进入设置页', 'PASS', Date.now() - start, '设置页正常进入');
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('右上角设置按钮进入设置页', 'FAIL', Date.now() - start, e.message, ss);
await goBackFromSubpage();
throw e;
}
});
});