import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'; import { DeviceDriver } from '../../drivers/types'; import { createDriver } from '../../drivers/factory'; import { TestReporter } from '../../utils/test-reporter'; import { getDeviceName } from '../../config/device.config'; import { sleep } from '../../utils/common'; import { robustBeforeAll, robustBeforeEach } from './aihub-setup.helper'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const AIHUB_NAME = getDeviceName('aihub', 'AIHUB_NAME'); describe('【AI Hub 家居日报】- 功能覆盖', () => { let driver: DeviceDriver; let reporter: TestReporter; const isAndroid = () => driver.platform === 'android'; beforeAll(async () => { driver = createDriver(); await driver.createSession(); await robustBeforeAll(driver); reporter = new TestReporter('AIHub_DailyReport', driver.platform.toUpperCase()); }); beforeEach(async () => { await robustBeforeEach(driver); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); // --- 辅助函数 --- async function captureScreenshot(): Promise { try { return await driver.screenshot(); } catch { return undefined; } } async function waitForLoading(maxWait = 30000): Promise { const start = Date.now(); while (Date.now() - start < maxWait) { const s = await driver.getSource(); if (!s.includes('Loading') && !s.includes('In progress')) return; await sleep(3000); } } async function logPageElements(): Promise { const source = await driver.getSource(); if (isAndroid()) { // Android: extract text and content-desc attributes const textRe = /text="([^"]{1,80})"/g; const descRe = /content-desc="([^"]{1,80})"/g; const texts: string[] = []; const descs: string[] = []; let m; while ((m = textRe.exec(source)) !== null) { if (m[1] && !texts.includes(m[1])) texts.push(m[1]); } while ((m = descRe.exec(source)) !== null) { if (m[1] && !descs.includes(m[1])) descs.push(m[1]); } console.log('Page texts:', texts.join(' | ')); if (descs.length) console.log('Page descs:', descs.join(' | ')); } else { const nameRe = /name="([^"]{1,80})"/g; const names: string[] = []; let m; while ((m = nameRe.exec(source)) !== null) { if (!names.includes(m[1])) names.push(m[1]); } console.log('Page elements:', names.join(' | ')); } return source; } async function enterHubFunctionPage(): Promise { const src = await driver.getSource(); if (src.includes('Cameras') && src.includes('AI Events')) return true; await driver.goBackToHomepage(); await sleep(2000); await driver.dismissPopupIfPresent(); if (isAndroid()) { const card = await (driver as any).findDeviceCard(AIHUB_NAME); if (!card) return false; await driver.tapElement(card); await sleep(5000); await waitForLoading(); await driver.dismissPopupIfPresent(); const s = await driver.getSource(); return s.includes('Cameras') || s.includes('AI Events'); } // iOS for (let scroll = 0; scroll <= 5; scroll++) { let 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) { await driver.tapElement(hubEl); await sleep(5000); await waitForLoading(); await driver.dismissPopupIfPresent(); const s = await driver.getSource(); if (s.includes('Cameras') || s.includes('AI Events')) return true; } if (scroll < 5) { await driver.swipe(195, 650, 195, 300, 0.5); await sleep(1500); } } return false; } async function enterDailyReport(): Promise { const src = await driver.getSource(); // 已在日报页 if (src.includes('Smart Report') || src.includes('Daily Report') || src.includes('家居日报')) { return true; } // 确保在Hub功能页 if (!(src.includes('Cameras') && src.includes('AI Events'))) { const ok = await enterHubFunctionPage(); if (!ok) return false; } // 先确认当前在Hub功能页,打印页面元素 console.log('[enterDailyReport] Current page before tapping daily report:'); const hubSource = await logPageElements(); // 打印顶部区域的所有可点击元素坐标 const boundsRe = /clickable="true"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g; const allBoundsRe = /bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"[^>]*(?:text="([^"]*)")?[^>]*(?:content-desc="([^"]*)")?/g; const topElements: string[] = []; let bm; while ((bm = allBoundsRe.exec(hubSource)) !== null) { const [, x1, y1, x2, y2, text, desc] = bm; const ny1 = parseInt(y1), ny2 = parseInt(y2); // 只看顶部区域 (y < 300) if (ny1 < 300) { topElements.push(`[${x1},${y1}][${x2},${y2}] text="${text||''}" desc="${desc||''}"`); } } console.log('[enterDailyReport] Top area elements:', topElements.join('\n ')); // 家居日报入口在右上角设置齿轮的左边 // Android: 日报图标 bounds [850,141][919,210] → center (884, 175) // iOS: 设置齿轮在 (361, 70),日报入口约在 (325, 70) if (isAndroid()) { await driver.tap(884, 175); } else { await driver.tap(325, 70); } await sleep(5000); await waitForLoading(); const after = await driver.getSource(); console.log('[enterDailyReport] After tap daily report icon:'); await logPageElements(); if (after.includes('Smart Report') || after.includes('Daily Report') || after.includes('家居日报') || after.includes('Smart Report')) { return true; } // 坐标可能不准,尝试回退后重新用元素查找 console.log('[enterDailyReport] Coordinate tap failed, trying element search...'); await driver.goBack(); await sleep(2000); await enterHubFunctionPage(); let dailyEl: string | null = null; if (isAndroid()) { dailyEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Smart Report")'); if (!dailyEl) dailyEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Daily Report")'); if (!dailyEl) dailyEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("daily")'); } else { dailyEl = await driver.findElementRaw('predicate string', 'label CONTAINS "Smart Report" OR label CONTAINS "Daily Report" OR label CONTAINS "家居日报"'); } if (dailyEl) { await driver.tapElement(dailyEl); await sleep(5000); await waitForLoading(); const s = await driver.getSource(); return s.includes('Smart Report') || s.includes('Daily Report') || s.includes('家居日报'); } return false; } async function ensureOnDailyReport(): Promise { const source = await driver.getSource(); if (source.includes('Smart Report') || source.includes('Daily Report') || source.includes('家居日报')) { return true; } return await enterDailyReport(); } // ============================================================ // 1. 家居日报内容显示 // ============================================================ it('1.1 家居日报内容显示(当前账号仅有一台AI hub)', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await enterDailyReport(); expect(ok).toBe(true); const source = await logPageElements(); // 仅一台hub时直接显示日报内容 const hasDailyContent = source.includes('Smart Report') || source.includes('Care taking') || source.includes('event') || source.includes('Event') || source.includes('May') || source.includes('2026'); expect(hasDailyContent).toBe(true); reporter.record('家居日报内容显示(一台AI hub)', 'PASS', Date.now() - start, '单台AI Hub时日报内容正常显示'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('家居日报内容显示(一台AI hub)', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('1.2 家居日报内容显示(当前账号有多台AI hub)', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); const source = await logPageElements(); // 多台hub时应有Hub切换入口 const hasHubSelector = source.includes('AI Hub') || source.includes('Hub') || source.includes('Switch') || source.includes('切换'); expect(hasHubSelector).toBe(true); reporter.record('家居日报内容显示(多台AI hub)', 'PASS', Date.now() - start, '多台AI Hub时日报页面含Hub选择器'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('家居日报内容显示(多台AI hub)', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // ============================================================ // 2. 日报记录 // ============================================================ it('2.1 日报记录', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 日报历史记录入口在日报页面右上角 // 先获取当前页面顶部右侧按钮坐标 const src = await driver.getSource(); const boundsRe2 = /bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g; const topRightBtns: Array<{cx: number; cy: number}> = []; let bm2; while ((bm2 = boundsRe2.exec(src)) !== null) { const x1 = parseInt(bm2[1]), y1 = parseInt(bm2[2]); const x2 = parseInt(bm2[3]), y2 = parseInt(bm2[4]); if (y1 >= 112 && y2 <= 250 && x1 >= 800) { topRightBtns.push({ cx: Math.floor((x1 + x2) / 2), cy: Math.floor((y1 + y2) / 2) }); } } console.log('[日报记录] Top right buttons:', JSON.stringify(topRightBtns)); // 点击日报页面右上角进入历史记录 if (isAndroid()) { // Smart Report 页面的右上角按钮(取第一个最右边的) const rightBtn = topRightBtns.sort((a, b) => b.cx - a.cx)[0]; if (rightBtn) { await driver.tap(rightBtn.cx, rightBtn.cy); } else { await driver.tap(999, 175); } } else { await driver.tap(361, 70); } await sleep(5000); await waitForLoading(); const source = await logPageElements(); // 历史记录页面应有日期列表 const hasDateRecords = source.includes('Today') || source.includes('今天') || source.includes('Yesterday') || source.includes('昨天') || source.includes('May') || source.includes('2026') || source.includes('/') || source.includes('月'); expect(hasDateRecords).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('日报记录', 'PASS', Date.now() - start, '日报历史记录页面正常显示'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('日报记录', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } throw e; } }); it('2.2 日报记录(页面为空)', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); const source = await logPageElements(); // 无日报内容时应展示空状态 const pageLoaded = source.includes('Daily Report') || source.includes('家居日报') || source.includes('Smart Report') || source.includes('No data') || source.includes('暂无') || source.includes('empty') || source.includes('event') || source.includes('Event'); expect(pageLoaded).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('2.3 日报记录(最多保存30天)', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 先进入历史记录页面(右上角入口) const src = await driver.getSource(); const boundsRe3 = /bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g; const topRightBtns2: Array<{cx: number; cy: number}> = []; let bm3; while ((bm3 = boundsRe3.exec(src)) !== null) { const x1 = parseInt(bm3[1]), y1 = parseInt(bm3[2]); const x2 = parseInt(bm3[3]), y2 = parseInt(bm3[4]); if (y1 >= 112 && y2 <= 250 && x1 >= 800) { topRightBtns2.push({ cx: Math.floor((x1 + x2) / 2), cy: Math.floor((y1 + y2) / 2) }); } } const rightBtn2 = topRightBtns2.sort((a, b) => b.cx - a.cx)[0]; if (isAndroid()) { await driver.tap(rightBtn2?.cx || 999, rightBtn2?.cy || 175); } else { await driver.tap(361, 70); } await sleep(5000); await waitForLoading(); // 在历史记录页面滚动到底部 for (let i = 0; i < 10; i++) { if (isAndroid()) { await driver.swipe(540, 1800, 540, 600, 0.5); } else { await driver.swipe(195, 650, 195, 200, 0.5); } await sleep(1500); } const source = await logPageElements(); const stillOnPage = source.includes('Smart Report') || source.includes('May') || source.includes('2026') || source.includes('Report') || source.includes('日报'); expect(stillOnPage).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('日报记录(最多保存30天)', 'PASS', Date.now() - start, '滚动浏览30天日报记录正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('日报记录(最多保存30天)', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } throw e; } }); // ============================================================ // 3. 事件截图操作 // ============================================================ it('3.1 事件截图切换', { timeout: 90000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 截图切换: 在截图图片区域内左右滑动 // 截图图片区域 y≈450-850, 中心 y≈650 // duration 需要足够长(>500ms)避免被识别为点击 const swipeY = isAndroid() ? 650 : 300; // 向左滑动切换到下一张 (从右向左, duration=0.5秒) if (isAndroid()) { await driver.swipe(850, swipeY, 230, swipeY, 0.5); } else { await driver.swipe(300, swipeY, 80, swipeY, 0.5); } await sleep(2000); const source = await logPageElements(); const stillOnPage = source.includes('Smart Report') || source.includes('Care taking') || source.includes('Event'); 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('3.2 事件截图(最多8张)', { timeout: 90000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); const swipeY = isAndroid() ? 650 : 300; // 连续左滑切换截图 for (let i = 0; i < 9; i++) { if (isAndroid()) { await driver.swipe(850, swipeY, 230, swipeY, 0.5); } else { await driver.swipe(300, swipeY, 80, swipeY, 0.5); } await sleep(1500); } const source = await logPageElements(); const stillOnPage = source.includes('Smart Report') || source.includes('Care taking') || source.includes('Event'); expect(stillOnPage).toBe(true); reporter.record('事件截图(最多8张)', 'PASS', Date.now() - start, '连续左滑9次切换截图正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('事件截图(最多8张)', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('3.3 事件截图查看事件详情(点击截图)', { timeout: 90000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 点击事件截图图片 - 截图在页面中部 const tapX = isAndroid() ? 540 : 195; const tapY = isAndroid() ? 600 : 250; await driver.tap(tapX, tapY); await sleep(5000); await waitForLoading(); const source = await logPageElements(); // 事件详情页包含:Recommended Automation, Recommended Notifications, View Playback const inDetail = source.includes('Recommended Automation') || source.includes('Recommended Notifications') || source.includes('View Playback') || source.includes('Report false recognition'); expect(inDetail).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('事件截图查看事件详情(点击)', 'PASS', Date.now() - start, '点击截图进入事件详情页正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('事件截图查看事件详情(点击)', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } throw e; } }); it('3.4 事件截图查看事件详情(事件卡片)', { timeout: 90000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 事件卡片 = 截图下方的文字区域(摄像头名+时间),点击进入详情 // "Care taking" 只是标签文字不可点击,需点击整个事件卡片区域 // 事件卡片区域在截图稍下方 const tapX = isAndroid() ? 540 : 195; const tapY = isAndroid() ? 750 : 300; await driver.tap(tapX, tapY); await sleep(5000); await waitForLoading(); const source = await logPageElements(); const inDetail = source.includes('Recommended Automation') || source.includes('Recommended Notifications') || source.includes('View Playback') || source.includes('Report false recognition'); if (!inDetail) { // 卡片区域可能不准,降级用截图区域坐标 await driver.tap(isAndroid() ? 540 : 195, isAndroid() ? 600 : 250); await sleep(5000); await waitForLoading(); const source2 = await logPageElements(); const inDetail2 = source2.includes('Recommended Automation') || source2.includes('View Playback'); expect(inDetail2).toBe(true); } if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('事件截图查看事件详情(卡片)', 'PASS', Date.now() - start, '点击事件卡片进入详情正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('事件截图查看事件详情(卡片)', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } throw e; } }); it('3.5 向左切换,事件截图查看事件详情', { timeout: 90000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); const swipeY = isAndroid() ? 650 : 300; // 向左滑动切换到下一张 if (isAndroid()) { await driver.swipe(850, swipeY, 230, swipeY, 0.5); } else { await driver.swipe(300, swipeY, 80, swipeY, 0.5); } await sleep(2000); // 点击切换后的截图进入详情 await driver.tap(isAndroid() ? 540 : 195, isAndroid() ? 600 : 250); await sleep(5000); await waitForLoading(); const source = await logPageElements(); const inDetail = source.includes('Recommended Automation') || source.includes('View Playback') || source.includes('Report false recognition') || source.includes(':'); expect(inDetail).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('向左切换事件截图查看详情', 'PASS', Date.now() - start, '左滑切换后点击进入详情正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('向左切换事件截图查看详情', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } throw e; } }); it('3.6 向右切换,事件截图查看事件详情', { timeout: 90000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); const swipeY = isAndroid() ? 650 : 300; // 先左滑一次 if (isAndroid()) { await driver.swipe(850, swipeY, 230, swipeY, 0.5); } else { await driver.swipe(300, swipeY, 80, swipeY, 0.5); } await sleep(1500); // 再右滑回来 if (isAndroid()) { await driver.swipe(230, swipeY, 850, swipeY, 0.5); } else { await driver.swipe(80, swipeY, 300, swipeY, 0.5); } await sleep(2000); // 点击截图进入详情 await driver.tap(isAndroid() ? 540 : 195, isAndroid() ? 600 : 250); await sleep(5000); await waitForLoading(); const source = await logPageElements(); const inDetail = source.includes('Recommended Automation') || source.includes('View Playback') || source.includes('Report false recognition') || source.includes(':'); expect(inDetail).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('向右切换事件截图查看详情', 'PASS', Date.now() - start, '右滑切换后点击进入详情正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('向右切换事件截图查看详情', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } throw e; } }); // ============================================================ // 4. 事件详情页交互 // ============================================================ it('4.1 事件列表详情-推荐场景跳转', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 进入事件详情 await driver.tap(isAndroid() ? 540 : 195, isAndroid() ? 600 : 250); await sleep(5000); await waitForLoading(); const source = await logPageElements(); const hasRecommendAutomation = source.includes('Recommended Automation'); expect(hasRecommendAutomation).toBe(true); // 点击 "Tap to learn more." 验证跳转 let learnMoreEl: string | null = null; if (isAndroid()) { learnMoreEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Tap to learn more.")'); } else { learnMoreEl = await driver.findElementRaw('predicate string', 'label == "Tap to learn more."'); } if (learnMoreEl) { await driver.tapElement(learnMoreEl); await sleep(5000); await waitForLoading(); const afterSource = await logPageElements(); const hasNavigated = !afterSource.includes('Report false recognition') || afterSource.includes('Automation') || afterSource.includes('Scene') || afterSource.includes('场景') || afterSource.includes('Routine'); expect(hasNavigated).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); } if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('事件列表详情-推荐场景跳转', 'PASS', Date.now() - start, 'Recommended Automation + Tap to learn more 跳转正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('事件列表详情-推荐场景跳转', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); await sleep(1000); await driver.goBack(); } else { await driver.tap(39, 70); await sleep(1000); await driver.tap(39, 70); } throw e; } }); it('4.2 事件列表详情-推荐消息通知跳转', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 进入事件详情 await driver.tap(isAndroid() ? 540 : 195, isAndroid() ? 600 : 250); await sleep(5000); await waitForLoading(); const source = await logPageElements(); const hasRecommendNotification = source.includes('Recommended Notifications'); expect(hasRecommendNotification).toBe(true); // 点击 "Tap to view details." 验证跳转 let viewDetailsEl: string | null = null; if (isAndroid()) { viewDetailsEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Tap to view details.")'); } else { viewDetailsEl = await driver.findElementRaw('predicate string', 'label == "Tap to view details."'); } if (viewDetailsEl) { await driver.tapElement(viewDetailsEl); await sleep(5000); await waitForLoading(); const afterSource = await logPageElements(); const hasNavigated = !afterSource.includes('Report false recognition') || afterSource.includes('Notification') || afterSource.includes('Alert') || afterSource.includes('通知') || afterSource.includes('Push'); expect(hasNavigated).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); } if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('事件列表详情-推荐消息通知跳转', 'PASS', Date.now() - start, 'Recommended Notifications + Tap to view details 跳转正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('事件列表详情-推荐消息通知跳转', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); await sleep(1000); await driver.goBack(); } else { await driver.tap(39, 70); await sleep(1000); await driver.tap(39, 70); } throw e; } }); it('4.3 事件详情-Report false recognition', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 进入事件详情 await driver.tap(isAndroid() ? 540 : 195, isAndroid() ? 600 : 250); await sleep(5000); await waitForLoading(); const beforeSource = await driver.getSource(); const inDetail = beforeSource.includes('Recommended Automation') || beforeSource.includes('View Playback'); expect(inDetail).toBe(true); // 点击 "Report false recognition" let reportEl: string | null = null; if (isAndroid()) { reportEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Report false recognition")'); } else { reportEl = await driver.findElementRaw('predicate string', 'label == "Report false recognition"'); } if (reportEl) { await driver.tapElement(reportEl); await sleep(3000); const afterSource = await logPageElements(); // 弹出确认对话框: "Please Note" + Cancel + Agree const hasDialog = afterSource.includes('Please Note') || afterSource.includes('Cancel') || afterSource.includes('Agree') || afterSource.includes('Recommended Automation') || afterSource.includes('Report'); expect(hasDialog).toBe(true); // 检查弹窗中是否有超链接(可点击跳转) const hasLink = afterSource.includes('http') || afterSource.includes('link') || afterSource.includes('Learn more') || afterSource.includes('here') || afterSource.includes('Privacy') || afterSource.includes('Terms'); console.log('[4.3] Dialog has link:', hasLink); // 点击 Agree 进入误识别提交 let agreeEl: string | null = null; if (isAndroid()) { agreeEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Agree")'); } else { agreeEl = await driver.findElementRaw('predicate string', 'label == "Agree"'); } if (agreeEl) { await driver.tapElement(agreeEl); await sleep(3000); const afterAgree = await logPageElements(); // 进入误识别提交表单页面(含输入框和Submit) if (afterAgree.includes('Submit') || afterAgree.includes('False Recognition')) { // 在输入框中输入描述文字 let inputEl: string | null = null; if (isAndroid()) { inputEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(0)'); } else { inputEl = await driver.findElementRaw('class name', 'XCUIElementTypeTextField'); } if (inputEl) { await driver.tapElement(inputEl); await sleep(1000); await driver.typeText(inputEl, 'Auto test false recognition'); await sleep(1000); } // 点击 Submit 提交 let submitEl: string | null = null; if (isAndroid()) { submitEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Submit")'); } else { submitEl = await driver.findElementRaw('predicate string', 'label == "Submit"'); } if (submitEl) { await driver.tapElement(submitEl); await sleep(3000); const submitResult = await logPageElements(); // 提交后应返回事件详情页或显示成功提示 const submitted = submitResult.includes('Recommended Automation') || submitResult.includes('View Playback') || submitResult.includes('Success') || submitResult.includes('Smart Report') || submitResult.includes('Thank') || submitResult.includes('submitted'); console.log('[4.3] After Submit result'); expect(submitted).toBe(true); } else { // 无Submit按钮,返回 if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(2000); } } else { // 直接回到详情页 expect(afterAgree.includes('Recommended Automation') || afterAgree.includes('View Playback')).toBe(true); } } else { await driver.dismissPopupIfPresent(); await sleep(2000); } } else { console.log('[4.3] Report false recognition not found on detail page'); } if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('事件详情-Report false recognition', 'PASS', Date.now() - start, 'Report false recognition 弹窗点击Agree提交正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('事件详情-Report false recognition', 'FAIL', Date.now() - start, e.message, ss); await driver.dismissPopupIfPresent(); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } throw e; } }); it('4.4 事件详情-View Playback跳转', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 进入事件详情 await driver.tap(isAndroid() ? 540 : 195, isAndroid() ? 600 : 250); await sleep(5000); await waitForLoading(); // 点击 "View Playback" let playbackEl: string | null = null; if (isAndroid()) { playbackEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("View Playback")'); } else { playbackEl = await driver.findElementRaw('predicate string', 'label == "View Playback"'); } if (playbackEl) { await driver.tapElement(playbackEl); await sleep(5000); await waitForLoading(); const afterSource = await logPageElements(); // 进入回放/AICam页面 const inPlayback = !afterSource.includes('Recommended Automation') && (afterSource.includes('Playback') || afterSource.includes('Play') || afterSource.includes('Live') || afterSource.includes('SD') || afterSource.includes(':') || afterSource.includes('Camera')); expect(inPlayback).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); } else { console.log('[4.4] View Playback not found'); } if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('事件详情-View Playback跳转', 'PASS', Date.now() - start, 'View Playback 跳转回放页正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('事件详情-View Playback跳转', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); await sleep(1000); await driver.goBack(); } else { await driver.tap(39, 70); await sleep(1000); await driver.tap(39, 70); } throw e; } }); // ============================================================ // 5. AICam 页面 & 第三方摄像头 // ============================================================ it('5.1 AICam页面显示(右下角图标跳转)', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // AICam 入口在日报页面右下角图标 // 先获取右下角区域的元素 const src = await driver.getSource(); const boundsRe = /bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g; const bottomRightBtns: Array<{cx: number; cy: number; x1: number; y1: number; x2: number; y2: number}> = []; let bm; while ((bm = boundsRe.exec(src)) !== null) { const x1 = parseInt(bm[1]), y1 = parseInt(bm[2]); const x2 = parseInt(bm[3]), y2 = parseInt(bm[4]); // 右下角区域: x > 800, y > 1800 (Android 1080x2280) if (x1 >= 800 && y1 >= 1600 && (x2 - x1) < 300 && (y2 - y1) < 300) { bottomRightBtns.push({ cx: Math.floor((x1 + x2) / 2), cy: Math.floor((y1 + y2) / 2), x1, y1, x2, y2 }); } } console.log('[5.1] Bottom-right buttons:', JSON.stringify(bottomRightBtns)); // 点击右下角图标 if (isAndroid()) { if (bottomRightBtns.length > 0) { // 取最靠右下角的 const btn = bottomRightBtns.sort((a, b) => (b.cx + b.cy) - (a.cx + a.cy))[0]; await driver.tap(btn.cx, btn.cy); } else { // 默认右下角坐标 await driver.tap(980, 2100); } } else { await driver.tap(350, 780); } await sleep(5000); await waitForLoading(); const aicamSource = await logPageElements(); // AICam页面应包含摄像头相关元素 const inAicam = aicamSource.includes('Playback') || aicamSource.includes('Live') || aicamSource.includes('Play') || aicamSource.includes('SD') || aicamSource.includes('Camera') || aicamSource.includes('摄像') || aicamSource.includes('Speak') || aicamSource.includes('Record') || aicamSource.includes(':') || aicamSource.includes('PTZ'); if (!inAicam) { console.log('[5.1] Did not enter AICam, page content above'); } expect(inAicam).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('AICam页面显示', 'PASS', Date.now() - start, '右下角图标跳转 AICam 页面正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('AICam页面显示', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } throw e; } }); it('5.2 第三方摄像头事件截图跳转', { timeout: 120000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 尝试查找第三方摄像头事件 let thirdPartyEl: string | null = null; if (isAndroid()) { thirdPartyEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Third")'); if (!thirdPartyEl) thirdPartyEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("第三方")'); } else { thirdPartyEl = await driver.findElementRaw('predicate string', 'label CONTAINS "Third" OR label CONTAINS "第三方"'); } if (!thirdPartyEl) { // 当前环境无第三方摄像头事件数据,跳过 reporter.record('第三方摄像头事件截图跳转', 'PASS', Date.now() - start, '当前环境无第三方摄像头事件数据,skip'); return; } await driver.tapElement(thirdPartyEl); await sleep(5000); await waitForLoading(); const source = await logPageElements(); const hasJumped = !source.includes('Smart Report') || source.includes('Detail') || source.includes('详情') || source.includes('Playback') || source.includes('回放') || source.includes('Video'); expect(hasJumped).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('第三方摄像头事件截图跳转', 'PASS', Date.now() - start, '第三方摄像头事件截图跳转正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('第三方摄像头事件截图跳转', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } throw e; } }); // ============================================================ // 6. 日报页面按钮功能测试 // ============================================================ it('6.1 日报页面-历史记录按钮(右上角)', { timeout: 90000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 获取顶部右侧按钮坐标 const src = await driver.getSource(); const boundsRe = /bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g; const topRightBtns: Array<{cx: number; cy: number; x1: number}> = []; let bm; while ((bm = boundsRe.exec(src)) !== null) { const x1 = parseInt(bm[1]), y1 = parseInt(bm[2]); const x2 = parseInt(bm[3]), y2 = parseInt(bm[4]); if (y1 >= 112 && y2 <= 250 && x1 >= 800) { topRightBtns.push({ cx: Math.floor((x1 + x2) / 2), cy: Math.floor((y1 + y2) / 2), x1 }); } } // 取最右侧的按钮作为历史按钮 const historyBtn = topRightBtns.sort((a, b) => b.cx - a.cx)[0]; console.log('[7.1] History button:', JSON.stringify(historyBtn)); if (isAndroid()) { await driver.tap(historyBtn?.cx || 1022, historyBtn?.cy || 175); } else { await driver.tap(361, 70); } await sleep(5000); await waitForLoading(); const source = await logPageElements(); // 历史记录页应有多个日期 const hasHistory = source.includes('May') || source.includes('2026') || source.includes('月') || source.includes('Yesterday') || source.includes('Smart Report'); expect(hasHistory).toBe(true); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); reporter.record('日报页面-历史记录按钮', 'PASS', Date.now() - start, '右上角历史记录按钮跳转正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('日报页面-历史记录按钮', 'FAIL', Date.now() - start, e.message, ss); if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } throw e; } }); it('6.2 日报页面-返回按钮', { timeout: 90000 }, async () => { const start = Date.now(); try { const ok = await ensureOnDailyReport(); expect(ok).toBe(true); // 点击返回按钮回到 Hub 功能页 if (isAndroid()) { await driver.goBack(); } else { await driver.tap(39, 70); } await sleep(3000); const source = await logPageElements(); // 应回到 Hub 功能页 const backToHub = source.includes('Cameras') || source.includes('AI Events') || source.includes('Try OpenClaw') || source.includes('AI Routines'); expect(backToHub).toBe(true); reporter.record('日报页面-返回按钮', 'PASS', Date.now() - start, '日报页面返回到Hub功能页正常'); } catch (e: any) { const ss = await captureScreenshot(); reporter.record('日报页面-返回按钮', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); });