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