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 { sleep } from '../../utils/common'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); // 摄像头拉流(P0 15974 出流类):点击摄像头 → 点画面唤出 overlay → 出流成功(KB/S) → 停留 N ms 复检流未断。 // 配置 = 当前账号已添加的摄像头(首页实际卡片名,2026-06 dump)。不做添加,直接用。 // ones 为对应 15974 step;Plus 2K/3K 与 ONES 的 Plus 3MP/5MP 为近似映射(实际型号以首页名为准)。 interface CamCfg { name: string; ones: string; } // Android 账号摄像头(首页实际卡名) const CAMERAS_ANDROID: CamCfg[] = [ { name: 'Indoor Cam fn', ones: '15974#4oeeJEV8' }, // Indoor Cam { name: 'Pan/Tilt Cam vo', ones: '15974#16szaUdD' }, // Pan/Tilt Cam { name: 'Pan/Tilt Cam 2K oy', ones: '15974#Hj5Z9jSf' }, // Pan/Tilt Cam 2K { name: 'Pan/Tilt Cam Plus 2K 06', ones: '15974#SaKmFWLw' }, // PTC Plus 3MP(近似) { name: 'Pan/Tilt Cam Plus 3K 09', ones: '15974#LDqSDPPT' }, // PTC Plus 5MP(近似) { name: 'Outdoor Spotlight Cam 2K jx', ones: '15974#WH7kyypF' }, // OSC 2K { name: 'Video Doorbell 47', ones: '15974' }, // 门铃 Video Doorbell(出流 stepId 待补) ]; // iOS 账号摄像头(首页实际卡名,2026-06 dump):Pan/Tilt Cam 2K 6O / Pan/Tilt Cam Plus 3K B3 / Outdoor Spotlight Cam 2K FV。 // 用型号子串(CONTAINS 匹配,忽略设备后缀)。 const CAMERAS_IOS: CamCfg[] = [ { name: 'Pan/Tilt Cam 2K', ones: '15974#Hj5Z9jSf' }, { name: 'Pan/Tilt Cam Plus 3K', ones: '15974#LDqSDPPT' }, { name: 'Outdoor Spotlight Cam 2K', ones: '15974#WH7kyypF' }, { name: 'Outdoor Pan/Tilt Cam 3K', ones: '15974' }, // Outdoor PTC(首页实测卡名;拉流流程同其它摄像头,step待补) ]; const CAMERAS: CamCfg[] = (process.env.PLATFORM || 'android').toLowerCase() === 'ios' ? CAMERAS_IOS : CAMERAS_ANDROID; // 调试默认 15s;P0 正式跑设 STREAM_HOLD_MS=180000(3min)。CAMERA_NAME 设了则只跑该摄像头。 const HOLD_MS = parseInt(process.env.STREAM_HOLD_MS || '15000', 10); const STREAM_TIMEOUT = parseInt(process.env.STREAM_TIMEOUT || '30000', 10); const ONLY = process.env.CAMERA_NAME || ''; const TARGETS = ONLY ? CAMERAS.filter((c) => c.name.includes(ONLY)) : CAMERAS; describe('Camera Stream - 摄像头拉流(出流+停留)', () => { let driver: DeviceDriver; let reporter: TestReporter; let screenWidth = 1080; let screenHeight = 2280; beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter('Camera_Stream', 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(); }); // 在 RN 首页找摄像头卡片并点击进入功能页(scrollIntoView 对 RN 首页失效 → 手动滚动找) async function tapCameraCard(name: string): Promise { for (let i = 0; i < 8; i++) { let el: string | null = null; if (driver.platform === 'ios') { el = await driver.findElementRaw('predicate string', `name CONTAINS "${name}" AND type == "XCUIElementTypeCell"`).catch(() => null); } else { el = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${name}")`).catch(() => null); } if (el) { const rect = await driver.getElementRect(el); // 点卡片缩略图区域(名字在缩略图下方)进功能页 await driver.tap(rect.x + Math.min(rect.width / 2, 120), Math.max(rect.y - 40, rect.y + 20)); await sleep(6000); // 进功能页后可能弹框(订阅/云存储/提示等)→ 点 Cancel 关闭(否则挡住画面致拉流检测失败) for (const c of ['Cancel', '取消']) { const cb = driver.platform === 'ios' ? (await driver.findElementRaw('name', c).catch(() => null)) || (await driver.findElementRaw('predicate string', `name == "${c}" OR label == "${c}"`).catch(() => null)) : await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${c}")`).catch(() => null); if (cb) { await driver.tapElement(cb); await sleep(1200); break; } } return true; } await driver.scrollDown(500); await sleep(800); } return false; } // 点画面唤出 overlay(KB/S 指标点一下才显示,且会自动隐藏) async function tapVideoArea(): Promise { await driver.tap(Math.round(screenWidth / 2), Math.round(screenHeight * 0.25)); await sleep(800); } // 等待出流:点画面唤出 overlay,KB/S 指标出现 = 拉流成功 async function waitStreamLive(timeout: number): Promise { const start = Date.now(); while (Date.now() - start < timeout) { await tapVideoArea(); const s = await driver.getSource(); if (/Disconnected|Connection failed|连接失败|设备离线|Device offline/i.test(s)) { throw new Error('摄像头未连接/离线'); } if (/KB\/S|MB\/S|KB\/s|kb\/s/i.test(s)) return true; await sleep(1500); } return false; } for (const cam of TARGETS) { it(`[P0][ONES:${cam.ones}][wifi] 拉流 ${cam.name} 出流成功并停留${Math.round(HOLD_MS / 1000)}s`, { timeout: HOLD_MS + 120000 }, async () => { const start = Date.now(); const label = `拉流${cam.name}`; try { const entered = await tapCameraCard(cam.name); if (!entered) throw new Error(`首页找不到摄像头卡片 "${cam.name}"`); const live = await waitStreamLive(STREAM_TIMEOUT); if (!live) { const ss = await driver.screenshot().catch(() => ''); reporter.record(label, 'FAIL', Date.now() - start, '进入功能页但未检测到出流(KB/S)', ss); throw new Error('出流超时(未出现 KB/S)'); } console.log(`${cam.name} 出流成功,停留 ${HOLD_MS}ms 复检...`); const checkpoints = Math.max(1, Math.round(HOLD_MS / 30000)); const interval = Math.round(HOLD_MS / checkpoints); for (let i = 0; i < checkpoints; i++) { await sleep(interval); const s = await driver.getSource(); if (/Disconnected|Connection failed|连接失败/i.test(s)) { const ss = await driver.screenshot().catch(() => ''); reporter.record(label, 'FAIL', Date.now() - start, `停留中流断开(第${i + 1}/${checkpoints}次复检)`, ss); throw new Error('停留期间流断开'); } console.log(` ${cam.name} 停留复检 ${i + 1}/${checkpoints} OK (+${Math.round((Date.now() - start) / 1000)}s)`); } const elapsed = ((Date.now() - start) / 1000).toFixed(1); reporter.record(label, 'PASS', Date.now() - start, `出流成功并稳定停留${Math.round(HOLD_MS / 1000)}s, 耗时${elapsed}s`); expect(live).toBe(true); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(label, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); } });