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) => 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 { try { return await driver.screenshot(); } catch { return undefined; } } async function dismissPopup(): Promise { await driver.dismissPopupIfPresent(); const gotIt = await driver.findElement(AICAM_LOCATORS.gotItButton); if (gotIt) { await driver.tapElement(gotIt); await sleep(1000); } } async function navigateToHubFromHome(): Promise { // 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 { 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 { 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 { if (isAndroid()) { await driver.goBack(); } else { await driver.tap(BACK_BTN().x, BACK_BTN().y); } await sleep(3000); } async function enterAIEvents(): Promise { 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 { 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 Events(AI事件分析) // ============================================================ 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; } }); });