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 { 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 { // 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 { // 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 { // 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 { // 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 = { 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; } }); });