848 lines
33 KiB
TypeScript
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;
|
|
}
|
|
});
|
|
});
|