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 { try { return await driver.screenshot(); } catch { return undefined; } } async function waitForLoading(maxWait = 30000): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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; } }); });