597 lines
20 KiB
TypeScript
597 lines
20 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 FullScreen Control - 摄像头全屏操作', () => {
|
|
let driver: DeviceDriver;
|
|
let reporter: TestReporter;
|
|
let screenWidth = 390;
|
|
let screenHeight = 844;
|
|
|
|
beforeAll(async () => {
|
|
driver = createDriver();
|
|
await driver.createSession();
|
|
reporter = new TestReporter('Camera_FullScreen', 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];
|
|
}
|
|
} 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 enterCameraPage(): 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')) break;
|
|
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')) break;
|
|
}
|
|
}
|
|
const checkSource = await driver.getSource();
|
|
if (!checkSource.includes('Direction') && !checkSource.includes('KB/S')) {
|
|
throw new Error('找不到摄像头卡片');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Wait for video stream (KB/S indicator)
|
|
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);
|
|
}
|
|
const source = await driver.getSource();
|
|
if (source.includes('Device online') || source.includes('设备在线')) {
|
|
await sleep(2000);
|
|
return;
|
|
}
|
|
throw new Error('视频流加载超时');
|
|
}
|
|
|
|
async function enterFullscreen(): Promise<void> {
|
|
// Tap video area to show overlay controls
|
|
await driver.tap(screenWidth / 2, 250);
|
|
await sleep(800);
|
|
// Tap fullscreen button (bottom-right of video overlay, last icon in bottom row)
|
|
await driver.tap(screenWidth - 28, 350);
|
|
await sleep(3000);
|
|
|
|
// Verify we entered fullscreen (no Tab Bar)
|
|
const source = await driver.getSource();
|
|
if (source.includes('Tab Bar')) {
|
|
// Retry: tap video and fullscreen again
|
|
await driver.tap(screenWidth / 2, 250);
|
|
await sleep(800);
|
|
await driver.tap(screenWidth - 28, 350);
|
|
await sleep(3000);
|
|
}
|
|
}
|
|
|
|
function isFullscreenSource(source: string): boolean {
|
|
return source.includes('SwitchBot')
|
|
&& !source.includes('Tab Bar')
|
|
&& !source.includes('Events')
|
|
&& !source.includes('Direction')
|
|
&& !source.includes('Features');
|
|
}
|
|
|
|
async function exitFullscreen(): Promise<void> {
|
|
// Tap center to show controls
|
|
await driver.tap(screenWidth / 2, screenHeight / 2);
|
|
await sleep(500);
|
|
// Find first button (exit/back) and click it
|
|
const buttons = await driver.findElementsRaw('class name', 'XCUIElementTypeButton');
|
|
if (buttons.length > 0) {
|
|
await driver.tapElement(buttons[0]);
|
|
} else {
|
|
// Retry tap center
|
|
await driver.tap(screenWidth / 2, screenHeight / 2);
|
|
await sleep(500);
|
|
const btns2 = await driver.findElementsRaw('class name', 'XCUIElementTypeButton');
|
|
if (btns2.length > 0) await driver.tapElement(btns2[0]);
|
|
}
|
|
await sleep(2000);
|
|
// Verify we exited (should have Tab Bar again)
|
|
const source = await driver.getSource();
|
|
if (!source.includes('Tab Bar') && !source.includes('Events')) {
|
|
// Still in fullscreen, try once more
|
|
await driver.tap(screenWidth / 2, screenHeight / 2);
|
|
await sleep(500);
|
|
const btns3 = await driver.findElementsRaw('class name', 'XCUIElementTypeButton');
|
|
if (btns3.length > 0) await driver.tapElement(btns3[0]);
|
|
await sleep(2000);
|
|
}
|
|
}
|
|
|
|
async function tapFullscreenControl(position: 'pause' | 'screenshot' | 'talk' | 'record' | 'speaker'): Promise<void> {
|
|
// In fullscreen (CSS rotated), controls are on the left side vertically
|
|
// pause~(70,458), screenshot~(70,534), talk~(70,610), record~(70,686)
|
|
// speaker is on right side at ~(348,581)
|
|
const positions: Record<string, { x: number; y: number }> = {
|
|
pause: { x: 70, y: 458 },
|
|
screenshot: { x: 70, y: 534 },
|
|
talk: { x: 70, y: 610 },
|
|
record: { x: 70, y: 686 },
|
|
speaker: { x: 348, y: 581 },
|
|
};
|
|
// Tap center to show controls first
|
|
await driver.tap(screenWidth / 2, screenHeight / 2);
|
|
await sleep(500);
|
|
const pos = positions[position];
|
|
await driver.tap(pos.x, pos.y);
|
|
await sleep(1000);
|
|
}
|
|
|
|
// ==================== 全屏画质切换 ====================
|
|
|
|
it('全屏-切换画质', async () => {
|
|
const start = Date.now();
|
|
try {
|
|
await enterCameraPage();
|
|
await enterFullscreen();
|
|
|
|
// Find HD/SD element and tap to toggle
|
|
const hdEl = await driver.findElementRaw('name', 'HD');
|
|
const sdEl = await driver.findElementRaw('name', 'SD');
|
|
const qualityEl = hdEl || sdEl;
|
|
const beforeQuality = hdEl ? 'HD' : 'SD';
|
|
|
|
if (!qualityEl) {
|
|
reporter.record('全屏-切换画质', 'SKIP', Date.now() - start, '未找到画质按钮');
|
|
await exitFullscreen();
|
|
return;
|
|
}
|
|
await driver.tapElement(qualityEl);
|
|
await sleep(8000);
|
|
|
|
const hdEl2 = await driver.findElementRaw('name', 'HD');
|
|
const sdEl2 = await driver.findElementRaw('name', 'SD');
|
|
const afterQuality = hdEl2 ? 'HD' : (sdEl2 ? 'SD' : 'unknown');
|
|
|
|
await exitFullscreen();
|
|
reporter.record('全屏-切换画质', 'PASS', Date.now() - start, `${beforeQuality} → ${afterQuality}`);
|
|
} 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 enterFullscreen();
|
|
|
|
// Tap speaker button (right side)
|
|
await tapFullscreenControl('speaker');
|
|
await sleep(3000);
|
|
|
|
// Toggle back
|
|
await tapFullscreenControl('speaker');
|
|
await sleep(2000);
|
|
|
|
await exitFullscreen();
|
|
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 enterFullscreen();
|
|
|
|
await tapFullscreenControl('screenshot');
|
|
await sleep(3000);
|
|
|
|
await exitFullscreen();
|
|
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 enterFullscreen();
|
|
|
|
// Start recording
|
|
await tapFullscreenControl('record');
|
|
await sleep(5000);
|
|
|
|
// Stop recording
|
|
await tapFullscreenControl('record');
|
|
await sleep(2000);
|
|
|
|
await exitFullscreen();
|
|
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 {
|
|
const cardId = await findCameraCard();
|
|
if (!cardId) throw new Error('找不到摄像头卡片');
|
|
await driver.tapElement(cardId);
|
|
await sleep(5000);
|
|
|
|
// Look for Settings tab or gear icon (top-right area)
|
|
const settingsEl = await driver.findElementRaw('name', 'Settings');
|
|
if (!settingsEl) {
|
|
// Try tapping top-right gear icon
|
|
const buttons = await driver.findElementsRaw('class name', 'XCUIElementTypeButton');
|
|
let gearBtn: string | null = null;
|
|
for (const btn of buttons) {
|
|
const rect = await driver.getElementRect(btn);
|
|
if (rect.x > 330 && rect.y < 80 && rect.width < 40) {
|
|
gearBtn = btn;
|
|
break;
|
|
}
|
|
}
|
|
if (gearBtn) {
|
|
await driver.tapElement(gearBtn);
|
|
await sleep(2000);
|
|
} else {
|
|
reporter.record('全屏-设置单向对讲', 'SKIP', Date.now() - start, '未找到Settings入口');
|
|
return;
|
|
}
|
|
} else {
|
|
await driver.tapElement(settingsEl);
|
|
await sleep(2000);
|
|
}
|
|
|
|
const basicEl = await driver.findElementRaw('name', 'Basic Settings');
|
|
if (basicEl) {
|
|
await driver.tapElement(basicEl);
|
|
await sleep(1500);
|
|
const talkEl = await driver.findElementRaw('name', 'Talk Mode');
|
|
if (talkEl) {
|
|
await driver.tapElement(talkEl);
|
|
await sleep(1500);
|
|
const oneWayEl = await driver.findElementRaw('name', 'One-way');
|
|
if (oneWayEl) {
|
|
await driver.tapElement(oneWayEl);
|
|
await sleep(3000);
|
|
}
|
|
}
|
|
}
|
|
|
|
const source = await driver.getSource();
|
|
const confirmed = source.includes('One-way') || source.includes('Talk Mode');
|
|
reporter.record('全屏-设置单向对讲', 'PASS', Date.now() - start, `One-way设置=${confirmed}`);
|
|
} 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 enterFullscreen();
|
|
|
|
// Tap talk button (left side, 3rd from top)
|
|
await tapFullscreenControl('talk');
|
|
await sleep(5000);
|
|
|
|
// Stop talk
|
|
await tapFullscreenControl('talk');
|
|
await sleep(2000);
|
|
|
|
await exitFullscreen();
|
|
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 {
|
|
const cardId = await findCameraCard();
|
|
if (!cardId) throw new Error('找不到摄像头卡片');
|
|
await driver.tapElement(cardId);
|
|
await sleep(5000);
|
|
|
|
const settingsEl = await driver.findElementRaw('name', 'Settings');
|
|
if (!settingsEl) {
|
|
const buttons = await driver.findElementsRaw('class name', 'XCUIElementTypeButton');
|
|
let gearBtn: string | null = null;
|
|
for (const btn of buttons) {
|
|
const rect = await driver.getElementRect(btn);
|
|
if (rect.x > 330 && rect.y < 80 && rect.width < 40) {
|
|
gearBtn = btn;
|
|
break;
|
|
}
|
|
}
|
|
if (gearBtn) {
|
|
await driver.tapElement(gearBtn);
|
|
await sleep(2000);
|
|
} else {
|
|
reporter.record('全屏-设置双向对讲', 'SKIP', Date.now() - start, '未找到Settings入口');
|
|
return;
|
|
}
|
|
} else {
|
|
await driver.tapElement(settingsEl);
|
|
await sleep(2000);
|
|
}
|
|
|
|
const basicEl = await driver.findElementRaw('name', 'Basic Settings');
|
|
if (basicEl) {
|
|
await driver.tapElement(basicEl);
|
|
await sleep(1500);
|
|
const talkEl = await driver.findElementRaw('name', 'Talk Mode');
|
|
if (talkEl) {
|
|
await driver.tapElement(talkEl);
|
|
await sleep(1500);
|
|
const twoWayEl = await driver.findElementRaw('name', 'Two-way');
|
|
if (twoWayEl) {
|
|
await driver.tapElement(twoWayEl);
|
|
await sleep(3000);
|
|
}
|
|
}
|
|
}
|
|
|
|
const source = await driver.getSource();
|
|
const confirmed = source.includes('Two-way') || source.includes('Talk Mode');
|
|
reporter.record('全屏-设置双向对讲', 'PASS', Date.now() - start, `Two-way设置=${confirmed}`);
|
|
} 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 enterFullscreen();
|
|
|
|
// Tap talk button
|
|
await tapFullscreenControl('talk');
|
|
await sleep(5000);
|
|
|
|
// Stop talk
|
|
await tapFullscreenControl('talk');
|
|
await sleep(2000);
|
|
|
|
await exitFullscreen();
|
|
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 enterFullscreen();
|
|
|
|
// Pause
|
|
await tapFullscreenControl('pause');
|
|
await sleep(3000);
|
|
|
|
// Resume
|
|
await tapFullscreenControl('pause');
|
|
await sleep(3000);
|
|
|
|
// Verify still in fullscreen and streaming
|
|
const source = await driver.getSource();
|
|
const stillFullscreen = isFullscreenSource(source);
|
|
|
|
await exitFullscreen();
|
|
|
|
// Verify not disconnected after exit
|
|
await sleep(1000);
|
|
const sourceAfter = await driver.getSource();
|
|
const isLive = !sourceAfter.includes('Disconnected');
|
|
|
|
reporter.record('全屏-播放暂停', 'PASS', Date.now() - start, `暂停/恢复完成, fullscreen=${stillFullscreen}, live=${isLive}`);
|
|
} catch (e: any) {
|
|
const ss = await driver.screenshot().catch(() => '');
|
|
reporter.record('全屏-播放暂停', 'FAIL', Date.now() - start, e.message, ss);
|
|
throw e;
|
|
}
|
|
});
|
|
|
|
// ==================== 全屏方向操作(PTZ) ====================
|
|
|
|
it('全屏-方向操作', async () => {
|
|
const start = Date.now();
|
|
try {
|
|
await enterCameraPage();
|
|
await enterFullscreen();
|
|
|
|
// In fullscreen, swipe to control PTZ direction
|
|
const centerX = screenWidth / 2;
|
|
const centerY = screenHeight / 2;
|
|
|
|
// Swipe up (tilt up)
|
|
await driver.swipe(centerX, centerY, centerX, centerY - 100, 0.5);
|
|
await sleep(2000);
|
|
// Swipe left (pan left)
|
|
await driver.swipe(centerX, centerY, centerX - 100, centerY, 0.5);
|
|
await sleep(2000);
|
|
// Swipe down (tilt down)
|
|
await driver.swipe(centerX, centerY, centerX, centerY + 100, 0.5);
|
|
await sleep(2000);
|
|
// Swipe right (pan right)
|
|
await driver.swipe(centerX, centerY, centerX + 100, centerY, 0.5);
|
|
await sleep(2000);
|
|
|
|
await exitFullscreen();
|
|
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 enterCameraPage();
|
|
await enterFullscreen();
|
|
|
|
const centerX = screenWidth / 2;
|
|
const centerY = screenHeight / 2;
|
|
|
|
// Double-tap to zoom in
|
|
await driver.doubleTap(centerX, centerY);
|
|
await sleep(3000);
|
|
|
|
// Double-tap to zoom back out
|
|
await driver.doubleTap(centerX, centerY);
|
|
await sleep(3000);
|
|
|
|
await exitFullscreen();
|
|
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 enterFullscreen();
|
|
|
|
const centerX = screenWidth / 2;
|
|
const centerY = screenHeight / 2;
|
|
|
|
// Double-tap to zoom in
|
|
await driver.doubleTap(centerX, centerY);
|
|
await sleep(3000);
|
|
|
|
// Verify still in fullscreen
|
|
const source = await driver.getSource();
|
|
const stillFullscreen = isFullscreenSource(source);
|
|
|
|
// Double-tap to zoom back out
|
|
await driver.doubleTap(centerX, centerY);
|
|
await sleep(3000);
|
|
|
|
await exitFullscreen();
|
|
reporter.record('全屏-双击缩放', 'PASS', Date.now() - start, `全屏双击缩放完成, fullscreen=${stillFullscreen}`);
|
|
} catch (e: any) {
|
|
const ss = await driver.screenshot().catch(() => '');
|
|
reporter.record('全屏-双击缩放', 'FAIL', Date.now() - start, e.message, ss);
|
|
throw e;
|
|
}
|
|
});
|
|
});
|