157 lines
7.5 KiB
TypeScript
157 lines
7.5 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 { 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<boolean> {
|
|
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<void> {
|
|
await driver.tap(Math.round(screenWidth / 2), Math.round(screenHeight * 0.25));
|
|
await sleep(800);
|
|
}
|
|
|
|
// 等待出流:点画面唤出 overlay,KB/S 指标出现 = 拉流成功
|
|
async function waitStreamLive(timeout: number): Promise<boolean> {
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
});
|