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'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('camera', 'CAMERA_DEVICE'); describe('Camera Events - 摄像头事件页', () => { let driver: DeviceDriver; let reporter: TestReporter; let screenWidth = 390; let screenHeight = 844; beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter('Camera_Events', driver.platform.toUpperCase()); const size = await driver.getWindowSize(); screenWidth = size.width; screenHeight = size.height; }, 30000); beforeEach(async () => { const source = await driver.getSource(); // Dismiss popup if present (inline to avoid extra getSource call) if (source.includes('XCUIElementTypeAlert') || source.includes('Upgrade') || source.includes("What's New")) { await driver.dismissPopupIfPresent(); await sleep(1000); } // On filter page — go back if (source.includes('Filter Options') || source.includes('Start time')) { await driver.tap(33, 69); await sleep(2000); return; } // On event detail page or subscription page — go back if (source.includes('View Playback') || source.includes('Subscribe') || source.includes('AI Guard')) { await driver.tap(33, 69); await sleep(2000); return; } // Already on camera page (Events tab view with Direction/Playback tabs) if (isOnCameraPage(source)) { // Make sure Events tab is selected const eventsEl = await driver.findElementRaw('name', 'Events'); if (eventsEl) { await driver.tapElement(eventsEl); await sleep(2000); } return; } // On event list page (has "All Events" but NOT camera tabs) — go back to camera page if (source.includes('All Events') && !source.includes('Direction') && !source.includes('Playback')) { await driver.tap(33, 69); await sleep(2000); return; } // On selection/delete mode — tap Cancel first if (source.includes('Select All') || source.includes('全选')) { const cancelEl = await driver.findElementRaw('name', 'Cancel') || await driver.findElementRaw('name', '取消'); if (cancelEl) { await driver.tapElement(cancelEl); await sleep(1000); } await driver.tap(33, 69); await sleep(2000); return; } // Already on homepage — no need to navigate const isHome = source.includes('主页') || source.includes('自动化') || (source.includes('Add') && source.includes('More') && !source.includes('Direction')); if (isHome) { return; } // Unknown state — try tapping back a few times then tap Home tab for (let i = 0; i < 3; i++) { await driver.tap(33, 69); await sleep(1500); const s = await driver.getSource(); if (isOnCameraPage(s) || s.includes('主页') || s.includes('自动化') || (s.includes('Add') && s.includes('More'))) { return; } } }, 60000); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); function isOnCameraPage(source: string): boolean { return (source.includes('Direction') && source.includes('Events') && source.includes('Playback')) || (source.includes('KB/S') && source.includes('Direction')) || (source.includes('Device online') && source.includes('Direction')); } async function enterCameraPage(): Promise { const currentSource = await driver.getSource(); if (isOnCameraPage(currentSource)) { return; } if (driver.platform === 'ios') { // Try finding camera card by accessible name const predicates = [ `name CONTAINS "${deviceName}" AND type == "XCUIElementTypeCell"`, 'name CONTAINS "Camera" AND type == "XCUIElementTypeCell"', 'name CONTAINS "Pan Tilt" AND type == "XCUIElementTypeCell"', ]; for (const pred of predicates) { const elems = await driver.findElementsRaw('predicate string', pred); if (elems.length > 0) { await driver.tapElement(elems[0]); await sleep(5000); return; } } // Camera preview card has no accessible name — tap below Cameras header for (let attempt = 0; attempt < 2; attempt++) { if (attempt === 0) { // Scroll to top first await driver.swipe(screenWidth / 2, 200, screenWidth / 2, 600, 0.3); await sleep(500); } else { // Scroll down to find Cameras section await driver.swipe(screenWidth / 2, 500, screenWidth / 2, 200, 0.5); await sleep(800); } const camerasBtn = await driver.findElementRaw('predicate string', 'name == "Cameras" AND type == "XCUIElementTypeButton"'); if (camerasBtn) { const rect = await driver.getElementRect(camerasBtn); await driver.tap(screenWidth / 2, rect.y + rect.height + 100); await sleep(5000); const source = await driver.getSource(); if (isOnCameraPage(source)) { return; } // Retry with different offset await driver.tap(screenWidth / 2, rect.y + rect.height + 150); await sleep(5000); const source2 = await driver.getSource(); if (isOnCameraPage(source2)) { return; } } } throw new Error('找不到摄像头卡片'); } else { const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${deviceName}")`); if (el) { await driver.tapElement(el); await sleep(5000); return; } const el2 = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Camera")'); if (el2) { await driver.tapElement(el2); await sleep(5000); return; } throw new Error('找不到摄像头卡片'); } } async function enterFullEventList(): Promise { const source = await driver.getSource(); if (source.includes('Filter Options')) { await driver.tap(33, 69); await sleep(2000); } // Already in full event list (has "All Events" in nav but NOT the camera tabs) if (source.includes('All Events') && !source.includes('Direction') && !source.includes('Playback')) { return; } const allEventsEl = await driver.findElementRaw('predicate string', 'name == "All Events" AND type == "XCUIElementTypeButton"'); if (allEventsEl) { await driver.tapElement(allEventsEl); await sleep(3000); } else { // Scroll down to find All Events button on camera page await driver.swipe(screenWidth / 2, 600, screenWidth / 2, 300, 0.5); await sleep(2000); const allEventsEl2 = await driver.findElementRaw('predicate string', 'name == "All Events" AND type == "XCUIElementTypeButton"'); if (allEventsEl2) { await driver.tapElement(allEventsEl2); await sleep(3000); } else { throw new Error('未找到All Events按钮'); } } } // ==================== 事件页面显示 ==================== it('事件页面显示', async () => { const start = Date.now(); try { await enterCameraPage(); await sleep(2000); let source = await driver.getSource(); let hasEvents = source.includes('Motion detected') || source.includes('检测到移动') || source.includes('All Events') || source.includes('Today'); // If events not visible, scroll down to events section if (!hasEvents) { await driver.swipe(screenWidth / 2, 600, screenWidth / 2, 300, 0.5); await sleep(2000); source = await driver.getSource(); hasEvents = source.includes('Motion detected') || source.includes('检测到移动') || source.includes('All Events') || source.includes('Today'); } expect(hasEvents).toBe(true); reporter.record('事件页面显示', 'PASS', Date.now() - start, '事件页加载成功'); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('事件页面显示', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // ==================== Tab切换 ==================== it('切换到Direction Tab', async () => { const start = Date.now(); try { await enterCameraPage(); await sleep(2000); const dirEl = await driver.findElementRaw('name', 'Direction'); if (!dirEl) { reporter.record('Direction Tab切换', 'FAIL', Date.now() - start, 'Direction Tab未找到'); throw new Error('Direction Tab未找到'); } await driver.tapElement(dirEl); await sleep(2000); const source = await driver.getSource(); const isDirection = source.includes('Direction') && !source.includes('All Events'); // Switch back to Events const eventsEl = await driver.findElementRaw('name', 'Events'); if (eventsEl) { await driver.tapElement(eventsEl); await sleep(2000); } reporter.record('Direction Tab切换', 'PASS', Date.now() - start, `Direction页=${isDirection}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('Direction Tab切换', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('切换到Playback Tab', async () => { const start = Date.now(); try { await enterCameraPage(); await sleep(2000); const playbackEl = await driver.findElementRaw('name', 'Playback'); if (!playbackEl) { reporter.record('Playback Tab切换', 'FAIL', Date.now() - start, 'Playback Tab未找到'); throw new Error('Playback Tab未找到'); } await driver.tapElement(playbackEl); await sleep(3000); const source = await driver.getSource(); const isPlayback = source.includes('Playback') && !source.includes('All Events'); reporter.record('Playback Tab切换', 'PASS', Date.now() - start, `Playback页=${isPlayback}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('Playback Tab切换', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // ==================== 进入全部事件列表 ==================== it('进入全部事件列表', async () => { const start = Date.now(); try { await enterCameraPage(); await enterFullEventList(); const source = await driver.getSource(); const hasEventList = source.includes('Motion detected') || source.includes('检测到移动'); const hasDelete = source.includes('Delete') || source.includes('删除'); expect(hasEventList).toBe(true); reporter.record('全部事件列表', 'PASS', Date.now() - start, `事件列表=${hasEventList}, Delete=${hasDelete}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('全部事件列表', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // ==================== 事件查看 ==================== it('事件查看', async () => { const start = Date.now(); try { await enterCameraPage(); await enterFullEventList(); // Event items have thumbnails on the right side // Tapping thumbnail → event detail; tapping text area → subscription const cells = await driver.findElementsRaw('predicate string', 'type == "XCUIElementTypeCell" AND visible == true'); if (cells.length === 0) { reporter.record('事件查看', 'SKIP', Date.now() - start, '无事件记录'); return; } const rect = await driver.getElementRect(cells[0]); // Tap the right side of the cell (thumbnail area) await driver.tap(rect.x + rect.width - 50, rect.y + rect.height / 2); await sleep(5000); const source = await driver.getSource(); const isEventDetail = source.includes('View Playback') || source.includes('1/'); const isSubscription = source.includes('Subscribe') || source.includes('AI Guard Plan'); if (isEventDetail) { const hasViewPlayback = source.includes('View Playback'); // Test View Playback (has text label, navigates to subscription/playback) const playbackBtn = await driver.findElementRaw('name', 'View Playback'); if (playbackBtn) { await driver.tapElement(playbackBtn); await sleep(3000); // Go back to event detail await driver.tap(33, 69); await sleep(2000); } // Close event detail (X button top-left) await driver.tap(24, 53); await sleep(2000); reporter.record('事件查看', 'PASS', Date.now() - start, `详情页加载成功, Playback=${hasViewPlayback}`); } else if (isSubscription) { await driver.tap(33, 69); await sleep(2000); reporter.record('事件查看', 'PASS', Date.now() - start, '事件跳转到订阅页(未订阅)'); } else { reporter.record('事件查看', 'PASS', Date.now() - start, '事件点击完成'); } } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('事件查看', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // ==================== 事件列表下拉刷新 ==================== it('事件列表下拉刷新', async () => { const start = Date.now(); try { await enterCameraPage(); await enterFullEventList(); // Pull down to refresh await driver.swipe(screenWidth / 2, 300, screenWidth / 2, 600, 0.5); await sleep(3000); const source = await driver.getSource(); const hasEvents = source.includes('Motion detected') || source.includes('检测到移动'); reporter.record('事件下拉刷新', 'PASS', Date.now() - start, `刷新后事件=${hasEvents}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('事件下拉刷新', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // ==================== 事件列表上滑加载 ==================== it('事件列表上滑加载更多', async () => { const start = Date.now(); try { await enterCameraPage(); await enterFullEventList(); // Swipe up to load more await driver.swipe(screenWidth / 2, 600, screenWidth / 2, 200, 0.5); await sleep(3000); const source = await driver.getSource(); const hasEvents = source.includes('Motion detected') || source.includes('检测到移动'); reporter.record('事件上滑加载', 'PASS', Date.now() - start, `滑动后有事件=${hasEvents}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('事件上滑加载', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // ==================== 事件编辑/删除 ==================== it('切换平铺视图并查看事件', async () => { const start = Date.now(); try { await enterCameraPage(); await enterFullEventList(); // Tap more button (second icon in nav bar right side) await driver.tap(356, 69); await sleep(2000); // Tap "Change View" to switch to tile/grid view const changeViewEl = await driver.findElementRaw('name', 'Change View'); if (!changeViewEl) { // Dismiss dropdown await driver.tap(screenWidth / 2, screenHeight / 2); await sleep(1000); reporter.record('平铺视图', 'SKIP', Date.now() - start, '未找到Change View选项'); return; } await driver.tapElement(changeViewEl); await sleep(3000); // In tile view, tap a thumbnail to view event detail const cells = await driver.findElementsRaw('predicate string', 'type == "XCUIElementTypeCell" AND visible == true'); if (cells.length > 0) { await driver.tapElement(cells[0]); await sleep(5000); const source = await driver.getSource(); const isEventDetail = source.includes('View Playback') || source.includes('1/') || source.includes('Motion detected'); const isSubscription = source.includes('Subscribe') || source.includes('AI Guard'); if (isEventDetail) { await driver.tap(24, 53); await sleep(2000); reporter.record('平铺视图', 'PASS', Date.now() - start, '平铺视图事件详情加载成功'); } else if (isSubscription) { await driver.tap(33, 69); await sleep(2000); reporter.record('平铺视图', 'PASS', Date.now() - start, '平铺视图事件跳转订阅页'); } else { reporter.record('平铺视图', 'PASS', Date.now() - start, '平铺视图切换成功'); } } else { reporter.record('平铺视图', 'PASS', Date.now() - start, '平铺视图切换成功(无事件)'); } } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('平铺视图', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('事件删除模式', async () => { const start = Date.now(); try { await enterCameraPage(); await enterFullEventList(); // Tap more button (second icon in nav bar right side) await driver.tap(356, 69); await sleep(2000); // Tap "Delete" from dropdown to enter delete/selection mode const deleteEl = await driver.findElementRaw('name', 'Delete'); if (!deleteEl) { await driver.tap(screenWidth / 2, screenHeight / 2); await sleep(1000); reporter.record('事件删除模式', 'SKIP', Date.now() - start, '未找到Delete选项'); return; } await driver.tapElement(deleteEl); await sleep(2000); // Select first event cell const cells = await driver.findElementsRaw('predicate string', 'type == "XCUIElementTypeCell" AND visible == true'); if (cells.length > 0) { await driver.tapElement(cells[0]); await sleep(1000); } // Tap Delete button (bottom of screen in selection mode) const deleteBtn = await driver.findElementRaw('name', 'Delete') || await driver.findElementRaw('name', '删除'); if (deleteBtn) { await driver.tapElement(deleteBtn); await sleep(2000); } // Confirmation dialog appears — look for confirm button within alert const source2 = await driver.getSource(); if (source2.includes('XCUIElementTypeAlert') || source2.includes('确认') || source2.includes('Confirm')) { const confirmBtn = await driver.findElementRaw('predicate string', '(name == "Confirm" OR name == "确认" OR name == "确定" OR name == "Delete" OR name == "删除") AND type == "XCUIElementTypeButton"'); if (confirmBtn) { await driver.tapElement(confirmBtn); await sleep(3000); } } else { const confirmDelete = await driver.findElementRaw('name', 'Confirm') || await driver.findElementRaw('name', '确认') || await driver.findElementRaw('name', '确定'); if (confirmDelete) { await driver.tapElement(confirmDelete); await sleep(3000); } } reporter.record('事件删除模式', 'PASS', Date.now() - start, '事件选择并删除成功'); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('事件删除模式', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('平铺视图事件详情操作', async () => { const start = Date.now(); try { await enterCameraPage(); await enterFullEventList(); // Switch to tile view await driver.tap(356, 69); await sleep(2000); const changeViewEl = await driver.findElementRaw('name', 'Change View'); if (!changeViewEl) { await driver.tap(screenWidth / 2, screenHeight / 2); await sleep(1000); reporter.record('平铺事件详情', 'SKIP', Date.now() - start, '未找到Change View'); return; } await driver.tapElement(changeViewEl); await sleep(3000); // Tap first event thumbnail const cells = await driver.findElementsRaw('predicate string', 'type == "XCUIElementTypeCell" AND visible == true'); if (cells.length === 0) { reporter.record('平铺事件详情', 'SKIP', Date.now() - start, '无事件'); return; } await driver.tapElement(cells[0]); await sleep(5000); const source = await driver.getSource(); const isDetail = source.includes('View Playback') || source.includes('1/'); if (!isDetail) { await driver.tap(33, 69); await sleep(2000); reporter.record('平铺事件详情', 'PASS', Date.now() - start, '事件点击完成(非详情页)'); return; } const hasPlayback = source.includes('View Playback'); // 1. Tap View Playback const playbackBtn = await driver.findElementRaw('name', 'View Playback'); if (playbackBtn) { await driver.tapElement(playbackBtn); await sleep(3000); await driver.tap(33, 69); await sleep(2000); } // Bottom-right 3 icons (left to right): Delete, Download, Share // Share is rightmost, Download is to its left, Delete is leftmost const shareX = screenWidth - 30; const downloadX = screenWidth - 80; const deleteX = screenWidth - 130; const btnY = screenHeight - 60; // 2. Tap Share (rightmost icon button) await driver.tap(shareX, btnY); await sleep(3000); // Dismiss share dialog — try multiple approaches // First try Cancel button const cancelEl = await driver.findElementRaw('name', 'Cancel') || await driver.findElementRaw('name', '取消'); if (cancelEl) { await driver.tapElement(cancelEl); } else { // Try swiping the share sheet down from its handle area await driver.swipe(screenWidth / 2, screenHeight * 0.4, screenWidth / 2, screenHeight * 0.9, 0.3); } await sleep(2000); // Verify dialog dismissed — if still showing, try tapping outside const sourceAfterDismiss = await driver.getSource(); if (sourceAfterDismiss.includes('Cancel') || sourceAfterDismiss.includes('取消') || sourceAfterDismiss.includes('AirDrop') || sourceAfterDismiss.includes('Messages') || sourceAfterDismiss.includes('Copy') || sourceAfterDismiss.includes('WeChat')) { // Still on share dialog, tap outside await driver.tap(screenWidth / 2, 50); await sleep(2000); } // After share dialog dismissed, still on event detail page // 3. Tap Download (left of Share) await driver.tap(downloadX, btnY); await sleep(3000); // Record current event time/index before delete const sourceBeforeDelete = await driver.getSource(); // Extract time or page indicator (e.g. "1/5", or timestamp text) const timeMatch = sourceBeforeDelete.match(/(\d{1,2}:\d{2})/); const indexMatch = sourceBeforeDelete.match(/(\d+)\/(\d+)/); const beforeTime = timeMatch ? timeMatch[1] : ''; const beforeIndex = indexMatch ? indexMatch[1] : ''; // 4. Tap Delete (leftmost icon) await driver.tap(deleteX, btnY); await sleep(2000); // Confirm delete dialog const confirmBtn = await driver.findElementRaw('predicate string', '(name == "Confirm" OR name == "确认" OR name == "确定" OR name == "Delete" OR name == "删除") AND type == "XCUIElementTypeButton"'); if (confirmBtn) { await driver.tapElement(confirmBtn); await sleep(3000); } // After delete, should jump to next event — verify time/index changed const sourceAfterDelete = await driver.getSource(); const isStillDetail = sourceAfterDelete.includes('View Playback') || sourceAfterDelete.includes('/'); const afterTimeMatch = sourceAfterDelete.match(/(\d{1,2}:\d{2})/); const afterIndexMatch = sourceAfterDelete.match(/(\d+)\/(\d+)/); const afterTime = afterTimeMatch ? afterTimeMatch[1] : ''; const afterIndex = afterIndexMatch ? afterIndexMatch[1] : ''; const timeChanged = (beforeTime && afterTime) ? beforeTime !== afterTime : true; const indexChanged = (beforeIndex && afterIndex) ? beforeIndex !== afterIndex : true; const jumpedToNext = isStillDetail && (timeChanged || indexChanged); reporter.record('平铺事件详情', 'PASS', Date.now() - start, `Playback=${hasPlayback}, 分享/下载/删除已测试, 删除后跳转下一事件=${jumpedToNext}, 时间:${beforeTime}→${afterTime}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('平铺事件详情', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // ==================== 事件筛选 ==================== it('事件筛选', async () => { const start = Date.now(); try { // Check current state — previous test may have left us anywhere const currentSource = await driver.getSource(); if (currentSource.includes('Filter Options') || currentSource.includes('Start time')) { // Already on filter page } else if (currentSource.includes('Motion detected') && !currentSource.includes('Direction') && !currentSource.includes('Playback')) { // On event list (list or tile view) — proceed directly } else if (currentSource.includes('All Events') && !currentSource.includes('Direction')) { // On event list page } else { // Try going back to event list from wherever we are for (let i = 0; i < 3; i++) { await driver.tap(33, 69); await sleep(2000); const s = await driver.getSource(); if (s.includes('Motion detected') && !s.includes('Direction')) break; if (s.includes('All Events') && !s.includes('Direction')) break; if (isOnCameraPage(s)) { await enterFullEventList(); break; } // If on homepage, enter camera then event list if (s.includes('主页') || s.includes('自动化') || (s.includes('Add') && s.includes('More') && !s.includes('Direction'))) { await enterCameraPage(); await enterFullEventList(); break; } } } // Filter button is an unnamed icon in nav bar (first icon on right side) // Nav bar buttons: filter at ~(313,69), more at ~(356,69) await driver.tap(313, 69); await sleep(3000); const source = await driver.getSource(); const hasFilterPage = source.includes('Filter Options') || source.includes('Start time'); if (!hasFilterPage) { reporter.record('事件筛选', 'FAIL', Date.now() - start, '筛选页面未打开'); throw new Error('筛选页面未打开'); } // === Select Start time === const startTimeEl = await driver.findElementRaw('name', 'Start time'); if (startTimeEl) { await driver.tapElement(startTimeEl); await sleep(3000); // Check what appeared — picker wheels or date picker const pickerSource = await driver.getSource(); const pickers = await driver.findElementsRaw('class name', 'XCUIElementTypePickerWheel'); if (pickers.length > 0) { // Scroll first picker wheel (usually month/day) to select earlier date const pickerRect = await driver.getElementRect(pickers[0]); await driver.swipe(pickerRect.x + pickerRect.width / 2, pickerRect.y + pickerRect.height / 2, pickerRect.x + pickerRect.width / 2, pickerRect.y + pickerRect.height / 2 + 80, 0.3); await sleep(1000); // If there's a second picker (hour/minute), scroll it too if (pickers.length > 1) { const pickerRect2 = await driver.getElementRect(pickers[1]); await driver.swipe(pickerRect2.x + pickerRect2.width / 2, pickerRect2.y + pickerRect2.height / 2, pickerRect2.x + pickerRect2.width / 2, pickerRect2.y + pickerRect2.height / 2 + 40, 0.3); await sleep(1000); } } else if (pickerSource.includes('DatePicker') || pickerSource.includes('calendar')) { // Inline date picker — tap a date cell const dateCells = await driver.findElementsRaw('class name', 'XCUIElementTypeButton'); if (dateCells.length > 3) { await driver.tapElement(dateCells[2]); await sleep(1000); } } // Confirm the date picker const doneEl = await driver.findElementRaw('name', 'Done') || await driver.findElementRaw('name', 'Confirm') || await driver.findElementRaw('name', '确定') || await driver.findElementRaw('name', '确认') || await driver.findElementRaw('name', 'OK'); if (doneEl) { await driver.tapElement(doneEl); await sleep(2000); } } // Verify we're back on filter page after selecting start time const afterStartSource = await driver.getSource(); if (!afterStartSource.includes('End time') && !afterStartSource.includes('Filter Options')) { // Might still be on picker — tap back await driver.tap(33, 69); await sleep(2000); } // === Select End time === const endTimeEl = await driver.findElementRaw('name', 'End time'); if (endTimeEl) { await driver.tapElement(endTimeEl); await sleep(3000); const pickers2 = await driver.findElementsRaw('class name', 'XCUIElementTypePickerWheel'); if (pickers2.length > 0) { // Scroll picker up slightly for end time (later date) const pickerRect2 = await driver.getElementRect(pickers2[0]); await driver.swipe(pickerRect2.x + pickerRect2.width / 2, pickerRect2.y + pickerRect2.height / 2, pickerRect2.x + pickerRect2.width / 2, pickerRect2.y + pickerRect2.height / 2 - 40, 0.3); await sleep(1000); } else { // Inline picker — tap a date const dateCells2 = await driver.findElementsRaw('class name', 'XCUIElementTypeButton'); if (dateCells2.length > 3) { await driver.tapElement(dateCells2[dateCells2.length - 2]); await sleep(1000); } } const doneEl2 = await driver.findElementRaw('name', 'Done') || await driver.findElementRaw('name', 'Confirm') || await driver.findElementRaw('name', '确定') || await driver.findElementRaw('name', '确认') || await driver.findElementRaw('name', 'OK'); if (doneEl2) { await driver.tapElement(doneEl2); await sleep(2000); } } // Verify back on filter page const afterEndSource = await driver.getSource(); if (!afterEndSource.includes('Filter Options') && !afterEndSource.includes('Save')) { await driver.tap(33, 69); await sleep(2000); } // === Select event type (checkbox) === const motionEl = await driver.findElementRaw('name', 'Motion detected'); if (motionEl) { await driver.tapElement(motionEl); await sleep(1000); } else { // Try other event types const eventTypeEl = await driver.findElementRaw('name', 'Person detected') || await driver.findElementRaw('name', 'All'); if (eventTypeEl) { await driver.tapElement(eventTypeEl); await sleep(1000); } } // === Tap Save === const saveEl = await driver.findElementRaw('name', 'Save'); if (saveEl) { await driver.tapElement(saveEl); await sleep(3000); } else { throw new Error('未找到Save按钮'); } // === Verify navigated back to event list page === const afterSource = await driver.getSource(); const backToEventList = (afterSource.includes('Motion detected') || afterSource.includes('Today') || afterSource.includes('All Events')) && !afterSource.includes('Filter Options') && !afterSource.includes('Start time'); if (!backToEventList) { reporter.record('事件筛选', 'FAIL', Date.now() - start, '筛选后未返回事件列表页'); throw new Error('筛选后未返回事件列表页'); } reporter.record('事件筛选', 'PASS', Date.now() - start, '筛选完成并返回事件列表'); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('事件筛选', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); });