AI_UIAutomation/tests/camera/camera_events.test.ts

848 lines
33 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';
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<void> {
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<void> {
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;
}
});
});