810 lines
29 KiB
TypeScript
810 lines
29 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 { execSync } from 'child_process';
|
||
import { robustBeforeAll, robustBeforeEach } from './aihub-setup.helper';
|
||
import * as dotenv from 'dotenv';
|
||
import * as path from 'path';
|
||
|
||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||
|
||
const AIHUB_NAME = getDeviceName('aihub', 'AIHUB_NAME');
|
||
|
||
describe('【AI Hub 投屏设置】- Screen Casting Settings 功能覆盖', () => {
|
||
let driver: DeviceDriver;
|
||
let reporter: TestReporter;
|
||
|
||
const isAndroid = () => driver.platform === 'android';
|
||
const SETTINGS_ICON = () => isAndroid() ? { x: 999, y: 175 } : { x: 361, y: 70 };
|
||
|
||
beforeAll(async () => {
|
||
driver = createDriver();
|
||
await driver.createSession();
|
||
await robustBeforeAll(driver);
|
||
reporter = new TestReporter('AIHub_ScreenCasting', driver.platform.toUpperCase());
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
await robustBeforeEach(driver);
|
||
});
|
||
|
||
afterAll(async () => {
|
||
reporter.generate();
|
||
await driver.destroySession();
|
||
});
|
||
|
||
// --- 辅助函数 ---
|
||
async function captureScreenshot(): Promise<string | undefined> {
|
||
try { return await driver.screenshot(); } catch { return undefined; }
|
||
}
|
||
|
||
async function waitForLoading(maxWait = 30000): Promise<void> {
|
||
const start = Date.now();
|
||
while (Date.now() - start < maxWait) {
|
||
const s = await driver.getSource();
|
||
if (!s.includes('Loading') && !s.includes('In progress')) return;
|
||
await sleep(3000);
|
||
}
|
||
}
|
||
|
||
async function logPageElements(): Promise<string> {
|
||
const source = await driver.getSource();
|
||
if (isAndroid()) {
|
||
const textRe = /text="([^"]{1,80})"/g;
|
||
const descRe = /content-desc="([^"]{1,80})"/g;
|
||
const texts: string[] = [];
|
||
const descs: string[] = [];
|
||
let m;
|
||
while ((m = textRe.exec(source)) !== null) {
|
||
if (m[1] && !texts.includes(m[1])) texts.push(m[1]);
|
||
}
|
||
while ((m = descRe.exec(source)) !== null) {
|
||
if (m[1] && !descs.includes(m[1])) descs.push(m[1]);
|
||
}
|
||
console.log('Page texts:', texts.join(' | '));
|
||
if (descs.length) console.log('Page descs:', descs.join(' | '));
|
||
} else {
|
||
const nameRe = /name="([^"]{1,80})"/g;
|
||
const names: string[] = [];
|
||
let m;
|
||
while ((m = nameRe.exec(source)) !== null) {
|
||
if (!names.includes(m[1])) names.push(m[1]);
|
||
}
|
||
console.log('Page elements:', names.join(' | '));
|
||
}
|
||
return source;
|
||
}
|
||
|
||
async function ensureAppRunning(): Promise<void> {
|
||
if (!isAndroid()) return;
|
||
try {
|
||
const src = await driver.getSource();
|
||
if (src.includes('com.theswitchbot.switchbot') || src.includes('SwitchBot')
|
||
|| src.includes('Home') || src.includes('Cameras') || src.includes('AI Events')) return;
|
||
} catch { /* app likely crashed */ }
|
||
try {
|
||
execSync('adb shell am force-stop com.theswitchbot.switchbot');
|
||
await sleep(2000);
|
||
execSync('adb shell am start -n com.theswitchbot.switchbot/.index.ui.SplashActivity');
|
||
await sleep(10000);
|
||
await driver.dismissPopupIfPresent();
|
||
} catch { /* ignore */ }
|
||
}
|
||
|
||
async function enterHubFunctionPage(): Promise<boolean> {
|
||
const src = await driver.getSource();
|
||
if (src.includes('Cameras') && src.includes('AI Events')) return true;
|
||
|
||
await ensureAppRunning();
|
||
await driver.goBackToHomepage();
|
||
await sleep(2000);
|
||
await driver.dismissPopupIfPresent();
|
||
|
||
if (isAndroid()) {
|
||
const card = await (driver as any).findDeviceCard(AIHUB_NAME);
|
||
if (!card) return false;
|
||
await driver.tapElement(card);
|
||
await sleep(5000);
|
||
await waitForLoading();
|
||
await driver.dismissPopupIfPresent();
|
||
const s = await driver.getSource();
|
||
return s.includes('Cameras') || s.includes('AI Events');
|
||
}
|
||
|
||
for (let scroll = 0; scroll <= 5; scroll++) {
|
||
let hubEl = await driver.findElementRaw('predicate string',
|
||
`name CONTAINS "${AIHUB_NAME}" AND type == "XCUIElementTypeCell"`);
|
||
if (!hubEl) {
|
||
hubEl = await driver.findElementRaw('predicate string', `label CONTAINS "${AIHUB_NAME}"`);
|
||
}
|
||
if (hubEl) {
|
||
await driver.tapElement(hubEl);
|
||
await sleep(5000);
|
||
await waitForLoading();
|
||
await driver.dismissPopupIfPresent();
|
||
const s = await driver.getSource();
|
||
if (s.includes('Cameras') || s.includes('AI Events')) return true;
|
||
}
|
||
if (scroll < 5) {
|
||
await driver.swipe(195, 650, 195, 300, 0.5);
|
||
await sleep(1500);
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function enterHubSettings(): Promise<boolean> {
|
||
const src = await driver.getSource();
|
||
if (src.includes('Motion Detection') || src.includes('Firmware')
|
||
|| src.includes('Do Not Disturb') || src.includes('Screen Casting')) {
|
||
return true;
|
||
}
|
||
|
||
const inHub = await enterHubFunctionPage();
|
||
if (!inHub) return false;
|
||
|
||
await driver.tap(SETTINGS_ICON().x, SETTINGS_ICON().y);
|
||
await sleep(5000);
|
||
await waitForLoading();
|
||
|
||
const settingSrc = await driver.getSource();
|
||
return settingSrc.includes('Motion Detection') || settingSrc.includes('Firmware')
|
||
|| settingSrc.includes('Do Not Disturb') || settingSrc.includes('Wi-Fi');
|
||
}
|
||
|
||
async function enterScreenCastingPage(): Promise<boolean> {
|
||
const steps: string[] = [];
|
||
|
||
// Check if already on Screen Casting settings page
|
||
const curSrc = await driver.getSource();
|
||
if (isScreenCastingPage(curSrc)) {
|
||
steps.push('已在投屏设置页');
|
||
console.log('ScreenCasting nav:', steps.join(' → '));
|
||
return true;
|
||
}
|
||
|
||
// If on edit/selection sub-page, go back
|
||
if (curSrc.includes('Save') && (curSrc.includes('Select camera') || curSrc.includes('Select Camera'))) {
|
||
await exitEditPage();
|
||
const afterSrc = await driver.getSource();
|
||
if (isScreenCastingPage(afterSrc)) {
|
||
steps.push('从选择页返回投屏设置');
|
||
console.log('ScreenCasting nav:', steps.join(' → '));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Navigate from Hub settings
|
||
const inSettings = await enterHubSettings();
|
||
if (!inSettings) {
|
||
steps.push('无法进入Hub设置页');
|
||
console.log('ScreenCasting nav:', steps.join(' → '));
|
||
return false;
|
||
}
|
||
steps.push('已在Hub设置页');
|
||
|
||
// Find and tap "Extended Display Settings" (投屏设置的实际入口名)
|
||
let scEl: string | null = null;
|
||
if (isAndroid()) {
|
||
scEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Extended Display Settings")');
|
||
if (!scEl) scEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Extended Display")');
|
||
if (!scEl) scEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("投屏")');
|
||
} else {
|
||
scEl = await driver.findElementRaw('predicate string', 'label CONTAINS "Extended Display"');
|
||
if (!scEl) scEl = await driver.findElementRaw('predicate string', 'label CONTAINS "投屏"');
|
||
}
|
||
|
||
if (!scEl) {
|
||
for (let i = 0; i < 3; i++) {
|
||
await driver.scrollDown(300);
|
||
await sleep(2000);
|
||
if (isAndroid()) {
|
||
scEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Extended Display Settings")');
|
||
if (!scEl) scEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Extended Display")');
|
||
} else {
|
||
scEl = await driver.findElementRaw('predicate string', 'label CONTAINS "Extended Display"');
|
||
}
|
||
if (scEl) break;
|
||
}
|
||
}
|
||
|
||
if (!scEl) {
|
||
steps.push('未找到Extended Display Settings入口');
|
||
console.log('ScreenCasting nav:', steps.join(' → '));
|
||
await logPageElements();
|
||
return false;
|
||
}
|
||
|
||
await driver.tapElement(scEl);
|
||
await sleep(3000);
|
||
await waitForLoading();
|
||
steps.push('点击Screen Casting');
|
||
|
||
await driver.dismissPopupIfPresent();
|
||
|
||
const finalSrc = await driver.getSource();
|
||
console.log('ScreenCasting nav:', steps.join(' → '));
|
||
return isScreenCastingPage(finalSrc);
|
||
}
|
||
|
||
function isScreenCastingPage(source: string): boolean {
|
||
// 投屏设置页: 包含 "Extended Display Settings" + 三种布局模式
|
||
return source.includes('Extended Display Settings')
|
||
&& (source.includes('Standard Layout') || source.includes('Report Layout') || source.includes('Live'));
|
||
}
|
||
|
||
async function exitEditPage(): Promise<void> {
|
||
for (let attempt = 0; attempt < 3; attempt++) {
|
||
const curSrc = await driver.getSource();
|
||
if (isScreenCastingPage(curSrc)) return;
|
||
await driver.goBack();
|
||
await sleep(1500);
|
||
if (isAndroid()) {
|
||
const confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
if (confirmEl) { await driver.tapElement(confirmEl); await sleep(2000); return; }
|
||
const okEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("OK")');
|
||
if (okEl) { await driver.tapElement(okEl); await sleep(2000); return; }
|
||
}
|
||
}
|
||
}
|
||
|
||
async function getCurrentMode(source?: string): Promise<string> {
|
||
const src = source || await driver.getSource();
|
||
// Modes: Standard Layout (普通), Report Layout (混合/需AI+), Live (实时)
|
||
// 检测当前选中的模式 - 需根据实际UI判断(可能是高亮/选中状态)
|
||
// 暂时通过页面包含的模式名来推断
|
||
if (src.includes('Standard Layout')) return 'standard';
|
||
if (src.includes('Report Layout')) return 'report';
|
||
if (src.includes('Live')) return 'live';
|
||
return 'unknown';
|
||
}
|
||
|
||
async function selectMode(modeName: string): Promise<boolean> {
|
||
let modeEl: string | null = null;
|
||
if (isAndroid()) {
|
||
// 先尝试通过content-desc定位(因为content-desc包含完整描述)
|
||
modeEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${modeName}")`);
|
||
if (!modeEl) modeEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${modeName}")`);
|
||
} else {
|
||
modeEl = await driver.findElementRaw('predicate string', `label CONTAINS "${modeName}"`);
|
||
}
|
||
if (!modeEl) return false;
|
||
await driver.tapElement(modeEl);
|
||
await sleep(2000);
|
||
return true;
|
||
}
|
||
|
||
async function tapSave(): Promise<boolean> {
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
if (!saveEl) saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Save")');
|
||
} else {
|
||
saveEl = await driver.findElementRaw('predicate string', 'label == "Save"');
|
||
}
|
||
if (!saveEl) return false;
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
await waitForLoading();
|
||
return true;
|
||
}
|
||
|
||
async function countSelectedCameras(source?: string): Promise<number> {
|
||
const src = source || await driver.getSource();
|
||
// Count checked/selected cameras - look for checkmarks or selected state
|
||
if (isAndroid()) {
|
||
const checkedRe = /checked="true"/g;
|
||
let count = 0;
|
||
let m;
|
||
while ((m = checkedRe.exec(src)) !== null) count++;
|
||
return count;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
// --- 测试用例 ---
|
||
|
||
// ========== 1. 投屏设置页面显示 ==========
|
||
|
||
it('1.1 投屏设置页面显示(已绑定摄像头)', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
const source = await logPageElements();
|
||
|
||
// 验证页面包含三种布局模式
|
||
const hasStandard = source.includes('Standard Layout');
|
||
const hasReport = source.includes('Report Layout');
|
||
const hasLive = source.includes('Live');
|
||
expect(hasStandard || hasReport || hasLive).toBe(true);
|
||
steps.push(`Standard=${hasStandard}, Report=${hasReport}, Live=${hasLive}`);
|
||
|
||
reporter.record('投屏设置页面显示(已绑定摄像头)', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('投屏设置页面显示(已绑定摄像头)', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('1.2 投屏设置页面-三种模式选项及描述', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
const source = await driver.getSource();
|
||
|
||
// Standard Layout描述
|
||
const hasStandardDesc = source.includes('Arranges snapshots based on connected camera count');
|
||
// Report Layout描述
|
||
const hasReportDesc = source.includes('smart reports on the left side');
|
||
// Live描述
|
||
const hasLiveDesc = source.includes('live feeds from selected camera');
|
||
|
||
expect(hasStandardDesc).toBe(true);
|
||
steps.push(`Standard描述=${hasStandardDesc}, Report描述=${hasReportDesc}, Live描述=${hasLiveDesc}`);
|
||
|
||
reporter.record('投屏设置页面-模式描述', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('投屏设置页面-模式描述', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('1.3 投屏设置页面-Report Layout需要AI+服务', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
const source = await driver.getSource();
|
||
|
||
// Report Layout显示需要AI+服务
|
||
const requiresAI = source.includes('AI+ service required') || source.includes('AI+');
|
||
expect(requiresAI).toBe(true);
|
||
steps.push(`AI+服务提示: ${requiresAI}`);
|
||
|
||
reporter.record('投屏设置-Report需AI+', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('投屏设置-Report需AI+', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ========== 2. 切换为Report Layout(混合模式) ==========
|
||
|
||
it('2.1 切换为Report Layout', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
// 点击Report Layout
|
||
const selected = await selectMode('Report Layout');
|
||
if (!selected) {
|
||
steps.push('未找到Report Layout选项');
|
||
await logPageElements();
|
||
reporter.record('切换为Report Layout', 'SKIP', Date.now() - start, steps.join(' → ') + ' [条件不满足skip]');
|
||
return;
|
||
}
|
||
steps.push('点击Report Layout');
|
||
await sleep(2000);
|
||
|
||
// 检查是否弹出AI+服务相关提示
|
||
const afterSrc = await driver.getSource();
|
||
if (afterSrc.includes('subscribe') || afterSrc.includes('Subscribe')
|
||
|| afterSrc.includes('service') || afterSrc.includes('enable')) {
|
||
steps.push('弹出AI+服务提示(需开通)');
|
||
await driver.goBack();
|
||
await sleep(2000);
|
||
} else {
|
||
steps.push('切换成功(AI+已开通)');
|
||
}
|
||
|
||
await logPageElements();
|
||
|
||
reporter.record('切换为Report Layout', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('切换为Report Layout', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('2.2 切换为Report Layout(AI+服务开关为关)', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
// 点击Report Layout
|
||
const selected = await selectMode('Report Layout');
|
||
if (!selected) {
|
||
steps.push('未找到Report Layout选项');
|
||
reporter.record('Report Layout(开关关)', 'SKIP', Date.now() - start, steps.join(' → ') + ' [skip]');
|
||
return;
|
||
}
|
||
steps.push('点击Report Layout');
|
||
await sleep(2000);
|
||
|
||
const afterSrc = await driver.getSource();
|
||
await logPageElements();
|
||
|
||
// 记录点击后的状态变化
|
||
if (afterSrc.includes('AI+') || afterSrc.includes('service')
|
||
|| afterSrc.includes('Subscribe') || afterSrc.includes('enable')) {
|
||
steps.push('显示AI+服务相关提示');
|
||
// 关闭弹窗/返回
|
||
const gotIt = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")');
|
||
if (gotIt) { await driver.tapElement(gotIt); await sleep(1000); }
|
||
else { await driver.goBack(); await sleep(2000); }
|
||
} else {
|
||
steps.push('无服务提示(可能已开通)');
|
||
}
|
||
|
||
reporter.record('Report Layout(开关关)', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('Report Layout(开关关)', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ========== 3. 切换为Standard Layout(普通模式) ==========
|
||
|
||
it('3.1 切换为Standard Layout', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
// 点击Standard Layout
|
||
const selected = await selectMode('Standard Layout');
|
||
if (!selected) {
|
||
steps.push('未找到Standard Layout选项');
|
||
await logPageElements();
|
||
expect(false).toBe(true);
|
||
return;
|
||
}
|
||
steps.push('点击Standard Layout');
|
||
await sleep(2000);
|
||
|
||
const afterSrc = await driver.getSource();
|
||
await logPageElements();
|
||
|
||
// 验证切换成功
|
||
steps.push('Standard Layout已选中');
|
||
|
||
reporter.record('切换为Standard Layout', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('切换为Standard Layout', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('3.2 从Report Layout切换回Standard Layout', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
// 先切到Report Layout
|
||
const toReport = await selectMode('Report Layout');
|
||
if (toReport) {
|
||
steps.push('先切到Report Layout');
|
||
await sleep(2000);
|
||
const src = await driver.getSource();
|
||
if (src.includes('subscribe') || src.includes('Subscribe') || src.includes('service')) {
|
||
steps.push('AI+未开通,无法切到Report');
|
||
await driver.goBack();
|
||
await sleep(2000);
|
||
reporter.record('Report→Standard', 'SKIP', Date.now() - start, steps.join(' → ') + ' [AI+不满足skip]');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 切回Standard Layout
|
||
const toStandard = await selectMode('Standard Layout');
|
||
expect(toStandard).toBe(true);
|
||
steps.push('切回Standard Layout');
|
||
await sleep(2000);
|
||
|
||
const afterSrc = await driver.getSource();
|
||
steps.push(`页面包含Standard: ${afterSrc.includes('Standard Layout')}`);
|
||
|
||
reporter.record('Report→Standard', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('Report→Standard', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ========== 4. 切换为Live模式(实时模式) ==========
|
||
|
||
it('4.1 切换为Live模式', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
// 点击Live
|
||
const selected = await selectMode('Live');
|
||
if (!selected) {
|
||
steps.push('未找到Live选项');
|
||
await logPageElements();
|
||
reporter.record('切换为Live模式', 'SKIP', Date.now() - start, steps.join(' → ') + ' [无Live选项skip]');
|
||
return;
|
||
}
|
||
steps.push('点击Live');
|
||
await sleep(3000);
|
||
|
||
// Live模式可能弹出摄像头选择或直接切换
|
||
const afterSrc = await driver.getSource();
|
||
await logPageElements();
|
||
|
||
if (afterSrc.includes('Select') || afterSrc.includes('选择')) {
|
||
steps.push('进入摄像头选择页');
|
||
}
|
||
|
||
// 返回到投屏设置页
|
||
await exitEditPage();
|
||
|
||
reporter.record('切换为Live模式', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('切换为Live模式', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('4.2 从Standard切换为Live模式', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
// 先确保在Standard Layout
|
||
await selectMode('Standard Layout');
|
||
await sleep(2000);
|
||
steps.push('确认Standard模式');
|
||
|
||
// 切换到Live
|
||
const selected = await selectMode('Live');
|
||
if (!selected) {
|
||
steps.push('未找到Live选项');
|
||
reporter.record('Standard→Live', 'SKIP', Date.now() - start, steps.join(' → ') + ' [skip]');
|
||
return;
|
||
}
|
||
steps.push('点击Live');
|
||
await sleep(3000);
|
||
|
||
const afterSrc = await driver.getSource();
|
||
await logPageElements();
|
||
steps.push('Live模式页面已加载');
|
||
|
||
// 返回
|
||
await exitEditPage();
|
||
|
||
reporter.record('Standard→Live', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('Standard→Live', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ========== 5. 实时模式摄像头选择限制 ==========
|
||
|
||
it('5.1 Live模式至少选择一个摄像头', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
// 切换到Live模式
|
||
const selected = await selectMode('Live');
|
||
if (!selected) {
|
||
steps.push('未找到Live选项');
|
||
reporter.record('Live至少选1摄像头', 'SKIP', Date.now() - start, steps.join(' → ') + ' [skip]');
|
||
return;
|
||
}
|
||
steps.push('切换到Live模式');
|
||
await sleep(3000);
|
||
|
||
const source = await driver.getSource();
|
||
await logPageElements();
|
||
|
||
if (isAndroid()) {
|
||
// 查找所有选中的checkbox
|
||
const checkboxes = await driver.findElementsRaw('-android uiautomator',
|
||
'new UiSelector().checked(true)');
|
||
if (checkboxes && checkboxes.length > 0) {
|
||
steps.push(`发现${checkboxes.length}个选中项`);
|
||
// 尝试取消全部
|
||
for (const cb of checkboxes) {
|
||
await driver.tapElement(cb);
|
||
await sleep(1000);
|
||
}
|
||
steps.push('取消全部选中');
|
||
await sleep(1000);
|
||
|
||
// 检查是否出现至少选1个的提示
|
||
const afterSrc = await driver.getSource();
|
||
const hasWarning = afterSrc.includes('at least') || afterSrc.includes('至少')
|
||
|| afterSrc.includes('minimum') || afterSrc.includes('select');
|
||
steps.push(`至少1个提示: ${hasWarning}`);
|
||
await logPageElements();
|
||
|
||
// 恢复:选中第一个
|
||
const firstCb = await driver.findElementRaw('-android uiautomator',
|
||
'new UiSelector().checkable(true).instance(0)');
|
||
if (firstCb) {
|
||
await driver.tapElement(firstCb);
|
||
await sleep(1000);
|
||
steps.push('恢复选中第1个');
|
||
}
|
||
} else {
|
||
steps.push('未发现checkbox元素,页面可能不同');
|
||
}
|
||
}
|
||
|
||
await exitEditPage();
|
||
|
||
reporter.record('Live至少选1摄像头', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('Live至少选1摄像头', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('5.2 Live模式至多选择四个摄像头', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
// 切换到Live模式
|
||
const selected = await selectMode('Live');
|
||
if (!selected) {
|
||
steps.push('未找到Live选项');
|
||
reporter.record('Live至多选4摄像头', 'SKIP', Date.now() - start, steps.join(' → ') + ' [skip]');
|
||
return;
|
||
}
|
||
steps.push('切换到Live模式');
|
||
await sleep(3000);
|
||
|
||
if (isAndroid()) {
|
||
const allCheckable = await driver.findElementsRaw('-android uiautomator',
|
||
'new UiSelector().checkable(true)');
|
||
steps.push(`可选摄像头数: ${allCheckable ? allCheckable.length : 0}`);
|
||
|
||
if (allCheckable && allCheckable.length > 4) {
|
||
// 先选满4个
|
||
let selectedCount = 0;
|
||
for (const cb of allCheckable) {
|
||
const attr = await driver.getElementAttribute(cb, 'checked');
|
||
if (attr === 'true') selectedCount++;
|
||
}
|
||
steps.push(`当前已选: ${selectedCount}`);
|
||
|
||
if (selectedCount < 4) {
|
||
for (const cb of allCheckable) {
|
||
if (selectedCount >= 4) break;
|
||
const attr = await driver.getElementAttribute(cb, 'checked');
|
||
if (attr !== 'true') {
|
||
await driver.tapElement(cb);
|
||
await sleep(500);
|
||
selectedCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 尝试选第5个
|
||
let fifthCb: string | null = null;
|
||
for (const cb of allCheckable) {
|
||
const attr = await driver.getElementAttribute(cb, 'checked');
|
||
if (attr !== 'true') {
|
||
fifthCb = cb;
|
||
break;
|
||
}
|
||
}
|
||
if (fifthCb) {
|
||
await driver.tapElement(fifthCb);
|
||
await sleep(1000);
|
||
const afterSrc = await driver.getSource();
|
||
const hasMaxWarning = afterSrc.includes('maximum') || afterSrc.includes('至多')
|
||
|| afterSrc.includes('most') || afterSrc.includes('up to 4')
|
||
|| afterSrc.includes('4');
|
||
steps.push(`超4个限制提示: ${hasMaxWarning}`);
|
||
await logPageElements();
|
||
}
|
||
} else {
|
||
steps.push('摄像头不超过4个,无法测试上限');
|
||
}
|
||
}
|
||
|
||
await exitEditPage();
|
||
|
||
reporter.record('Live至多选4摄像头', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('Live至多选4摄像头', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('5.3 Live模式取消保存', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterScreenCastingPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入投屏设置页');
|
||
|
||
// 记录当前状态
|
||
const beforeSrc = await driver.getSource();
|
||
steps.push('记录当前状态');
|
||
|
||
// 切换到Live模式
|
||
const selected = await selectMode('Live');
|
||
if (!selected) {
|
||
steps.push('未找到Live选项');
|
||
reporter.record('Live取消保存', 'SKIP', Date.now() - start, steps.join(' → ') + ' [skip]');
|
||
return;
|
||
}
|
||
steps.push('点击Live');
|
||
await sleep(2000);
|
||
|
||
// 不保存,直接退出(goBack → Confirm退出)
|
||
await exitEditPage();
|
||
steps.push('退出不保存');
|
||
|
||
// 验证回到投屏设置页且模式未变
|
||
await sleep(2000);
|
||
const afterSrc = await driver.getSource();
|
||
if (afterSrc.includes('Extended Display Settings')) {
|
||
steps.push('已回到投屏设置页');
|
||
}
|
||
|
||
reporter.record('Live取消保存', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('Live取消保存', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
});
|