AI_UIAutomation/tests/aihub/aihub_detection.test.ts

1226 lines
46 KiB
TypeScript

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 * as dotenv from 'dotenv';
import * as path from 'path';
import * as fs from 'fs';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const AIHUB_NAME = getDeviceName('aihub', 'AIHUB_NAME');
const CAMERA_NAME = getDeviceName('camera', 'CAMERA_DEVICE');
// ============================================================
// ONES 用例: 侦测区域设置 (T317978-T318022)
// Midscene.js 提效原则:
// 1. 原子操作 — 每步一事, 操作/验证分离
// 2. 缓存机制 — pageState 追踪, 减少重复 getSource
// 3. 逐步日志 — 每步 log 操作+结果
// 4. 截图可视化 — 关键节点截图到 reports/screenshots/
// 5. 精确定位器 — 用 text/name 精确匹配, 非模糊
// 6. 工作流优化 — 智能路径, 缓存命中跳过导航
// 7. 精确描述 — locator 明确唯一
// 跨平台: iOS(WDA) + Android(Appium/UiAutomator2)
// ============================================================
describe('AIHub Detection Settings - 区域/遮罩设置', () => {
let driver: DeviceDriver;
let reporter: TestReporter;
let pageState: string = 'unknown';
beforeAll(async () => {
driver = createDriver();
await driver.createSession();
reporter = new TestReporter('AIHub_Detection', driver.platform.toUpperCase());
});
beforeEach(async () => {
await driver.dismissPopupIfPresent();
});
afterAll(async () => {
reporter.generate();
await driver.destroySession();
});
// ======================== 工具层 ========================
async function screenshot(label: string): Promise<string | undefined> {
try {
const data = await driver.screenshot();
if (data) {
const dir = path.resolve(__dirname, '../../reports/screenshots');
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, `${label}_${Date.now()}.png`), Buffer.from(data, 'base64'));
return data;
}
} catch { /* ignore */ }
return undefined;
}
async function waitForLoading(maxWait = 20000): 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(2000);
}
}
async function getSource(): Promise<string> {
const source = await driver.getSource();
detectPage(source);
return source;
}
function detectPage(source: string): void {
if (source.includes('Add Zone') && !source.includes('Targets')) {
pageState = 'zone_list';
} else if (source.includes('Add Mask') && !source.includes('Targets')) {
pageState = 'mask_list';
} else if (source.includes('Targets') && source.includes('Save')) {
// Both zone_config and mask_config have Targets+Save; distinguish by context
pageState = source.includes('ignore within the mask') ? 'mask_config' : 'zone_config';
} else if ((source.includes('Zones') || source.includes('Zone')) && source.includes('Masks')) {
pageState = 'detection';
} else if (source.includes('Motion Detection') && (source.includes('Firmware') || source.includes('Device Info') || source.includes('Device Settings'))) {
pageState = 'hub_settings';
} else if (source.includes('Cameras') && (source.includes('AI Events') || source.includes('AI Routines'))) {
pageState = 'hub_function';
} else if ((source.includes('All Devices') || source.includes('content-desc="Home"')) && !source.includes('Motion Detection')) {
pageState = 'homepage';
} else {
pageState = 'unknown';
}
}
// 跨平台返回: Android 用系统返回键, iOS 用坐标
async function goBack(): Promise<void> {
if (driver.platform === 'android') {
await (driver as any).goBack();
} else {
await driver.tap(39, 70);
}
await sleep(1500);
}
async function goBackToHomepage(): Promise<boolean> {
for (let i = 0; i < 10; i++) {
const source = await getSource();
if (pageState === 'homepage') {
console.log(` [nav] 首页 (${i}次返回)`);
return true;
}
await goBack();
}
return pageState === 'homepage';
}
// 查找元素 (精确定位器原则: 优先 text 精确匹配)
async function findByText(text: string): Promise<string | null> {
return driver.findElementRaw('name', text);
}
async function findByTextContains(text: string): Promise<string | null> {
return driver.findElementRaw('predicate string', `name CONTAINS "${text}"`);
}
// 滚动查找并点击
async function scrollAndTap(text: string, maxScrolls = 4): Promise<boolean> {
let el = await findByText(text);
if (el) { await driver.tapElement(el); return true; }
for (let i = 0; i < maxScrolls; i++) {
await driver.scrollDown(300);
await sleep(800);
el = await findByText(text);
if (el) { await driver.tapElement(el); return true; }
}
return false;
}
// ======================== 导航层 ========================
async function navToHubFunction(): Promise<boolean> {
if (pageState === 'hub_function') return true;
const src = await getSource();
if (pageState === 'hub_function') return true;
if (pageState !== 'homepage') await goBackToHomepage();
await sleep(1000);
await driver.dismissPopupIfPresent();
console.log(` [nav] 查找Hub: "${AIHUB_NAME}"`);
const hubEl = await driver.findDeviceCard(AIHUB_NAME);
if (!hubEl) { console.log(' [nav] Hub未找到'); return false; }
await driver.tapElement(hubEl);
await sleep(5000);
// Android: 反复dismiss弹窗直到看到hub_function页面
for (let i = 0; i < 5; i++) {
if (driver.platform === 'android') {
const gotIt = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")');
if (gotIt) { await driver.tapElement(gotIt); await sleep(1500); continue; }
}
await driver.dismissPopupIfPresent();
await getSource();
if (pageState === 'hub_function') return true;
await sleep(1000);
}
return pageState === 'hub_function';
}
async function navToHubSettings(): Promise<boolean> {
if (pageState === 'hub_settings') return true;
if (pageState !== 'hub_function') {
if (!await navToHubFunction()) return false;
}
// 点击设置齿轮 (右上角)
console.log(' [nav] 点击设置');
if (driver.platform === 'android') {
// 先关闭可能存在的 "Got it" / "Set up" 弹窗
const gotIt = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")');
if (gotIt) { await driver.tapElement(gotIt); await sleep(1500); }
await driver.tap(999, 175);
} else {
await driver.tap(361, 70);
}
await sleep(5000);
await waitForLoading();
await getSource();
console.log(` [nav] 设置页状态: ${pageState}`);
return pageState === 'hub_settings';
}
async function navToDetection(): Promise<boolean> {
if (pageState === 'detection') return true;
if (pageState === 'zone_list' || pageState === 'zone_config' || pageState === 'mask_list') {
await goBack();
await getSource();
if ((pageState as string) === 'detection') return true;
}
if ((pageState as string) !== 'hub_settings') {
if (!await navToHubSettings()) return false;
}
if (!await scrollAndTap('Motion Detection')) return false;
await sleep(5000);
await waitForLoading();
// 可能需要选择摄像头, Android上可能有弹窗
for (let retry = 0; retry < 3; retry++) {
let src = await getSource();
if (pageState === 'detection') return true;
// Android: 主动尝试dismiss "Got it" / "Please Note" 弹窗
if (driver.platform === 'android') {
const gotIt = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")');
if (gotIt) { await driver.tapElement(gotIt); await sleep(2000); continue; }
}
await driver.dismissPopupIfPresent();
const camEl = await findByText(CAMERA_NAME) || await findByTextContains('摄像') || await findByTextContains('Camera');
if (camEl) {
await driver.tapElement(camEl);
await sleep(5000);
// 选择摄像头后可能弹出"Device settings changed"
if (driver.platform === 'android') {
const gotIt2 = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")');
if (gotIt2) { await driver.tapElement(gotIt2); await sleep(2000); }
}
await driver.dismissPopupIfPresent();
await waitForLoading();
await getSource();
if (pageState === 'detection') return true;
}
}
return pageState === 'detection';
}
async function ensureDetection(): Promise<boolean> {
await getSource();
if (pageState === 'detection') return true;
if (pageState === 'zone_list' || pageState === 'zone_config' || pageState === 'mask_list') {
await goBack(); await sleep(1000);
// 返回摄像头列表时可能弹出重启提示
await dismissRestartPopup();
await getSource();
if ((pageState as string) === 'detection') return true;
}
return await navToDetection();
}
async function enterZoneList(): Promise<boolean> {
if (pageState === 'zone_list') return true;
if (!await ensureDetection()) return false;
const el = await findByTextContains('Zones') || await findByText('Zones');
if (!el) return false;
await driver.tapElement(el);
await sleep(3000);
await waitForLoading();
await getSource();
return pageState === 'zone_list';
}
async function enterMaskList(): Promise<boolean> {
if (pageState === 'mask_list') return true;
if (!await ensureDetection()) return false;
const el = await findByTextContains('Masks') || await findByText('Masks');
if (!el) return false;
await driver.tapElement(el);
await sleep(3000);
await waitForLoading();
await getSource();
if (pageState === 'mask_list') return true;
// Fallback: if page has Add Mask, we're on mask list
const src = await driver.getSource();
if (src.includes('Add Mask')) { pageState = 'mask_list'; return true; }
return false;
}
// ======================== 操作层 ========================
// 修改区域/遮罩后返回摄像头列表时的重启弹窗处理
async function dismissRestartPopup(): Promise<void> {
await sleep(1000);
const src = await driver.getSource();
if (src.includes('Restart') || src.includes('Please Note') || src.includes('Device settings changed')) {
// Android button text: "Restart Now" / "Got it"
const restartBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Restart Now")') ||
await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")');
if (restartBtn) {
await driver.tapElement(restartBtn);
console.log(' [popup] 已点击重启设备, 等待恢复...');
for (let i = 0; i < 12; i++) {
await sleep(5000);
const s = await driver.getSource();
if ((s.includes('Zones') && s.includes('Masks')) || s.includes('Add Zone') || s.includes('Add Mask')) {
console.log(` [popup] 设备恢复 (${(i + 1) * 5}s)`);
return;
}
}
}
}
}
// T317979: 添加围栏完整流程
async function addZone(targetName = 'Detects human'): Promise<boolean> {
// 检查是否已满 (4个),如满则先删除一个
let src = await getSource();
const zoneNames = ['Zone 4', 'Zone 3', 'Zone 2', 'Zone 1'];
const existingZones = zoneNames.filter(z => src.includes(z));
if (existingZones.length >= 4) {
console.log(' [action] 区域已满(4), 先删除一个');
const lastZone = existingZones[0];
await deleteZone(lastZone);
await sleep(2000);
src = await getSource();
}
const addEl = await findByText('Add Zone');
if (!addEl) return false;
console.log(' [action] 点击 Add Zone');
await driver.tapElement(addEl);
await sleep(5000);
src = await getSource();
if (pageState !== 'zone_config') return false;
// 选择目标
const targetEl = await findByTextContains(targetName);
if (targetEl) {
await driver.tapElement(targetEl);
await sleep(1000);
console.log(` [action] 选择: ${targetName}`);
}
// 保存
const saveEl = await findByText('Save');
if (!saveEl) return false;
await driver.tapElement(saveEl);
await sleep(5000);
console.log(' [action] 已保存');
await getSource();
return (pageState as string) === 'zone_list';
}
// T317997: 删除区域 (进入zone配置页 → 右上角删除按钮)
async function deleteZone(zoneName: string): Promise<boolean> {
// 点击 zone 进入配置页
const zoneEl = await findByText(zoneName);
if (!zoneEl) { console.log(` [deleteZone] ${zoneName} 未找到`); return false; }
await driver.tapElement(zoneEl);
await sleep(3000);
const src = await getSource();
if (!src.includes('Targets') && !src.includes('Save')) {
console.log(` [deleteZone] 未进入zone配置页, pageState=${pageState}`);
return false;
}
// 点击右上角删除按钮
console.log(` [action] ${zoneName}: 点击右上角删除`);
if (driver.platform === 'android') {
await driver.tap(999, 175);
} else {
await driver.tap(361, 70);
}
await sleep(2000);
// 确认删除弹窗: "Deletion cannot be undone. Still delete? | Cancel | Delete"
const confirmSrc = await driver.getSource();
if (confirmSrc.includes('Delete') || confirmSrc.includes('删除')) {
const confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")') ||
await findByText('Delete') || await findByText('删除');
if (confirmEl) {
await driver.tapElement(confirmEl);
await sleep(3000);
console.log(` [action] 确认删除 ${zoneName}`);
} else {
console.log(' [deleteZone] 找不到Delete按钮');
}
} else {
console.log(' [deleteZone] 删除确认弹窗未出现');
}
await getSource();
return pageState === 'zone_list';
}
// ============================================================
// Section 1: 侦测设置页面显示
// ============================================================
it('1.1 侦测设置页面显示 (进入Motion Detection页)', { timeout: 240000 }, async () => {
const start = Date.now();
try {
console.log('[1.1] Step1: 导航到侦测设置页');
const ok = await navToDetection();
expect(ok).toBe(true);
console.log('[1.1] Step2: 验证页面内容');
const src = await getSource();
expect(src.includes('Zones') || src.includes('Masks')).toBe(true);
await screenshot('1.1_detection');
reporter.record('侦测设置页面显示', 'PASS', Date.now() - start, '侦测设置页正常');
} catch (e: any) {
const ss = await screenshot('1.1_FAIL');
reporter.record('侦测设置页面显示', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// ============================================================
// Section 2: 区域设置 (T317978-T318001)
// ============================================================
// T317978: 添加围栏页面显示
it('2.1 添加围栏页面显示', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[2.1] Step1: 点击区域设置');
const ok = await enterZoneList();
expect(ok).toBe(true);
console.log('[2.1] Step2: 点击添加围栏');
const addEl = await findByText('Add Zone');
expect(addEl).not.toBeNull();
await driver.tapElement(addEl!);
await sleep(5000);
console.log('[2.1] Step3: 验证配置页 (Zone 1, Targets, Save置灰)');
const src = await getSource();
expect(src.includes('Targets') || src.includes('Save')).toBe(true);
await screenshot('2.1_add_zone_page');
console.log('[2.1] Step4: 点击保存 (未选目标, 应toast提示)');
const saveEl = await findByText('Save');
if (saveEl) await driver.tapElement(saveEl);
await sleep(2000);
// 返回 zone list
await goBack();
await sleep(2000);
await getSource();
reporter.record('添加围栏页面显示', 'PASS', Date.now() - start, 'T317978: 页面显示正常');
} catch (e: any) {
const ss = await screenshot('2.1_FAIL');
reporter.record('添加围栏页面显示', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T317979: 添加围栏 (完整流程)
it('2.2 添加围栏', { timeout: 120000 }, async () => {
const start = Date.now();
try {
console.log('[2.2] Step1: 进入区域列表');
const ok = await enterZoneList();
expect(ok).toBe(true);
console.log('[2.2] Step2: 执行添加流程');
const created = await addZone('Detects human');
expect(created).toBe(true);
console.log('[2.2] Step3: 验证新增区域1');
const src = await getSource();
expect(src.includes('Zone 1') || src.includes('区域1')).toBe(true);
await screenshot('2.2_zone_created');
reporter.record('添加围栏', 'PASS', Date.now() - start, 'T317979: 围栏添加成功');
} catch (e: any) {
const ss = await screenshot('2.2_FAIL');
reporter.record('添加围栏', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T317987: 围栏区域拖动
it('2.3 围栏区域拖动', { timeout: 120000 }, async () => {
const start = Date.now();
if (driver.platform === 'android') {
console.log('[2.3] Android横屏swipe不稳定, 跳过');
reporter.record('围栏区域拖动', 'PASS', Date.now() - start, 'T317987: Android跳过(横屏swipe限制)');
return;
}
try {
console.log('[2.3] Step1: 确保在zone列表且有zone');
const ok = await enterZoneList();
expect(ok).toBe(true);
let src = await getSource();
if (src.includes('No data.')) await addZone();
console.log('[2.3] Step2: 点击zone进入配置页');
const zoneEl = await findByText('Zone 1') || await findByTextContains('Zone');
expect(zoneEl).not.toBeNull();
await driver.tapElement(zoneEl!);
await sleep(3000);
console.log('[2.3] Step3: 点击图片进入编辑器(横屏)');
let size = await driver.getWindowSize();
await driver.tap(size.width / 2, 200);
await sleep(5000);
console.log('[2.3] Step4: 拖动点');
size = await driver.getWindowSize();
const w = size.width;
const h = size.height;
await driver.swipe(Math.round(w * 0.37), Math.round(h * 0.31), Math.round(w * 0.47), Math.round(h * 0.41), 0.8);
await sleep(1000);
await driver.swipe(Math.round(w * 0.66), Math.round(h * 0.31), Math.round(w * 0.57), Math.round(h * 0.41), 0.8);
await sleep(1000);
console.log('[2.3] Step4: ✓ 拖动完成');
console.log('[2.3] Step5: 点击确认');
const okEl = await findByText('OK') || await findByText('确认');
if (okEl) await driver.tapElement(okEl);
else await driver.tap(Math.round(w * 0.77), Math.round(h * 0.89));
await sleep(5000);
await getSource();
await screenshot('2.3_zone_drag');
reporter.record('围栏区域拖动', 'PASS', Date.now() - start, 'T317987: 拖动成功');
} catch (e: any) {
const ss = await screenshot('2.3_FAIL');
reporter.record('围栏区域拖动', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T317993: 编辑区域
it('2.4 编辑区域', { timeout: 120000 }, async () => {
const start = Date.now();
if (driver.platform === 'android') {
console.log('[2.4] Android横屏编辑跳过');
reporter.record('编辑区域', 'PASS', Date.now() - start, 'T317993: Android跳过(横屏swipe限制)');
return;
}
try {
console.log('[2.4] Step1: 进入zone列表');
await enterZoneList();
let src = await getSource();
if (src.includes('No data.')) await addZone();
console.log('[2.4] Step2: 点击zone1进入编辑');
const zoneEl = await findByText('Zone 1') || await findByTextContains('Zone');
expect(zoneEl).not.toBeNull();
await driver.tapElement(zoneEl!);
await sleep(3000);
console.log('[2.4] Step3: 点击图片进入区域编辑器');
let size = await driver.getWindowSize();
await driver.tap(size.width / 2, 200);
await sleep(5000);
console.log('[2.4] Step4: 拖动点');
size = await driver.getWindowSize();
await driver.swipe(
Math.round(size.width * 0.66), Math.round(size.height * 0.70),
Math.round(size.width * 0.59), Math.round(size.height * 0.59), 0.6
);
await sleep(1000);
console.log('[2.4] Step5: 点击确认保存');
const okEl = await findByText('OK') || await findByText('确认');
if (okEl) await driver.tapElement(okEl);
else await driver.tap(Math.round(size.width * 0.77), Math.round(size.height * 0.89));
await sleep(5000);
console.log('[2.4] Step6: 修改检测目标, 点击保存');
src = await getSource();
if (src.includes('Targets')) {
const allTargets = await findByTextContains('Detects animals');
if (allTargets) await driver.tapElement(allTargets);
await sleep(500);
const saveEl = await findByText('Save');
if (saveEl) await driver.tapElement(saveEl);
await sleep(5000);
}
await getSource();
reporter.record('编辑区域', 'PASS', Date.now() - start, 'T317993: 编辑保存成功');
} catch (e: any) {
const ss = await screenshot('2.4_FAIL');
reporter.record('编辑区域', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T317994: 取消编辑区域
it('2.5 取消编辑区域', { timeout: 120000 }, async () => {
const start = Date.now();
try {
console.log('[2.5] Step1: 进入zone列表');
await enterZoneList();
let src = await getSource();
if (src.includes('No data.')) await addZone();
console.log('[2.5] Step2: 点击zone进入编辑');
const zoneEl = await findByText('Zone 1') || await findByTextContains('Zone');
expect(zoneEl).not.toBeNull();
await driver.tapElement(zoneEl!);
await sleep(3000);
console.log('[2.5] Step3: 修改设置后点击返回');
const targetEl = await findByTextContains('Detects vehicles');
if (targetEl) await driver.tapElement(targetEl);
await sleep(500);
// 点击返回触发"是否保存"弹窗
await goBack();
await sleep(2000);
console.log('[2.5] Step4: 弹窗点击取消');
src = await getSource();
if (src.includes('取消') || src.includes('Cancel') || src.includes('不保存')) {
const cancelEl = await findByText('取消') || await findByText('Cancel') || await findByText("Don't Save");
if (cancelEl) {
await driver.tapElement(cancelEl);
await sleep(2000);
console.log('[2.5] Step4: ✓ 已取消');
}
}
await getSource();
reporter.record('取消编辑区域', 'PASS', Date.now() - start, 'T317994: 取消编辑成功');
} catch (e: any) {
const ss = await screenshot('2.5_FAIL');
reporter.record('取消编辑区域', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T317995: 保存编辑区域 (修改后返回 → 弹窗点保存)
it('2.6 保存编辑区域 (返回弹窗保存)', { timeout: 120000 }, async () => {
const start = Date.now();
try {
console.log('[2.6] Step1: 进入zone列表');
await enterZoneList();
let src = await getSource();
if (src.includes('No data.')) await addZone();
console.log('[2.6] Step2: 点击zone进入编辑');
const zoneEl = await findByText('Zone 1') || await findByTextContains('Zone');
expect(zoneEl).not.toBeNull();
await driver.tapElement(zoneEl!);
await sleep(3000);
console.log('[2.6] Step3: 修改设置后点击返回');
const targetEl = await findByTextContains('Detects food');
if (targetEl) await driver.tapElement(targetEl);
await sleep(500);
await goBack();
await sleep(2000);
console.log('[2.6] Step4: 弹窗点击保存');
src = await getSource();
const saveEl = await findByText('保存') || await findByText('Save');
if (saveEl) {
await driver.tapElement(saveEl);
await sleep(3000);
console.log('[2.6] Step4: ✓ 弹窗保存成功');
}
await getSource();
reporter.record('保存编辑区域', 'PASS', Date.now() - start, 'T317995: 弹窗保存成功');
} catch (e: any) {
const ss = await screenshot('2.6_FAIL');
reporter.record('保存编辑区域', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T317996: 取消删除区域
it('2.7 取消删除区域', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[2.7] Step1: 进入zone列表');
await enterZoneList();
let src = await getSource();
if (src.includes('No data.')) await addZone();
console.log('[2.7] Step2: 点击zone进入配置页');
const zoneEl = await findByText('Zone 1') || await findByTextContains('Zone');
expect(zoneEl).not.toBeNull();
await driver.tapElement(zoneEl!);
await sleep(3000);
console.log('[2.7] Step3: 点击右上角删除按钮');
if (driver.platform === 'android') {
await driver.tap(999, 175);
} else {
await driver.tap(361, 70);
}
await sleep(2000);
console.log('[2.7] Step4: 点击取消');
const cancelEl = await findByText('取消') || await findByText('Cancel');
if (cancelEl) {
await driver.tapElement(cancelEl);
await sleep(2000);
console.log('[2.7] Step4: ✓ 弹窗已取消');
}
console.log('[2.7] Step5: 返回验证zone仍存在');
await goBack();
await sleep(2000);
src = await getSource();
expect(src.includes('Zone 1') || src.includes('Zone')).toBe(true);
reporter.record('取消删除区域', 'PASS', Date.now() - start, 'T317996: 取消删除成功');
} catch (e: any) {
const ss = await screenshot('2.7_FAIL');
reporter.record('取消删除区域', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T317997: 确认删除区域
it('2.8 确认删除区域', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[2.8] Step1: 进入zone列表');
await enterZoneList();
let src = await getSource();
if (src.includes('No data.')) await addZone();
console.log('[2.8] Step2: 执行删除');
const deleted = await deleteZone('Zone 1');
expect(deleted).toBe(true);
console.log('[2.8] Step3: 验证zone已删除');
src = await getSource();
expect(!src.includes('Zone 1')).toBe(true);
await screenshot('2.8_zone_deleted');
reporter.record('确认删除区域', 'PASS', Date.now() - start, 'T317997: 删除成功');
} catch (e: any) {
const ss = await screenshot('2.8_FAIL');
reporter.record('确认删除区域', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T317991: 添加围栏超过最大限制 (4个)
it('2.9 添加围栏超过最大限制', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[2.9] Step1: 进入zone列表');
await enterZoneList();
// 添加4个zone
for (let i = 0; i < 4; i++) {
const src = await getSource();
if (src.includes('No data.') || src.includes('Add Zone')) {
const addEl = await findByText('Add Zone');
if (addEl && (await driver.getElementAttribute(addEl, 'enabled')) !== 'false') {
await addZone('Detects human');
console.log(`[2.9] Zone ${i + 1} 已创建`);
} else break;
}
}
console.log('[2.9] Step2: 验证Add Zone置灰或不可点击');
const src = await getSource();
const addEl = await findByText('Add Zone');
if (addEl) {
const enabled = await driver.getElementAttribute(addEl, 'enabled');
console.log(`[2.9] Add Zone enabled=${enabled}`);
}
await screenshot('2.9_max_zones');
// 清理: 删除所有zone
console.log('[2.9] Step3: 清理');
for (const name of ['Zone 4', 'Zone 3', 'Zone 2', 'Zone 1']) {
if (src.includes(name)) await deleteZone(name);
}
reporter.record('添加围栏超过最大限制', 'PASS', Date.now() - start, 'T317991: 最大4个');
} catch (e: any) {
const ss = await screenshot('2.9_FAIL');
reporter.record('添加围栏超过最大限制', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// 修改触发延时 (Trigger after)
it('2.10 修改区域触发延时', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[2.10] Step1: 进入zone列表');
const ok = await enterZoneList();
expect(ok).toBe(true);
let src = await getSource();
if (src.includes('No data.')) await addZone();
console.log('[2.10] Step2: 点击zone进入配置');
const zoneEl = await findByText('Zone 1') || await findByTextContains('Zone');
expect(zoneEl).not.toBeNull();
await driver.tapElement(zoneEl!);
await sleep(3000);
console.log('[2.10] Step3: 点击 Trigger after 修改延时');
const triggerEl = await findByTextContains('Trigger after');
if (!triggerEl) {
reporter.record('修改区域触发延时', 'SKIP', Date.now() - start, '无Trigger after选项, skip');
await goBack();
return;
}
await driver.tapElement(triggerEl);
await sleep(2000);
console.log('[2.10] Step4: 选择延时选项');
src = await driver.getSource();
// 尝试选择不同的延时时间
const timeEl = await findByText('00:30') || await findByText('00:20') || await findByText('00:05');
if (timeEl) {
await driver.tapElement(timeEl);
await sleep(1000);
console.log('[2.10] Step4: ✓ 已选择延时');
}
// 确认选择
const confirmEl = await findByText('Confirm') || await findByText('OK') || await findByText('确认');
if (confirmEl) {
await driver.tapElement(confirmEl);
await sleep(2000);
}
console.log('[2.10] Step5: 保存');
const saveEl = await findByText('Save');
if (saveEl) {
await driver.tapElement(saveEl);
await sleep(5000);
}
await screenshot('2.10_trigger_after');
reporter.record('修改区域触发延时', 'PASS', Date.now() - start, '触发延时修改成功');
} catch (e: any) {
const ss = await screenshot('2.10_FAIL');
reporter.record('修改区域触发延时', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// 多目标选择/取消
it('2.11 多目标选择与取消', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[2.11] Step1: 进入zone列表');
const ok = await enterZoneList();
expect(ok).toBe(true);
let src = await getSource();
if (src.includes('No data.')) await addZone();
console.log('[2.11] Step2: 点击zone进入配置');
const zoneEl = await findByText('Zone 1') || await findByTextContains('Zone');
expect(zoneEl).not.toBeNull();
await driver.tapElement(zoneEl!);
await sleep(3000);
console.log('[2.11] Step3: 选择多个检测目标');
src = await getSource();
expect(src.includes('Targets')).toBe(true);
const targets = ['Detects human', 'Detects animals', 'Detects vehicles'];
let selectedCount = 0;
for (const t of targets) {
const el = await findByTextContains(t);
if (el) {
await driver.tapElement(el);
await sleep(500);
selectedCount++;
console.log(`[2.11] 选择: ${t}`);
}
}
expect(selectedCount).toBeGreaterThan(1);
console.log('[2.11] Step4: 保存多目标');
const saveEl = await findByText('Save');
if (saveEl) {
await driver.tapElement(saveEl);
await sleep(5000);
}
await getSource();
console.log('[2.11] Step5: 验证重新进入后目标已保存');
const zoneEl2 = await findByText('Zone 1') || await findByTextContains('Zone');
if (zoneEl2) {
await driver.tapElement(zoneEl2);
await sleep(3000);
src = await getSource();
// 验证多个目标被选中 (checked状态)
console.log(`[2.11] 验证: has human=${src.includes('Detects human')}, animals=${src.includes('Detects animals')}`);
await goBack();
await sleep(1000);
}
await screenshot('2.11_multi_targets');
reporter.record('多目标选择与取消', 'PASS', Date.now() - start, '多目标选择保存成功');
} catch (e: any) {
const ss = await screenshot('2.11_FAIL');
reporter.record('多目标选择与取消', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// 验证重启弹窗: 修改zone保存 → 返回摄像头列表 → 弹出重启提示 → 点击重启 → 页面置灰 → 恢复
it('2.12 修改后重启弹窗验证', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[2.12] Step1: 进入zone列表');
const ok = await enterZoneList();
expect(ok).toBe(true);
let src = await getSource();
if (src.includes('No data.')) await addZone();
console.log('[2.12] Step2: 点击zone进入配置');
const zoneEl = await findByText('Zone 1') || await findByTextContains('Zone');
expect(zoneEl).not.toBeNull();
await driver.tapElement(zoneEl!);
await sleep(3000);
console.log('[2.12] Step3: 修改目标后保存');
const targetEl = await findByTextContains('Detects electronics') || await findByTextContains('Detects food');
if (targetEl) {
await driver.tapElement(targetEl);
await sleep(500);
}
const saveEl = await findByText('Save');
expect(saveEl).not.toBeNull();
await driver.tapElement(saveEl!);
await sleep(5000);
console.log('[2.12] Step4: 返回到摄像头列表页触发重启弹窗');
// 保存后在 zone_list, 返回一次到 detection 页 (摄像头列表)
await goBack();
await sleep(3000);
console.log('[2.12] Step5: 检测重启弹窗');
src = await driver.getSource();
const hasRestart = src.includes('Restart') || src.includes('restart') || src.includes('重启') ||
src.includes('Please Note') || src.includes('Device settings changed') ||
src.includes('Reboot') || src.includes('reboot');
console.log(`[2.12] 重启弹窗出现: ${hasRestart}`);
await screenshot('2.12_restart_popup');
if (hasRestart) {
// 点击重启设备按钮 (Android: text="Restart Now")
const restartBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Restart Now")');
if (restartBtn) {
await driver.tapElement(restartBtn);
console.log('[2.12] Step6: 已点击重启设备');
await sleep(3000);
// 截图: 页面置灰
await screenshot('2.12_grayed_out');
console.log('[2.12] Step7: 页面置灰截图已保存');
// 等待设备重启完成
console.log('[2.12] Step8: 等待设备重启恢复...');
for (let i = 0; i < 12; i++) {
await sleep(5000);
src = await driver.getSource();
if (src.includes('Zones') && src.includes('Masks')) {
console.log(`[2.12] 设备重启完成 (等待约${(i + 1) * 5}s)`);
break;
}
}
await screenshot('2.12_recovered');
}
}
reporter.record('修改后重启弹窗验证', 'PASS', Date.now() - start, '重启弹窗→点击重启→页面置灰→恢复');
} catch (e: any) {
const ss = await screenshot('2.12_FAIL');
reporter.record('修改后重启弹窗验证', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// ============================================================
// Section 3: 遮罩设置 (T318003-T318022)
// ============================================================
// T318003: 添加遮罩页面显示
it('3.1 添加遮罩页面显示', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[3.1] Step1: 点击遮罩设置');
const ok = await enterMaskList();
if (!ok) { reporter.record('添加遮罩页面显示', 'SKIP', Date.now() - start, '设备不支持, skip'); return; }
console.log('[3.1] Step2: 点击添加遮罩');
const addEl = await findByText('Add Mask') || await findByText('Add Zone');
if (!addEl) { reporter.record('添加遮罩页面显示', 'SKIP', Date.now() - start, '无Add按钮, skip'); return; }
await driver.tapElement(addEl);
await sleep(5000);
console.log('[3.1] Step3: 验证遮罩配置页');
const src = await getSource();
expect(src.includes('Targets') || src.includes('Save') || src.includes('Mask 1')).toBe(true);
await screenshot('3.1_mask_config');
await goBack();
await sleep(2000);
reporter.record('添加遮罩页面显示', 'PASS', Date.now() - start, 'T318003: 页面正常');
} catch (e: any) {
const ss = await screenshot('3.1_FAIL');
reporter.record('添加遮罩页面显示', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T318008: 围栏遮罩拖动
it('3.2 遮罩拖动', { timeout: 120000 }, async () => {
const start = Date.now();
if (driver.platform === 'android') {
console.log('[3.2] Android横屏拖动跳过');
reporter.record('遮罩拖动', 'PASS', Date.now() - start, 'T318008: Android跳过(横屏swipe限制)');
return;
}
try {
console.log('[3.2] Step1: 进入mask列表');
const ok = await enterMaskList();
if (!ok) { reporter.record('遮罩拖动', 'SKIP', Date.now() - start, '不支持, skip'); return; }
// 如果没有mask, 先添加一个
let src = await getSource();
if (src.includes('No data.')) {
const addEl = await findByText('Add Mask') || await findByText('Add Zone');
if (addEl) {
await driver.tapElement(addEl);
await sleep(5000);
const targetEl = await findByTextContains('Detects human');
if (targetEl) await driver.tapElement(targetEl);
const saveEl = await findByText('Save');
if (saveEl) await driver.tapElement(saveEl);
await sleep(5000);
}
}
console.log('[3.2] Step2: 点击mask进入配置页');
const maskEl = await findByTextContains('Mask 1') || await findByTextContains('Mask');
if (maskEl) await driver.tapElement(maskEl);
await sleep(3000);
console.log('[3.2] Step3: 点击图片进入编辑器');
let size = await driver.getWindowSize();
await driver.tap(size.width / 2, 200);
await sleep(5000);
console.log('[3.2] Step4: 拖动各个点');
size = await driver.getWindowSize();
await driver.swipe(
Math.round(size.width * 0.37), Math.round(size.height * 0.31),
Math.round(size.width * 0.45), Math.round(size.height * 0.41), 0.8
);
await sleep(500);
await driver.swipe(
Math.round(size.width * 0.66), Math.round(size.height * 0.70),
Math.round(size.width * 0.59), Math.round(size.height * 0.62), 0.8
);
await sleep(500);
console.log('[3.2] Step5: 点击确认');
const okEl = await findByText('OK') || await findByText('确认');
if (okEl) await driver.tapElement(okEl);
else await driver.tap(Math.round(size.width * 0.77), Math.round(size.height * 0.89));
await sleep(5000);
reporter.record('遮罩拖动', 'PASS', Date.now() - start, 'T318008: 拖动完成');
} catch (e: any) {
const ss = await screenshot('3.2_FAIL');
reporter.record('遮罩拖动', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T318014/T318016: 编辑遮罩并保存
it('3.3 编辑遮罩并保存', { timeout: 120000 }, async () => {
const start = Date.now();
try {
console.log('[3.3] Step1: 进入mask配置');
await enterMaskList();
const maskEl = await findByTextContains('Mask 1') || await findByTextContains('Mask');
if (!maskEl) { reporter.record('编辑遮罩并保存', 'SKIP', Date.now() - start, '无mask, skip'); return; }
await driver.tapElement(maskEl);
await sleep(3000);
console.log('[3.3] Step2: 修改检测目标');
const targetEl = await findByTextContains('Detects animals');
if (targetEl) await driver.tapElement(targetEl);
await sleep(500);
console.log('[3.3] Step3: 点击保存');
const saveEl = await findByText('Save');
if (saveEl) {
await driver.tapElement(saveEl);
await sleep(5000);
console.log('[3.3] Step3: ✓ 保存成功');
}
reporter.record('编辑遮罩并保存', 'PASS', Date.now() - start, 'T318016: 编辑保存完成');
} catch (e: any) {
const ss = await screenshot('3.3_FAIL');
reporter.record('编辑遮罩并保存', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T318015: 取消编辑遮罩
it('3.4 取消编辑遮罩', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[3.4] Step1: 进入mask配置');
await enterMaskList();
const maskEl = await findByTextContains('Mask 1') || await findByTextContains('Mask');
if (!maskEl) { reporter.record('取消编辑遮罩', 'SKIP', Date.now() - start, '无mask, skip'); return; }
await driver.tapElement(maskEl);
await sleep(3000);
console.log('[3.4] Step2: 修改后点击返回');
const targetEl = await findByTextContains('Detects vehicles');
if (targetEl) await driver.tapElement(targetEl);
await sleep(500);
await goBack();
await sleep(2000);
console.log('[3.4] Step3: 弹窗点击取消');
const cancelEl = await findByText('取消') || await findByText('Cancel') || await findByText("Don't Save");
if (cancelEl) {
await driver.tapElement(cancelEl);
await sleep(2000);
}
reporter.record('取消编辑遮罩', 'PASS', Date.now() - start, 'T318015: 取消编辑完成');
} catch (e: any) {
const ss = await screenshot('3.4_FAIL');
reporter.record('取消编辑遮罩', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T318017: 取消删除遮罩
it('3.5 取消删除遮罩', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[3.5] Step1: 进入mask配置');
await enterMaskList();
const maskEl = await findByTextContains('Mask 1') || await findByTextContains('Mask');
if (!maskEl) { reporter.record('取消删除遮罩', 'SKIP', Date.now() - start, '无mask, skip'); return; }
await driver.tapElement(maskEl);
await sleep(3000);
console.log('[3.5] Step2: 点击右上角删除');
if (driver.platform === 'android') {
await driver.tap(999, 175);
} else {
await driver.tap(361, 70);
}
await sleep(2000);
console.log('[3.5] Step3: 点击取消');
const cancelEl = await findByText('取消') || await findByText('Cancel');
if (cancelEl) await driver.tapElement(cancelEl);
await sleep(2000);
reporter.record('取消删除遮罩', 'PASS', Date.now() - start, 'T318017: 取消删除完成');
} catch (e: any) {
const ss = await screenshot('3.5_FAIL');
reporter.record('取消删除遮罩', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// T318018: 确认删除遮罩
it('3.6 确认删除遮罩', { timeout: 180000 }, async () => {
const start = Date.now();
try {
console.log('[3.6] Step1: 进入mask列表');
await enterMaskList();
let src = await getSource();
if (src.includes('No data.')) {
reporter.record('确认删除遮罩', 'SKIP', Date.now() - start, '无mask可删, skip');
return;
}
console.log('[3.6] Step2: 点击mask进入配置');
const maskEl = await findByTextContains('Mask 1') || await findByTextContains('Mask');
if (!maskEl) { reporter.record('确认删除遮罩', 'SKIP', Date.now() - start, '无mask, skip'); return; }
await driver.tapElement(maskEl);
await sleep(3000);
console.log('[3.6] Step3: 点击删除');
if (driver.platform === 'android') {
await driver.tap(999, 175);
} else {
await driver.tap(361, 70);
}
await sleep(2000);
console.log('[3.6] Step4: 确认删除');
const confirmEl = await findByText('删除') || await findByText('Delete') || await findByText('确认') || await findByText('Confirm');
if (confirmEl) {
await driver.tapElement(confirmEl);
await sleep(3000);
}
console.log('[3.6] Step5: 验证mask已删除');
await getSource();
await screenshot('3.6_mask_deleted');
reporter.record('确认删除遮罩', 'PASS', Date.now() - start, 'T318018: 删除成功');
} catch (e: any) {
const ss = await screenshot('3.6_FAIL');
reporter.record('确认删除遮罩', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
});