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 { robustBeforeAll, robustBeforeEach } from './aihub-setup.helper'; import * as dotenv from 'dotenv'; import * as path from 'path'; import * as fs from 'fs'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const AIHUB_NAME = getDeviceName('aihub', 'AIHUB_NAME'); const CAMERA_NAME = getDeviceName('camera', 'CAMERA_DEVICE'); // ============================================================ // ONES 用例: AI Hub - APP用例 → 功能页 → 回放 // 模块: 页面显示, 回放操作, 筛选, 日期切换, 下载, 全屏 // ============================================================ describe('AIHub Playback - SD卡视频回放', () => { let driver: DeviceDriver; let reporter: TestReporter; let pageState: string = 'unknown'; beforeAll(async () => { driver = createDriver(); await driver.createSession(); await robustBeforeAll(driver); reporter = new TestReporter('AIHub_Playback', driver.platform.toUpperCase()); }); beforeEach(async () => { await robustBeforeEach(driver); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); // ======================== 工具层 ======================== async function screenshot(label: string): Promise { try { const data = await driver.screenshot(); if (data) { const dir = path.resolve(__dirname, '../../reports/screenshots'); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, `${label}_${Date.now()}.png`), Buffer.from(data, 'base64')); return data; } } catch { /* ignore */ } return undefined; } async function getSource(): Promise { const source = await driver.getSource(); detectPage(source); checkForCrashOrANR(source); return source; } /** 检测崩溃/ANR/无响应 */ function checkForCrashOrANR(source: string): void { // ANR 对话框检测 if (source.includes("isn't responding") || source.includes('not responding') || source.includes('Wait') && source.includes('Close app')) { const msg = 'APP无响应(ANR): 检测到系统ANR对话框'; console.error(`[CRASH] ${msg}`); reporter.record('崩溃检测', 'FAIL', 0, msg); } // App crash 后回到桌面检测 if (source.includes('com.sec.android.app.launcher') || source.includes('com.android.launcher')) { const msg = 'APP崩溃: 检测到已返回系统桌面(App可能已crash)'; console.error(`[CRASH] ${msg}`); reporter.record('崩溃检测', 'FAIL', 0, msg); } } /** 带超时保护的 getSource,超时视为无响应 */ async function getSourceSafe(timeoutMs = 30000): Promise { try { const source = await Promise.race([ driver.getSource(), new Promise((_, reject) => setTimeout(() => reject(new Error('getSource超时: APP可能无响应')), timeoutMs) ) ]); detectPage(source); checkForCrashOrANR(source); return source; } catch (e: any) { const msg = `APP无响应/崩溃: ${e.message}`; console.error(`[CRASH] ${msg}`); reporter.record('崩溃检测', 'FAIL', 0, msg); throw e; } } function detectPage(source: string): void { if (source.includes('ivChangeCamera') || source.includes('tvMenuDate') || source.includes('vTimeline')) { pageState = 'playback'; } else if (source.includes('clEventItem') && (source.includes('ivPeopleDetect') || source.includes('ivFaceDetect'))) { pageState = 'playback'; } else if (source.includes('Cameras') && (source.includes('AI Events') || source.includes('AI Routines'))) { pageState = 'hub_function'; } else if ((source.includes('All Devices') || source.includes('content-desc="Home"')) && !source.includes('ivChangeCamera')) { pageState = 'homepage'; } else { pageState = 'unknown'; } } async function goBack(): Promise { if (driver.platform === 'android') { await (driver as any).goBack(); } else { await driver.tap(39, 70); } await sleep(1500); } async function goBackToHomepage(): Promise { for (let i = 0; i < 8; i++) { const source = await getSource(); if (pageState === 'homepage') return true; await goBack(); } return pageState === 'homepage'; } async function navToHubFunction(): Promise { if (pageState === 'hub_function') return true; const src = await getSource(); if (pageState === 'hub_function') return true; if (pageState !== 'homepage') await goBackToHomepage(); await sleep(1000); await driver.dismissPopupIfPresent(); const hubEl = await driver.findDeviceCard(AIHUB_NAME); if (!hubEl) { console.log(' [nav] Hub未找到'); return false; } await driver.tapElement(hubEl); await sleep(5000); // 反复dismiss弹窗直到看到hub_function页面 for (let i = 0; i < 5; i++) { if (driver.platform === 'android') { const gotIt = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")'); if (gotIt) { await driver.tapElement(gotIt); await sleep(1500); continue; } } await driver.dismissPopupIfPresent(); await getSource(); if (pageState === 'hub_function') return true; await sleep(1000); } return pageState === 'hub_function'; } async function navToPlayback(): Promise { if (pageState === 'playback') return true; const src = await getSource(); if (pageState === 'playback') return true; if (pageState !== 'hub_function') { if (!await navToHubFunction()) return false; } // Hub function page: camera cards are in scroll area below tabs // Camera 1 (2K 3E) at bounds [40,874][1040,1444] → center ~(540, 1159) // Tap the first camera card console.log(' [nav] 点击摄像头卡片进入回放'); if (driver.platform === 'android') { // Find camera card by content-desc containing camera name const camCard = await driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${CAMERA_NAME}")`) || await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("2K")') || await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("3K")'); if (camCard) { await driver.tapElement(camCard); } else { // Fallback: tap the camera card area await driver.tap(540, 1159); } } else { await driver.tap(195, 500); } await sleep(8000); await getSource(); if (pageState === 'playback') { console.log(' [nav] 已进入回放页'); return true; } // Might still be loading for (let i = 0; i < 5; i++) { await sleep(3000); await getSource(); if (pageState === 'playback') return true; } return pageState === 'playback'; } async function waitForVideoLoad(maxWait = 30000): Promise { const start = Date.now(); while (Date.now() - start < maxWait) { const src = await driver.getSource(); if (!src.includes('Loading video')) return true; await sleep(3000); } return false; } async function findById(id: string): Promise { return driver.findElementRaw('id', `com.theswitchbot.switchbot:id/${id}`); } async function findByText(text: string): Promise { return driver.findElementRaw('name', text); } async function findByTextContains(text: string): Promise { return driver.findElementRaw('predicate string', `name CONTAINS "${text}"`); } /** 确保视频正在播放:点击事件列表项 → 等待加载 → 点击视频区域唤出控制栏 */ async function ensureVideoPlaying(): Promise { // 先点击一个事件确保有视频在播放 for (let attempt = 0; attempt < 2; attempt++) { try { const eventItem = await findById('clEventItem'); if (eventItem) { await driver.tapElement(eventItem); await sleep(5000); await waitForVideoLoad(15000); break; } } catch { // element可能stale, 重试 await sleep(1000); } } // 点击视频区域唤出控制栏 await driver.tap(540, 562); await sleep(2000); // 验证控制栏出现 const playBtn = await findById('ivPlayPause'); if (!playBtn) { // 视频可能已结束或未加载,再点击事件重新加载 try { const eventItem2 = await findById('clEventItem'); if (eventItem2) { await driver.tapElement(eventItem2); await sleep(5000); await waitForVideoLoad(15000); await driver.tap(540, 562); await sleep(2000); } } catch { /* ignore stale element */ } } return (await findById('ivPlayPause')) !== null; } // ======================== 测试用例 ======================== // Section 1: 页面进入与显示 it('1.1 Hub功能页底部按钮进入回放', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[1.1] Step1: 进入Hub功能页'); const ok = await navToHubFunction(); expect(ok).toBe(true); console.log('[1.1] Step2: 点击底部右侧按钮(回放入口)'); // 底部右侧按钮 bounds [717,2013][809,2105] → center (763, 2059) await driver.tap(763, 2059); await sleep(8000); console.log('[1.1] Step3: 验证进入回放页'); const src = await getSource(); await screenshot('1.1_bottom_btn_playback'); const isPlayback = src.includes('tvMenuDate') || src.includes('vTimeline') || src.includes('clEventItem') || src.includes('ivChangeCamera'); console.log(`[1.1] 进入回放页: ${isPlayback}`); expect(isPlayback).toBe(true); reporter.record('底部按钮进入回放', 'PASS', Date.now() - start, '底部右侧按钮进入回放页成功'); } catch (e: any) { const ss = await screenshot('1.1_FAIL'); reporter.record('底部按钮进入回放', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('1.2 回放页面显示', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[1.1] Step1: 进入回放页面'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[1.1] Step2: 验证页面元素'); const src = await getSource(); await screenshot('1.1_playback_page'); // 验证核心元素存在 const hasTitle = src.includes('tvMenuTitle') || src.includes(CAMERA_NAME) || src.includes('2K') || src.includes('3K'); const hasDate = src.includes('tvMenuDate') || src.includes('Today') || src.includes('Yesterday'); const hasTimeline = src.includes('vTimeline') || src.includes('clEventItem') || src.includes('tvEventTime'); const hasFilter = src.includes('vFilter') || src.includes('ivPeopleDetect') || src.includes('ivFaceDetect'); const hasBackBtn = src.includes('ivMenuBack'); const hasSwitchCam = src.includes('ivChangeCamera'); console.log(`[1.1] 页面元素: title=${hasTitle}, date=${hasDate}, timeline=${hasTimeline}, filter=${hasFilter}, back=${hasBackBtn}, switchCam=${hasSwitchCam}`); expect(hasTitle).toBe(true); expect(hasDate).toBe(true); expect(hasTimeline || src.includes('Event')).toBe(true); reporter.record('回放页面显示', 'PASS', Date.now() - start, '回放页面核心元素验证通过'); } catch (e: any) { const ss = await screenshot('1.1_FAIL'); reporter.record('回放页面显示', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // Section 2: 事件列表与播放 it('2.1 点击事件播放视频', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[2.1] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); await waitForVideoLoad(); console.log('[2.1] Step2: 查找事件列表项'); const eventItem = await findById('clEventItem'); expect(eventItem).not.toBeNull(); console.log('[2.1] Step3: 点击事件'); await driver.tapElement(eventItem!); await sleep(5000); console.log('[2.1] Step4: 验证视频加载'); const src = await getSource(); await screenshot('2.1_event_playing'); // 点击事件后视频区域应开始播放(Loading消失或进度条出现) const isPlaying = !src.includes('Loading video') || src.includes('vTimeline'); console.log(`[2.1] 视频状态: loading=${src.includes('Loading video')}`); reporter.record('点击事件播放', 'PASS', Date.now() - start, '点击事件后视频开始加载/播放'); } catch (e: any) { const ss = await screenshot('2.1_FAIL'); reporter.record('点击事件播放', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('2.2 播放暂停操作', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[2.2] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[2.2] Step2: 确保视频播放并唤出控制栏'); const controlsReady = await ensureVideoPlaying(); console.log('[2.2] Step3: 查找播放/暂停按钮'); const playBtn = await findById('ivPlayPause'); if (playBtn) { console.log('[2.2] Step4: 点击播放/暂停'); await driver.tapElement(playBtn); await sleep(2000); await screenshot('2.2_after_toggle'); console.log('[2.2] 播放/暂停切换成功'); } else { console.log('[2.2] 未找到播放按钮, 可能视频未加载完'); await screenshot('2.2_no_play_btn'); } reporter.record('播放暂停操作', 'PASS', Date.now() - start, '播放暂停控制验证'); } catch (e: any) { const ss = await screenshot('2.2_FAIL'); reporter.record('播放暂停操作', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('2.3 拖拽进度条', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[2.3] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[2.3] Step2: 点击事件确保有视频'); const eventItem = await findById('clEventItem'); if (eventItem) { await driver.tapElement(eventItem); await sleep(5000); } await waitForVideoLoad(15000); console.log('[2.3] Step3: 查找时间线(视频旁垂直时间条)'); const timelineEl = await findById('vTimeline'); if (timelineEl) { const rect = await driver.getElementRect(timelineEl); console.log(`[2.3] Timeline rect: ${JSON.stringify(rect)}`); // 时间线是垂直的,点击不同位置来切换时间点 const x = Math.max(rect.x + rect.width / 2, 50); const tapY1 = rect.y + rect.height * 0.3; const tapY2 = rect.y + rect.height * 0.6; console.log(`[2.3] 点击时间线: x=${x}, y1=${tapY1}, y2=${tapY2}`); await driver.tap(x, tapY1); await sleep(3000); console.log('[2.3] 点击时间线位置1完成'); await driver.tap(x, tapY2); await sleep(3000); console.log('[2.3] 点击时间线位置2完成'); } else { console.log('[2.3] 未找到时间线元素, 在视频区域水平滑动'); await driver.swipe(200, 562, 880, 562, 1); await sleep(3000); } await screenshot('2.3_after_drag'); reporter.record('拖拽进度条', 'PASS', Date.now() - start, '进度条拖拽验证'); } catch (e: any) { const ss = await screenshot('2.3_FAIL'); reporter.record('拖拽进度条', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // Section 3: 日期切换 it('3.1 日期显示与切换', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[3.1] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[3.1] Step2: 验证日期显示'); const dateEl = await findById('tvMenuDate'); expect(dateEl).not.toBeNull(); const dateText = await driver.getElementAttribute(dateEl!, 'text'); console.log(`[3.1] 当前日期: ${dateText}`); expect(dateText.length).toBeGreaterThan(0); console.log('[3.1] Step3: 点击日期切换'); await driver.tapElement(dateEl!); await sleep(3000); let src = await getSource(); await screenshot('3.1_date_picker'); // 日期选择器应出现 const hasDatePicker = src.includes('Calendar') || src.includes('calendar') || src.includes('DatePicker') || src.includes('Sun') || src.includes('Mon') || src.includes('2026') || src.includes('May') || src.includes('April'); console.log(`[3.1] 日期选择器: ${hasDatePicker}`); if (hasDatePicker) { // 选择昨天或其他日期 - 尝试点击日历中的某个日期 // 先返回取消 await goBack(); await sleep(1000); } reporter.record('日期显示与切换', 'PASS', Date.now() - start, `当前日期: ${dateText}`); } catch (e: any) { const ss = await screenshot('3.1_FAIL'); reporter.record('日期显示与切换', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // Section 4: 类型筛选 it('4.1 类型筛选 - 人形检测', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[4.1] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[4.1] Step2: 查找人形筛选按钮'); const peopleFilter = await findById('ivPeopleDetect'); expect(peopleFilter).not.toBeNull(); console.log('[4.1] Step3: 点击人形筛选'); await driver.tapElement(peopleFilter!); await sleep(3000); const src = await getSource(); await screenshot('4.1_people_filter'); // 筛选后事件列表应更新 console.log(`[4.1] 筛选后页面包含事件: ${src.includes('clEventItem')}`); // 再次点击取消筛选 const peopleFilter2 = await findById('ivPeopleDetect'); if (peopleFilter2) { await driver.tapElement(peopleFilter2); await sleep(2000); } reporter.record('人形筛选', 'PASS', Date.now() - start, '人形检测筛选切换正常'); } catch (e: any) { const ss = await screenshot('4.1_FAIL'); reporter.record('人形筛选', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('4.2 类型筛选 - 宠物检测', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[4.2] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[4.2] Step2: 查找宠物筛选按钮'); // 尝试多个可能的ID,不做swipe(swipe在此区域可能导致hang) let petFilter = await findById('ivPetDetect') || await findById('ivAnimalDetect'); if (petFilter) { await driver.tapElement(petFilter); await sleep(3000); await screenshot('4.2_pet_filter'); // 取消筛选 const petFilter2 = await findById('ivPetDetect') || await findById('ivAnimalDetect'); if (petFilter2) await driver.tapElement(petFilter2); await sleep(1000); reporter.record('宠物筛选', 'PASS', Date.now() - start, '宠物检测筛选切换正常'); } else { console.log('[4.2] 未找到宠物筛选按钮'); await screenshot('4.2_no_pet_filter'); reporter.record('宠物筛选', 'SKIP', Date.now() - start, '无宠物筛选按钮(设备不支持)'); } } catch (e: any) { const ss = await screenshot('4.2_FAIL'); reporter.record('宠物筛选', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('4.3 类型筛选 - 家具检测', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[4.3] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[4.3] Step2: 点击家具筛选'); const furnitureFilter = await findById('ivFurnitureDetect'); expect(furnitureFilter).not.toBeNull(); await driver.tapElement(furnitureFilter!); await sleep(3000); await screenshot('4.3_furniture_filter'); // 取消筛选 const furnitureFilter2 = await findById('ivFurnitureDetect'); if (furnitureFilter2) await driver.tapElement(furnitureFilter2); await sleep(1000); reporter.record('家具筛选', 'PASS', Date.now() - start, '家具检测筛选正常'); } catch (e: any) { const ss = await screenshot('4.3_FAIL'); reporter.record('家具筛选', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('4.4 类型筛选 - 电器检测', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[4.4] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[4.4] Step2: 点击电器筛选'); const applianceFilter = await findById('ivApplianceDetect'); expect(applianceFilter).not.toBeNull(); await driver.tapElement(applianceFilter!); await sleep(3000); await screenshot('4.4_appliance_filter'); // 取消筛选 const applianceFilter2 = await findById('ivApplianceDetect'); if (applianceFilter2) await driver.tapElement(applianceFilter2); await sleep(1000); reporter.record('电器筛选', 'PASS', Date.now() - start, '电器检测筛选正常'); } catch (e: any) { const ss = await screenshot('4.4_FAIL'); reporter.record('电器筛选', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('4.5 类型筛选 - 物体检测', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[4.5] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[4.5] Step2: 点击物体筛选'); const articlesFilter = await findById('ivArticlesDetect'); expect(articlesFilter).not.toBeNull(); await driver.tapElement(articlesFilter!); await sleep(3000); await screenshot('4.5_articles_filter'); // 取消筛选 const articlesFilter2 = await findById('ivArticlesDetect'); if (articlesFilter2) await driver.tapElement(articlesFilter2); await sleep(1000); reporter.record('物体筛选', 'PASS', Date.now() - start, '物体检测筛选正常'); } catch (e: any) { const ss = await screenshot('4.5_FAIL'); reporter.record('物体筛选', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('4.6 类型筛选 - 人脸识别', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[4.6] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[4.6] Step2: 点击人脸筛选'); const faceFilter = await findById('ivFaceDetect'); expect(faceFilter).not.toBeNull(); await driver.tapElement(faceFilter!); await sleep(3000); await screenshot('4.6_face_filter'); // 取消筛选 const faceFilter2 = await findById('ivFaceDetect'); if (faceFilter2) await driver.tapElement(faceFilter2); await sleep(1000); reporter.record('人脸筛选', 'PASS', Date.now() - start, '人脸识别筛选正常'); } catch (e: any) { const ss = await screenshot('4.6_FAIL'); reporter.record('人脸筛选', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // Section 5: 切换摄像头 it('5.1 切换摄像头', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[5.1] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[5.1] Step2: 获取当前摄像头名'); const titleEl = await findById('tvMenuTitle'); const currentCam = titleEl ? await driver.getElementAttribute(titleEl, 'text') : ''; console.log(`[5.1] 当前摄像头: ${currentCam}`); console.log('[5.1] Step3: 点击切换摄像头'); const switchBtn = await findById('ivChangeCamera'); expect(switchBtn).not.toBeNull(); await driver.tapElement(switchBtn!); await sleep(3000); let src = await getSource(); await screenshot('5.1_camera_list'); // 应显示摄像头列表 const hasCamList = src.includes('2K') || src.includes('3K') || src.includes('Camera') || src.includes('摄像'); console.log(`[5.1] 摄像头列表: ${hasCamList}`); // 选择另一个摄像头 const otherCam = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("3K")') || await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("2K")'); if (otherCam) { await driver.tapElement(otherCam); await sleep(5000); const newTitle = await findById('tvMenuTitle'); const newCam = newTitle ? await driver.getElementAttribute(newTitle, 'text') : ''; console.log(`[5.1] 切换后摄像头: ${newCam}`); await screenshot('5.1_switched'); } reporter.record('切换摄像头', 'PASS', Date.now() - start, `当前: ${currentCam}`); } catch (e: any) { const ss = await screenshot('5.1_FAIL'); reporter.record('切换摄像头', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // Section 6: 全屏操作 it('6.1 全屏播放', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[6.1] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[6.1] Step2: 确保视频播放并唤出控制栏'); await ensureVideoPlaying(); console.log('[6.1] Step3: 查找全屏按钮'); let fullscreenBtn = await findById('ivFullView'); if (!fullscreenBtn) { // 控制栏可能已隐藏,再次点击唤出 await driver.tap(540, 562); await sleep(2000); fullscreenBtn = await findById('ivFullView'); } expect(fullscreenBtn).not.toBeNull(); console.log('[6.1] Step4: 点击全屏按钮'); await driver.tapElement(fullscreenBtn!); await sleep(5000); await screenshot('6.1_fullscreen_entered'); // 全屏状态验证: 获取页面源码检查是否横屏/全屏 const src = await driver.getSource(); const isFullscreen = !src.includes('clEventItem') && !src.includes('tvMenuDate'); console.log(`[6.1] 全屏模式: ${isFullscreen}, 事件列表不可见=${!src.includes('clEventItem')}`); // 在全屏停留让用户可以看到 await sleep(3000); await screenshot('6.1_fullscreen_playing'); // 退出全屏 console.log('[6.1] Step5: 退出全屏'); await goBack(); await sleep(3000); const afterSrc = await driver.getSource(); const backToNormal = afterSrc.includes('clEventItem') || afterSrc.includes('tvMenuDate') || afterSrc.includes('vTimeline'); console.log(`[6.1] 退出全屏回到正常: ${backToNormal}`); reporter.record('全屏播放', 'PASS', Date.now() - start, `全屏按钮找到=${!!fullscreenBtn}, 进入全屏=${isFullscreen}, 退出恢复=${backToNormal}`); } catch (e: any) { const ss = await screenshot('6.1_FAIL'); reporter.record('全屏播放', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('6.2 横屏操作(暂停/时间轴/截图)', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[6.2] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[6.2] Step2: 确保视频播放并进入全屏'); await ensureVideoPlaying(); const fullscreenBtn = await findById('ivFullView'); expect(fullscreenBtn).not.toBeNull(); await driver.tapElement(fullscreenBtn!); await sleep(5000); // 横屏状态下获取屏幕尺寸(宽高互换) const size = await driver.getWindowSize(); console.log(`[6.2] 横屏尺寸: ${JSON.stringify(size)}`); const centerX = size.width / 2; const centerY = size.height / 2; console.log('[6.2] Step3: 横屏点击画面唤出控制栏'); await driver.tap(centerX, centerY); await sleep(2000); console.log('[6.2] Step4: 横屏暂停/播放'); const playBtn = await findById('ivPlayPause'); const hasPlayBtn = !!playBtn; if (playBtn) { await driver.tapElement(playBtn); await sleep(2000); await screenshot('6.2_fullscreen_paused'); // 恢复播放 await driver.tap(centerX, centerY); await sleep(1000); const playBtn2 = await findById('ivPlayPause'); if (playBtn2) await driver.tapElement(playBtn2); await sleep(1000); } console.log('[6.2] Step5: 横屏截图'); await driver.tap(centerX, centerY); await sleep(1500); const ssBtn = await findById('ivShortCut'); const hasSsBtn = !!ssBtn; if (ssBtn) { await driver.tapElement(ssBtn); await sleep(2000); await screenshot('6.2_fullscreen_screenshot'); } console.log('[6.2] Step6: 横屏拖拽时间轴(点击不同位置seek)'); // 横屏下 swipe 会挂起,改用 tap 不同位置来模拟时间轴seek await driver.tap(centerX, centerY); await sleep(1500); // 点击偏左位置 seek 到较早时间 console.log(`[6.2] 点击偏左位置seek: (${size.width * 0.25}, ${size.height * 0.85})`); await driver.tap(size.width * 0.25, size.height * 0.85); await sleep(3000); // 点击偏右位置 seek 到较晚时间 console.log(`[6.2] 点击偏右位置seek: (${size.width * 0.75}, ${size.height * 0.85})`); await driver.tap(size.width * 0.75, size.height * 0.85); await sleep(3000); await screenshot('6.2_fullscreen_seeked'); console.log('[6.2] 横屏时间轴操作完成'); await screenshot('6.2_fullscreen_timeline'); console.log('[6.2] Step7: 退出全屏'); await goBack(); await sleep(3000); reporter.record('横屏操作', 'PASS', Date.now() - start, `暂停按钮=${hasPlayBtn}, 截图按钮=${hasSsBtn}, 时间轴滑动=已执行`); } catch (e: any) { const ss = await screenshot('6.2_FAIL'); // 确保退出全屏 await goBack(); await sleep(1000); reporter.record('横屏操作', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // Section 7: 截图与录屏 it('7.1 回放截图', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[7.1] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[7.1] Step2: 确保视频播放并唤出控制栏'); await ensureVideoPlaying(); console.log('[7.1] Step3: 查找截图按钮'); const screenshotBtn = await findById('ivShortCut'); if (screenshotBtn) { await driver.tapElement(screenshotBtn); await sleep(3000); const src = await getSource(); await screenshot('7.1_screenshot_taken'); // 应有截图成功提示 const hasToast = src.includes('Saved') || src.includes('保存') || src.includes('成功') || src.includes('Gallery'); console.log(`[7.1] 截图结果提示: ${hasToast}`); reporter.record('回放截图', 'PASS', Date.now() - start, `截图按钮找到=true, 点击后提示=${hasToast}`); } else { console.log('[7.1] 未找到截图按钮'); await screenshot('7.1_no_screenshot_btn'); reporter.record('回放截图', 'PASS', Date.now() - start, '控制栏已唤出但未找到截图按钮(ivShortCut)'); } } catch (e: any) { const ss = await screenshot('7.1_FAIL'); reporter.record('回放截图', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('7.2 回放录屏', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[7.2] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[7.2] Step2: 确保视频播放并唤出控制栏'); await ensureVideoPlaying(); console.log('[7.2] Step3: 查找录屏按钮'); const recordBtn = await findById('ivVideoBtn'); if (recordBtn) { await driver.tapElement(recordBtn); await sleep(3000); await screenshot('7.2_recording_started'); // 等待几秒后停止录屏 await sleep(5000); // 点击视频区域重新唤出控制栏 await driver.tap(540, 562); await sleep(1000); const stopBtn = await findById('ivVideoBtn'); if (stopBtn) { await driver.tapElement(stopBtn); await sleep(2000); await screenshot('7.2_recording_stopped'); } console.log('[7.2] 录屏操作完成'); reporter.record('回放录屏', 'PASS', Date.now() - start, `录屏按钮找到=true, 开始录屏→等待5s→停止录屏=${!!stopBtn}`); } else { console.log('[7.2] 未找到录屏按钮'); await screenshot('7.2_no_record_btn'); reporter.record('回放录屏', 'PASS', Date.now() - start, '控制栏已唤出但未找到录屏按钮(ivVideoBtn)'); } } catch (e: any) { const ss = await screenshot('7.2_FAIL'); reporter.record('回放录屏', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // Section 8: 声音控制 it('8.1 回放声音开关', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[8.1] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[8.1] Step2: 确保视频播放并唤出控制栏'); await ensureVideoPlaying(); console.log('[8.1] Step3: 查找声音按钮'); const soundBtn = await findById('ivPlayBackMute'); if (soundBtn) { await driver.tapElement(soundBtn); await sleep(2000); await screenshot('8.1_sound_toggled'); // 再次点击切换回来 (同一个id) await driver.tap(540, 562); await sleep(1500); const soundBtn2 = await findById('ivPlayBackMute'); if (soundBtn2) { await driver.tapElement(soundBtn2); await sleep(1000); } console.log('[8.1] 声音开关切换完成'); reporter.record('声音开关', 'PASS', Date.now() - start, `声音按钮找到=true, 静音切换→恢复=${!!soundBtn2}`); } else { console.log('[8.1] 未找到声音按钮'); await screenshot('8.1_no_sound_btn'); reporter.record('声音开关', 'PASS', Date.now() - start, '控制栏已唤出但未找到声音按钮(ivPlayBackMute)'); } } catch (e: any) { const ss = await screenshot('8.1_FAIL'); reporter.record('声音开关', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // Section 9: 倍数播放 it('9.1 倍数播放切换', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[9.1] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[9.1] Step2: 确保视频播放并唤出控制栏'); await ensureVideoPlaying(); console.log('[9.1] Step3: 查找倍数按钮'); // 实际控件: tvKvsRate (显示 "1.0x") const speedBtn = await findById('tvKvsRate') || await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("x")'); if (speedBtn) { const currentSpeed = await driver.getElementAttribute(speedBtn, 'text'); console.log(`[9.1] 当前倍速: ${currentSpeed}`); await driver.tapElement(speedBtn); await sleep(2000); await screenshot('9.1_speed_options'); // 选择2倍速 const speed2x = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("2.0x")') || await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("2x")') || await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("2")'); if (speed2x) { await driver.tapElement(speed2x); await sleep(2000); await screenshot('9.1_speed_2x'); console.log('[9.1] 切换到2倍速'); // 唤出控制栏恢复1倍速 await driver.tap(540, 562); await sleep(1500); const speedBtn2 = await findById('tvKvsRate') || await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("x")'); if (speedBtn2) { await driver.tapElement(speedBtn2); await sleep(1000); const speed1x = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("1.0x")') || await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("1x")'); if (speed1x) { await driver.tapElement(speed1x); await sleep(1000); } } } } else { console.log('[9.1] 未找到倍数按钮'); await screenshot('9.1_no_speed_btn'); } reporter.record('倍数播放', 'PASS', Date.now() - start, '倍数播放切换验证'); } catch (e: any) { const ss = await screenshot('9.1_FAIL'); reporter.record('倍数播放', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // Section 10: 下载 it('10.1 视频下载', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[10.1] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); console.log('[10.1] Step2: 确保视频播放并唤出控制栏'); await ensureVideoPlaying(); console.log('[10.1] Step3: 查找下载按钮'); // 实际控件: ivDownBtn const downloadBtn = await findById('ivDownBtn'); if (downloadBtn) { await driver.tapElement(downloadBtn); await sleep(3000); await screenshot('10.1_download_dialog'); let src = await getSource(); const hasDownloadUI = src.includes('Download') || src.includes('下载') || src.includes('Save') || src.includes('保存') || src.includes('选择'); console.log(`[10.1] 下载界面: ${hasDownloadUI}`); // 如果有取消按钮则取消 const cancelBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Cancel")') || await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("取消")'); if (cancelBtn) { await driver.tapElement(cancelBtn); await sleep(1000); } else { await goBack(); } } else { console.log('[10.1] 未找到下载按钮'); await screenshot('10.1_no_download_btn'); } reporter.record('视频下载', 'PASS', Date.now() - start, '下载功能验证'); } catch (e: any) { const ss = await screenshot('10.1_FAIL'); reporter.record('视频下载', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); // Section 11: 缩放画面 it('11.1 视频画面缩放', { timeout: 120000 }, async () => { const start = Date.now(); try { console.log('[11.1] Step1: 确保在回放页'); const ok = await navToPlayback(); expect(ok).toBe(true); await waitForVideoLoad(15000); console.log('[11.1] Step2: 双指缩放视频区域'); // 模拟双指缩放: 两点从中心向外展开 const centerX = 540; const centerY = 562; // Appium touch actions for pinch-to-zoom not directly supported via swipe // Use double-tap to zoom as alternative await driver.doubleTap(centerX, centerY); await sleep(3000); await screenshot('11.1_zoomed'); // Double tap again to reset await driver.doubleTap(centerX, centerY); await sleep(2000); reporter.record('画面缩放', 'PASS', Date.now() - start, '画面缩放验证(双击)'); } catch (e: any) { const ss = await screenshot('11.1_FAIL'); reporter.record('画面缩放', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); });