1069 lines
36 KiB
TypeScript
1069 lines
36 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 Control - 摄像头功能页操作', () => {
|
|
let driver: DeviceDriver;
|
|
let reporter: TestReporter;
|
|
let screenWidth = 390;
|
|
let screenHeight = 844;
|
|
|
|
beforeAll(async () => {
|
|
driver = createDriver();
|
|
await driver.createSession();
|
|
reporter = new TestReporter('Camera_Control', driver.platform.toUpperCase());
|
|
const size = await driver.getWindowSize();
|
|
screenWidth = size.width;
|
|
screenHeight = size.height;
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await driver.dismissPopupIfPresent();
|
|
await driver.goBackToHomepage();
|
|
await driver.dismissPopupIfPresent();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
reporter.generate();
|
|
await driver.destroySession();
|
|
});
|
|
|
|
async function findCameraCard(): Promise<string | null> {
|
|
if (driver.platform === 'ios') {
|
|
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) return elems[0];
|
|
}
|
|
await driver.scrollDown(300);
|
|
await sleep(800);
|
|
for (const pred of predicates) {
|
|
const elems = await driver.findElementsRaw('predicate string', pred);
|
|
if (elems.length > 0) return elems[0];
|
|
}
|
|
} else {
|
|
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${deviceName}")`);
|
|
if (el) return el;
|
|
const el2 = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Camera")');
|
|
if (el2) return el2;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function enterControlPage(waitForVideo = true): Promise<void> {
|
|
// Check if already on camera page
|
|
const currentSource = await driver.getSource();
|
|
const isOnCamera = (currentSource.includes('Direction') && currentSource.includes('Events') && currentSource.includes('Playback'))
|
|
|| (currentSource.includes('KB/S') && currentSource.includes('Direction'))
|
|
|| (currentSource.includes('Device online') && currentSource.includes('Direction'));
|
|
if (isOnCamera) return;
|
|
|
|
const cardId = await findCameraCard();
|
|
if (cardId) {
|
|
await driver.tapElement(cardId);
|
|
} else {
|
|
// Camera card has no accessible name — tap below "Cameras" header button
|
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
if (attempt === 0) {
|
|
await driver.swipe(screenWidth / 2, 200, screenWidth / 2, 600, 0.3);
|
|
await sleep(500);
|
|
} else {
|
|
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 s = await driver.getSource();
|
|
if (s.includes('Direction') || s.includes('KB/S')) return;
|
|
await driver.tap(screenWidth / 2, rect.y + rect.height + 150);
|
|
await sleep(5000);
|
|
const s2 = await driver.getSource();
|
|
if (s2.includes('Direction') || s2.includes('KB/S')) return;
|
|
}
|
|
}
|
|
throw new Error('找不到摄像头卡片');
|
|
}
|
|
|
|
if (!waitForVideo) {
|
|
// Just wait for page to appear (for Features/Direction tabs that don't need video)
|
|
await sleep(5000);
|
|
return;
|
|
}
|
|
|
|
// Wait for video stream to fully load (KB/S indicator appears when stream is active)
|
|
const start = Date.now();
|
|
while (Date.now() - start < 20000) {
|
|
const source = await driver.getSource();
|
|
if (source.includes('Disconnected') || source.includes('Connection failed')) {
|
|
throw new Error('摄像头未连接');
|
|
}
|
|
if (source.includes('KB/S')) {
|
|
await sleep(2000);
|
|
return;
|
|
}
|
|
await sleep(1500);
|
|
}
|
|
// Even if KB/S not found, continue if Device online
|
|
const source = await driver.getSource();
|
|
if (source.includes('Device online') || source.includes('设备在线')) {
|
|
await sleep(2000);
|
|
return;
|
|
}
|
|
throw new Error('视频流加载超时');
|
|
}
|
|
|
|
async function tapVideoArea(): Promise<void> {
|
|
await driver.tap(screenWidth / 2, 200);
|
|
await sleep(1000);
|
|
}
|
|
|
|
async function tapTab(tabName: string): Promise<void> {
|
|
const el = await driver.findElementRaw('name', tabName);
|
|
if (el) {
|
|
await driver.tapElement(el);
|
|
await sleep(2000);
|
|
} else {
|
|
throw new Error(`Tab "${tabName}" not found`);
|
|
}
|
|
}
|
|
|
|
// Video overlay control buttons (icon-only, positioned by coordinates)
|
|
// Top row (y≈137): HD(257,137), Speaker(314,137), Grid(354,137)
|
|
// Bottom row (y≈338): Pause(16,338), Screenshot(99,338), Talk(183,338), Record(266,338), Fullscreen(350,338)
|
|
async function tapHDButton(): Promise<void> {
|
|
await driver.tap(270, 137);
|
|
}
|
|
|
|
async function tapSpeakerButton(): Promise<void> {
|
|
await driver.tap(324, 147);
|
|
}
|
|
|
|
async function tapPauseButton(): Promise<void> {
|
|
await driver.tap(28, 350);
|
|
}
|
|
|
|
async function tapScreenshotButton(): Promise<void> {
|
|
await driver.tap(111, 350);
|
|
}
|
|
|
|
async function tapTalkButton(): Promise<void> {
|
|
await driver.tap(195, 350);
|
|
}
|
|
|
|
async function tapRecordButton(): Promise<void> {
|
|
await driver.tap(278, 350);
|
|
}
|
|
|
|
async function tapFullscreenButton(): Promise<void> {
|
|
await driver.tap(362, 350);
|
|
}
|
|
|
|
async function tapGridButton(): Promise<void> {
|
|
await driver.tap(354, 137);
|
|
}
|
|
|
|
// ==================== 四宫格 ====================
|
|
|
|
it('功能页打开四宫格', async () => {
|
|
const start = Date.now();
|
|
try {
|
|
await enterControlPage();
|
|
await tapVideoArea();
|
|
|
|
// Tap grid button (top-right, next to speaker)
|
|
await tapGridButton();
|
|
await sleep(3000);
|
|
|
|
// In quad view, look for Add button to add another camera
|
|
const addEl = await driver.findElementRaw('name', 'Add')
|
|
|| await driver.findElementRaw('name', '添加')
|
|
|| await driver.findElementRaw('name', '+');
|
|
if (addEl) {
|
|
await driver.tapElement(addEl);
|
|
await sleep(3000);
|
|
|
|
// Should show device list to select camera — pick one if available
|
|
const source = await driver.getSource();
|
|
const cells = await driver.findElementsRaw('predicate string',
|
|
'type == "XCUIElementTypeCell" AND visible == true');
|
|
if (cells.length > 0) {
|
|
// Tap first available camera device
|
|
await driver.tapElement(cells[0]);
|
|
await sleep(3000);
|
|
}
|
|
|
|
// Go back if on device selection page
|
|
const sourceAfter = await driver.getSource();
|
|
if (!sourceAfter.includes('KB/S') && !sourceAfter.includes('Direction')) {
|
|
await driver.tap(33, 69);
|
|
await sleep(2000);
|
|
}
|
|
}
|
|
|
|
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 enterControlPage();
|
|
await tapVideoArea();
|
|
|
|
// Open grid view
|
|
await tapGridButton();
|
|
await sleep(3000);
|
|
|
|
// Tap grid button again or tap a specific camera to return to single view
|
|
await tapVideoArea();
|
|
await tapGridButton();
|
|
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 enterControlPage();
|
|
await tapVideoArea();
|
|
|
|
const hdEl = await driver.findElementRaw('name', 'HD');
|
|
const sdEl = await driver.findElementRaw('name', 'SD');
|
|
|
|
if (hdEl) {
|
|
await driver.tapElement(hdEl);
|
|
await sleep(8000);
|
|
await tapVideoArea();
|
|
const sdAfter = await driver.findElementRaw('name', 'SD');
|
|
const detail = sdAfter ? 'HD → SD' : 'HD → (toggled)';
|
|
reporter.record('切换画质', 'PASS', Date.now() - start, detail);
|
|
} else if (sdEl) {
|
|
await driver.tapElement(sdEl);
|
|
await sleep(8000);
|
|
await tapVideoArea();
|
|
const hdAfter = await driver.findElementRaw('name', 'HD');
|
|
const detail = hdAfter ? 'SD → HD' : 'SD → (toggled)';
|
|
reporter.record('切换画质', 'PASS', Date.now() - start, detail);
|
|
} else {
|
|
// Fallback: tap by coordinate
|
|
await tapHDButton();
|
|
await sleep(8000);
|
|
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 enterControlPage();
|
|
await tapVideoArea();
|
|
|
|
// Tap speaker button (top row, icon only)
|
|
await tapSpeakerButton();
|
|
await sleep(3000);
|
|
|
|
// Tap again to toggle back
|
|
await tapVideoArea();
|
|
await tapSpeakerButton();
|
|
await sleep(2000);
|
|
|
|
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 enterControlPage();
|
|
await tapVideoArea();
|
|
await tapScreenshotButton();
|
|
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 enterControlPage();
|
|
await tapVideoArea();
|
|
|
|
// Start recording
|
|
await tapRecordButton();
|
|
await sleep(5000);
|
|
|
|
// Stop recording
|
|
await tapVideoArea();
|
|
await tapRecordButton();
|
|
await sleep(2000);
|
|
|
|
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 enterControlPage();
|
|
await tapVideoArea();
|
|
|
|
// Pause
|
|
await tapPauseButton();
|
|
await sleep(3000);
|
|
|
|
// Resume
|
|
await tapVideoArea();
|
|
await tapPauseButton();
|
|
await sleep(3000);
|
|
|
|
const source = await driver.getSource();
|
|
const isLive = !source.includes('Disconnected');
|
|
expect(isLive).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;
|
|
}
|
|
});
|
|
|
|
// ==================== 通话 ====================
|
|
|
|
it('功能页通话', async () => {
|
|
const start = Date.now();
|
|
try {
|
|
await enterControlPage();
|
|
await tapVideoArea();
|
|
|
|
// Start talk
|
|
await tapTalkButton();
|
|
await sleep(5000);
|
|
|
|
// Stop talk
|
|
await tapVideoArea();
|
|
await tapTalkButton();
|
|
await sleep(2000);
|
|
|
|
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 enterControlPage(false);
|
|
await tapTab('Features');
|
|
|
|
const nvEl = await driver.findElementRaw('name', 'Night Vision');
|
|
expect(nvEl).not.toBeNull();
|
|
await driver.tapElement(nvEl!);
|
|
await sleep(2000);
|
|
|
|
// Select "On" / "Always on"
|
|
const onEl = await driver.findElementRaw('name', 'On') || await driver.findElementRaw('name', 'Always on');
|
|
if (onEl) {
|
|
await driver.tapElement(onEl);
|
|
await sleep(3000);
|
|
}
|
|
|
|
reporter.record('夜视-打开', 'PASS', Date.now() - start, 'Night Vision=On');
|
|
} 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 enterControlPage(false);
|
|
await tapTab('Features');
|
|
|
|
const nvEl = await driver.findElementRaw('name', 'Night Vision');
|
|
expect(nvEl).not.toBeNull();
|
|
await driver.tapElement(nvEl!);
|
|
await sleep(2000);
|
|
|
|
const offEl = await driver.findElementRaw('name', 'Off');
|
|
if (offEl) {
|
|
await driver.tapElement(offEl);
|
|
await sleep(3000);
|
|
}
|
|
|
|
reporter.record('夜视-关闭', 'PASS', Date.now() - start, 'Night Vision=Off');
|
|
} 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 enterControlPage(false);
|
|
await tapTab('Features');
|
|
|
|
const nvEl = await driver.findElementRaw('name', 'Night Vision');
|
|
expect(nvEl).not.toBeNull();
|
|
await driver.tapElement(nvEl!);
|
|
await sleep(2000);
|
|
|
|
const autoEl = await driver.findElementRaw('name', 'Auto');
|
|
if (autoEl) {
|
|
await driver.tapElement(autoEl);
|
|
await sleep(3000);
|
|
}
|
|
|
|
reporter.record('夜视-自动', 'PASS', Date.now() - start, 'Night Vision=Auto');
|
|
} 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 enterControlPage(false);
|
|
await tapTab('Features');
|
|
|
|
const motionEl = await driver.findElementRaw('name', 'Motion Detection')
|
|
|| await driver.findElementRaw('name', 'Motion');
|
|
expect(motionEl).not.toBeNull();
|
|
await driver.tapElement(motionEl!);
|
|
await sleep(3000);
|
|
|
|
reporter.record('移动监测开关', 'PASS', Date.now() - start, 'Motion Detection切换完成');
|
|
} 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 enterControlPage(false);
|
|
await tapTab('Features');
|
|
|
|
const trackEl = await driver.findElementRaw('name', 'Motion Tracking');
|
|
if (!trackEl) {
|
|
reporter.record('移动追踪', 'SKIP', Date.now() - start, '当前设备不支持Motion Tracking');
|
|
return;
|
|
}
|
|
await driver.tapElement(trackEl);
|
|
await sleep(3000);
|
|
|
|
reporter.record('移动追踪', 'PASS', Date.now() - start, 'Motion Tracking切换完成');
|
|
} 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 enterControlPage(false);
|
|
await tapTab('Features');
|
|
|
|
const alarmEl = await driver.findElementRaw('name', 'Sound Alert')
|
|
|| await driver.findElementRaw('name', 'Sound Alarm');
|
|
if (!alarmEl) {
|
|
reporter.record('声音报警', 'SKIP', Date.now() - start, '当前设备不支持Sound Alert');
|
|
return;
|
|
}
|
|
await driver.tapElement(alarmEl);
|
|
await sleep(3000);
|
|
|
|
// Toggle back
|
|
await driver.tapElement(alarmEl);
|
|
await sleep(2000);
|
|
|
|
reporter.record('声音报警', 'PASS', Date.now() - start, 'Sound Alert开/关完成');
|
|
} 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 enterControlPage(false);
|
|
await tapTab('Direction');
|
|
|
|
// PTZ controls - swipe in the direction pad area (center of screen below video)
|
|
const centerX = screenWidth / 2;
|
|
const centerY = 500;
|
|
|
|
// Swipe up
|
|
await driver.swipe(centerX, centerY, centerX, centerY - 80, 0.5);
|
|
await sleep(2000);
|
|
// Swipe left
|
|
await driver.swipe(centerX, centerY, centerX - 80, centerY, 0.5);
|
|
await sleep(2000);
|
|
// Swipe down
|
|
await driver.swipe(centerX, centerY, centerX, centerY + 80, 0.5);
|
|
await sleep(2000);
|
|
// Swipe right
|
|
await driver.swipe(centerX, centerY, centerX + 80, centerY, 0.5);
|
|
await sleep(2000);
|
|
|
|
reporter.record('方向操作', 'PASS', Date.now() - start, 'PTZ方向控制完成');
|
|
} 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 enterControlPage(false);
|
|
await tapTab('Features');
|
|
|
|
const privacyEl = await driver.findElementRaw('name', 'Privacy Mode')
|
|
|| await driver.findElementRaw('name', 'Privacy');
|
|
expect(privacyEl).not.toBeNull();
|
|
await driver.tapElement(privacyEl!);
|
|
await sleep(5000);
|
|
|
|
// Verify privacy mode state changed
|
|
let source = await driver.getSource();
|
|
const privacyOn = source.includes('Privacy') || source.includes('Turn on') || source.includes('turned off');
|
|
console.log('隐私模式:', privacyOn);
|
|
|
|
// If privacy mode turned on, restore by tapping again or "Turn on" button
|
|
if (source.includes('Turn on')) {
|
|
const turnOnEl = await driver.findElementRaw('name', 'Turn on');
|
|
if (turnOnEl) {
|
|
await driver.tapElement(turnOnEl);
|
|
await sleep(5000);
|
|
}
|
|
} else {
|
|
// Toggle back
|
|
const privacyEl2 = await driver.findElementRaw('name', 'Privacy Mode')
|
|
|| await driver.findElementRaw('name', 'Privacy');
|
|
if (privacyEl2) {
|
|
await driver.tapElement(privacyEl2);
|
|
await sleep(5000);
|
|
}
|
|
}
|
|
|
|
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 enterControlPage();
|
|
await tapVideoArea();
|
|
await tapFullscreenButton();
|
|
await sleep(3000);
|
|
|
|
// In fullscreen/landscape mode - tap center to show controls, then tap exit button
|
|
const size = await driver.getWindowSize();
|
|
await driver.tap(size.width / 2, size.height / 2);
|
|
await sleep(1000);
|
|
|
|
// Try tapping the fullscreen/exit button (typically top-right in landscape)
|
|
await driver.tap(size.width - 40, 40);
|
|
await sleep(2000);
|
|
|
|
// If still in landscape, force back to portrait
|
|
const sizeAfter = await driver.getWindowSize();
|
|
if (sizeAfter.width > sizeAfter.height) {
|
|
// Tap back area in landscape
|
|
await driver.tap(40, 40);
|
|
await sleep(2000);
|
|
}
|
|
|
|
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 enterControlPage();
|
|
|
|
// Double-tap video area to zoom in
|
|
const centerX = screenWidth / 2;
|
|
const videoY = 200;
|
|
await driver.doubleTap(centerX, videoY);
|
|
await sleep(3000);
|
|
|
|
// Double-tap again to zoom back out
|
|
await driver.doubleTap(centerX, videoY);
|
|
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;
|
|
}
|
|
});
|
|
|
|
// ==================== PTZ预置位 ====================
|
|
|
|
it('功能页PTZ预置位', async () => {
|
|
const start = Date.now();
|
|
try {
|
|
await enterControlPage(false);
|
|
await tapTab('Direction');
|
|
await sleep(2000);
|
|
|
|
// Check if Presets section is visible
|
|
const source = await driver.getSource();
|
|
if (!source.includes('Presets')) {
|
|
reporter.record('PTZ预置位', 'SKIP', Date.now() - start, 'Direction页无Presets区域');
|
|
return;
|
|
}
|
|
|
|
// Find the "+" add button: it's below the Presets header area
|
|
// In empty state, there's a large button in the center and a hint text
|
|
const presetsEl = await driver.findElementRaw('name', 'Presets');
|
|
if (!presetsEl) {
|
|
reporter.record('PTZ预置位', 'SKIP', Date.now() - start, '未找到Presets元素');
|
|
return;
|
|
}
|
|
|
|
// Look for the hint text to locate the add button precisely
|
|
const hintEl = await driver.findElementRaw('predicate string',
|
|
'name CONTAINS "Tap" AND name CONTAINS "add"');
|
|
if (hintEl) {
|
|
// The "+" button is above the hint text, in the center
|
|
const hintRect = await driver.getElementRect(hintEl);
|
|
await driver.tap(screenWidth / 2, hintRect.y - 50);
|
|
} else {
|
|
// Find buttons in the presets area below the header
|
|
const presetsRect = await driver.getElementRect(presetsEl);
|
|
const buttons = await driver.findElementsRaw('class name', 'XCUIElementTypeButton');
|
|
let addBtn: string | null = null;
|
|
for (const btn of buttons) {
|
|
const rect = await driver.getElementRect(btn);
|
|
if (rect.y > presetsRect.y + 50 && rect.y < presetsRect.y + 200
|
|
&& rect.width > 40 && rect.height > 40) {
|
|
addBtn = btn;
|
|
break;
|
|
}
|
|
}
|
|
if (addBtn) {
|
|
const addRect = await driver.getElementRect(addBtn);
|
|
await driver.tap(addRect.x + addRect.width / 2, addRect.y + addRect.height / 2);
|
|
} else {
|
|
// Last resort: tap center below Presets
|
|
await driver.tap(screenWidth / 2, presetsRect.y + 130);
|
|
}
|
|
}
|
|
await sleep(2000);
|
|
|
|
// Check if "Add Preset" dialog appeared
|
|
let yesEl = await driver.findElementRaw('name', 'Yes');
|
|
if (!yesEl) {
|
|
// Try tapping the small "+" icon next to Presets header (right side)
|
|
const presetsRect = await driver.getElementRect(presetsEl);
|
|
await driver.tap(screenWidth - 30, presetsRect.y + presetsRect.height / 2);
|
|
await sleep(2000);
|
|
yesEl = await driver.findElementRaw('name', 'Yes');
|
|
}
|
|
|
|
if (!yesEl) {
|
|
reporter.record('PTZ预置位', 'SKIP', Date.now() - start, '未弹出Add Preset对话框');
|
|
return;
|
|
}
|
|
|
|
// Tap Yes to add preset
|
|
await driver.tapElement(yesEl);
|
|
await sleep(8000);
|
|
|
|
// Verify preset was added
|
|
const sourceAfterAdd = await driver.getSource();
|
|
if (sourceAfterAdd.includes('You have not added')) {
|
|
reporter.record('PTZ预置位', 'SKIP', Date.now() - start, '预置位添加失败');
|
|
return;
|
|
}
|
|
|
|
// Find the newly added preset (e.g. "Presets1")
|
|
const presetEl = await driver.findElementRaw('predicate string',
|
|
'name BEGINSWITH "Presets" AND name != "Presets" AND type == "XCUIElementTypeStaticText"');
|
|
if (!presetEl) {
|
|
reporter.record('PTZ预置位', 'PASS', Date.now() - start, '预置位添加完成(未找到名称元素)');
|
|
return;
|
|
}
|
|
|
|
// Rename: Long press the preset
|
|
const presetRect = await driver.getElementRect(presetEl);
|
|
const cx = presetRect.x + presetRect.width / 2;
|
|
const cy = presetRect.y + presetRect.height / 2;
|
|
await driver.longPress(cx, cy, 2);
|
|
await sleep(1500);
|
|
|
|
const renameEl = await driver.findElementRaw('name', 'Rename');
|
|
if (renameEl) {
|
|
await driver.tapElement(renameEl);
|
|
await sleep(2000);
|
|
|
|
// Clear and type new name
|
|
const clearEl = await driver.findElementRaw('name', 'Clear text');
|
|
if (clearEl) await driver.tapElement(clearEl);
|
|
await sleep(300);
|
|
|
|
const textFields = await driver.findElementsRaw('class name', 'XCUIElementTypeTextField');
|
|
if (textFields.length > 0) {
|
|
await driver.typeText(textFields[0], 'TestPreset');
|
|
await sleep(500);
|
|
}
|
|
|
|
// Confirm rename
|
|
const yesRename = await driver.findElementRaw('name', 'Yes');
|
|
if (yesRename) {
|
|
await driver.tapElement(yesRename);
|
|
await sleep(3000);
|
|
}
|
|
}
|
|
|
|
// Delete: Long press again
|
|
await sleep(2000);
|
|
const presetEl2 = await driver.findElementRaw('predicate string',
|
|
'name CONTAINS "Preset" AND name != "Presets" AND type == "XCUIElementTypeStaticText"');
|
|
if (presetEl2) {
|
|
const presetRect2 = await driver.getElementRect(presetEl2);
|
|
await driver.longPress(presetRect2.x + presetRect2.width / 2, presetRect2.y + presetRect2.height / 2, 2);
|
|
await sleep(1500);
|
|
|
|
const deleteEl = await driver.findElementRaw('name', 'Delete');
|
|
if (deleteEl) {
|
|
await driver.tapElement(deleteEl);
|
|
await sleep(1500);
|
|
const confirmEl = await driver.findElementRaw('name', 'Confirm');
|
|
if (confirmEl) {
|
|
await driver.tapElement(confirmEl);
|
|
await sleep(3000);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify deletion
|
|
const finalSource = await driver.getSource();
|
|
const deleted = finalSource.includes('You have not added') || !finalSource.includes('TestPreset');
|
|
reporter.record('PTZ预置位', 'PASS', Date.now() - start, `预置位添加/重命名/删除完成, 删除=${deleted}`);
|
|
} catch (e: any) {
|
|
const ss = await driver.screenshot().catch(() => '');
|
|
reporter.record('PTZ预置位', 'FAIL', Date.now() - start, e.message, ss);
|
|
throw e;
|
|
}
|
|
});
|
|
|
|
|
|
// ==================== 翻转画面 ====================
|
|
|
|
it('功能页翻转画面', async () => {
|
|
const start = Date.now();
|
|
try {
|
|
await enterControlPage(false);
|
|
|
|
// Navigate to Settings by tapping the gear icon (top-right of nav bar)
|
|
await driver.tap(362, 69);
|
|
await sleep(3000);
|
|
|
|
// Verify we're on Settings page
|
|
let source = await driver.getSource();
|
|
if (!source.includes('More Features') && !source.includes('Motion Detection')) {
|
|
// Not on Settings page, try finding Settings element
|
|
const settingsEl = await driver.findElementRaw('name', 'Settings');
|
|
if (settingsEl) {
|
|
await driver.tapElement(settingsEl);
|
|
await sleep(2000);
|
|
source = await driver.getSource();
|
|
}
|
|
}
|
|
|
|
if (!source.includes('More Features')) {
|
|
// Scroll to find More Features
|
|
await driver.scrollDown(300);
|
|
await sleep(1000);
|
|
source = await driver.getSource();
|
|
}
|
|
|
|
// Tap "More Features"
|
|
const moreFeaturesEl = await driver.findElementRaw('name', 'More Features');
|
|
if (!moreFeaturesEl) {
|
|
reporter.record('翻转画面', 'SKIP', Date.now() - start, '未找到More Features');
|
|
return;
|
|
}
|
|
await driver.tapElement(moreFeaturesEl);
|
|
await sleep(2000);
|
|
|
|
// Tap "Video Display"
|
|
const videoDisplayEl = await driver.findElementRaw('name', 'Video Display');
|
|
if (!videoDisplayEl) {
|
|
reporter.record('翻转画面', 'SKIP', Date.now() - start, '未找到Video Display');
|
|
return;
|
|
}
|
|
await driver.tapElement(videoDisplayEl);
|
|
await sleep(2000);
|
|
|
|
// Find and toggle "Flip Screen"
|
|
const flipEl = await driver.findElementRaw('name', 'Flip Screen');
|
|
if (flipEl) {
|
|
// Find the switch associated with Flip Screen
|
|
const switches = await driver.findElementsRaw('class name', 'XCUIElementTypeSwitch');
|
|
let flipSwitch: string | null = null;
|
|
for (const sw of switches) {
|
|
const name = await driver.getElementAttribute(sw, 'name');
|
|
if (name && name.includes('Flip')) {
|
|
flipSwitch = sw;
|
|
break;
|
|
}
|
|
}
|
|
if (!flipSwitch && switches.length > 0) {
|
|
// Use the last switch (Flip Screen is typically the last item)
|
|
flipSwitch = switches[switches.length - 1];
|
|
}
|
|
|
|
if (flipSwitch) {
|
|
await driver.tapElement(flipSwitch);
|
|
await sleep(5000);
|
|
// Toggle back
|
|
await driver.tapElement(flipSwitch);
|
|
await sleep(3000);
|
|
reporter.record('翻转画面', 'PASS', Date.now() - start, 'Flip Screen切换完成');
|
|
} else {
|
|
// Try tapping the element directly
|
|
await driver.tapElement(flipEl);
|
|
await sleep(5000);
|
|
await driver.tapElement(flipEl);
|
|
await sleep(3000);
|
|
reporter.record('翻转画面', 'PASS', Date.now() - start, 'Flip Screen点击完成');
|
|
}
|
|
} else {
|
|
reporter.record('翻转画面', 'SKIP', Date.now() - start, '未找到Flip Screen');
|
|
}
|
|
} catch (e: any) {
|
|
const ss = await driver.screenshot().catch(() => '');
|
|
reporter.record('翻转画面', 'FAIL', Date.now() - start, e.message, ss);
|
|
throw e;
|
|
}
|
|
});
|
|
|
|
// ==================== LED指示灯 ====================
|
|
|
|
it('功能页LED指示灯开关', async () => {
|
|
const start = Date.now();
|
|
try {
|
|
await enterControlPage(false);
|
|
|
|
// Navigate to Settings by tapping gear icon (top-right of nav bar)
|
|
await driver.tap(362, 69);
|
|
await sleep(3000);
|
|
|
|
// Verify we're on Settings page
|
|
let source = await driver.getSource();
|
|
if (!source.includes('More Features') && !source.includes('Motion Detection')) {
|
|
const settingsEl = await driver.findElementRaw('name', 'Settings');
|
|
if (settingsEl) {
|
|
await driver.tapElement(settingsEl);
|
|
await sleep(2000);
|
|
source = await driver.getSource();
|
|
}
|
|
}
|
|
|
|
if (!source.includes('More Features')) {
|
|
await driver.scrollDown(300);
|
|
await sleep(1000);
|
|
}
|
|
|
|
// Tap "More Features"
|
|
const moreFeaturesEl = await driver.findElementRaw('name', 'More Features');
|
|
if (!moreFeaturesEl) {
|
|
reporter.record('LED指示灯', 'SKIP', Date.now() - start, '未找到More Features');
|
|
return;
|
|
}
|
|
await driver.tapElement(moreFeaturesEl);
|
|
await sleep(2000);
|
|
|
|
// Find "Indicator Light" and toggle its switch
|
|
const indicatorEl = await driver.findElementRaw('name', 'Indicator Light');
|
|
if (!indicatorEl) {
|
|
reporter.record('LED指示灯', 'SKIP', Date.now() - start, '当前设备不支持Indicator Light');
|
|
return;
|
|
}
|
|
|
|
// Find the switch for Indicator Light
|
|
const switches = await driver.findElementsRaw('class name', 'XCUIElementTypeSwitch');
|
|
let ledSwitch: string | null = null;
|
|
for (const sw of switches) {
|
|
const name = await driver.getElementAttribute(sw, 'name');
|
|
if (name && name.includes('Indicator')) {
|
|
ledSwitch = sw;
|
|
break;
|
|
}
|
|
}
|
|
if (!ledSwitch && switches.length > 0) {
|
|
ledSwitch = switches[0];
|
|
}
|
|
|
|
if (ledSwitch) {
|
|
await driver.tapElement(ledSwitch);
|
|
await sleep(5000);
|
|
// Toggle back
|
|
await driver.tapElement(ledSwitch);
|
|
await sleep(3000);
|
|
reporter.record('LED指示灯', 'PASS', Date.now() - start, 'Indicator Light切换完成');
|
|
} else {
|
|
// Try tapping the element directly
|
|
await driver.tapElement(indicatorEl);
|
|
await sleep(3000);
|
|
reporter.record('LED指示灯', 'PASS', Date.now() - start, 'Indicator Light点击完成');
|
|
}
|
|
} catch (e: any) {
|
|
const ss = await driver.screenshot().catch(() => '');
|
|
reporter.record('LED指示灯', 'FAIL', Date.now() - start, e.message, ss);
|
|
throw e;
|
|
}
|
|
});
|
|
|
|
// ==================== 移动监测灵敏度 ====================
|
|
|
|
it('功能页移动监测灵敏度', async () => {
|
|
const start = Date.now();
|
|
try {
|
|
await enterControlPage(false);
|
|
await tapTab('Features');
|
|
|
|
// First find and tap Motion Detection to enter its settings
|
|
const motionEl = await driver.findElementRaw('name', 'Motion Detection')
|
|
|| await driver.findElementRaw('name', 'Motion');
|
|
if (!motionEl) {
|
|
reporter.record('移动监测灵敏度', 'SKIP', Date.now() - start, '未找到Motion Detection');
|
|
return;
|
|
}
|
|
|
|
// Check if there's a sensitivity sub-option (may need long press or right-arrow)
|
|
const rect = await driver.getElementRect(motionEl);
|
|
// Tap the right side of the element (usually has a > arrow for settings)
|
|
await driver.tap(rect.x + rect.width - 20, rect.y + rect.height / 2);
|
|
await sleep(2000);
|
|
|
|
const source = await driver.getSource();
|
|
const hasSensitivity = source.includes('Sensitivity') || source.includes('灵敏度')
|
|
|| source.includes('High') || source.includes('Medium') || source.includes('Low');
|
|
|
|
if (hasSensitivity) {
|
|
// Try selecting a different sensitivity level
|
|
const medEl = await driver.findElementRaw('name', 'Medium')
|
|
|| await driver.findElementRaw('name', '中');
|
|
if (medEl) {
|
|
await driver.tapElement(medEl);
|
|
await sleep(2000);
|
|
}
|
|
reporter.record('移动监测灵敏度', 'PASS', Date.now() - start, '灵敏度设置完成');
|
|
} else {
|
|
// Toggle motion detection on/off as fallback
|
|
await driver.tapElement(motionEl);
|
|
await sleep(2000);
|
|
reporter.record('移动监测灵敏度', 'SKIP', Date.now() - start, '无灵敏度子设置');
|
|
}
|
|
} catch (e: any) {
|
|
const ss = await driver.screenshot().catch(() => '');
|
|
reporter.record('移动监测灵敏度', 'FAIL', Date.now() - start, e.message, ss);
|
|
throw e;
|
|
}
|
|
});
|
|
});
|