AI_UIAutomation/tests/aihub/aihub_daily_report.test.ts

1123 lines
43 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, 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<string | undefined> {
try { return await driver.screenshot(); } catch { return undefined; }
}
async function waitForLoading(maxWait = 30000): Promise<void> {
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<string> {
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<boolean> {
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<boolean> {
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<boolean> {
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;
}
});
});