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 { 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 { 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 { 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 { if (driver.platform === 'android') { await (driver as any).goBack(); } else { await driver.tap(39, 70); } await sleep(1500); } async function goBackToHomepage(): Promise { 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 { return driver.findElementRaw('name', text); } async function findByTextContains(text: string): Promise { return driver.findElementRaw('predicate string', `name CONTAINS "${text}"`); } // 滚动查找并点击 async function scrollAndTap(text: string, maxScrolls = 4): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { // 检查是否已满 (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 { // 点击 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; } }); });