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, onesCtrl, setupResilientHooks, findHomeCard } from '../../utils/common'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); // Lock 功能页控制(在首页卡片控制 lock_card 之外补充):点卡进设备功能页 → 点 lavAnimate 操作圈 解锁/上锁 → 读 tvStatus 校验翻转。 // 功能页元素(实测,对齐 Woan_UI_autotest):状态 Android id=tvStatus / iOS ScrollView StaticText[1];操作 Android id=lavAnimate / iOS Other[1]。 // 进功能页会先弹"About Home Sharing"引导(Got it)→ 需点掉。远程(非蓝牙直连)解锁有二次确认框。 const PKG = process.env.APP_PKG || 'com.theswitchbot.switchbot'; const LOCKS = [ { name: process.env.LOCK_DEVICE || 'Lock 6D', model: 'Lock 6F' }, { name: process.env.LOCK_PRO_DEVICE || 'Lock Pro DE', model: 'LockPro 7G' }, { name: process.env.LOCK_LITE_DEVICE || 'Lock Lite', model: 'Lock Lite' }, // 无控制 step → 锚点 [P0] 兜底 ]; describe('Lock Control - 功能页控制(进设备页 解锁/上锁)', () => { let driver: DeviceDriver; let reporter: TestReporter; setupResilientHooks(() => driver); const isIOS = () => driver.platform === 'ios'; beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter('Lock_Control', driver.platform.toUpperCase()); }); beforeEach(async () => { await driver.dismissPopupIfPresent(); await driver.goBackToHomepage(); await sleep(500); await driver.dismissPopupIfPresent(); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); // 读功能页锁状态(tvStatus / iOS StaticText[1]) async function readStatus(): Promise<'Locked' | 'Unlocked' | 'unknown'> { let txt = ''; const el = isIOS() ? await driver.findElementRaw('xpath', '//XCUIElementTypeScrollView/*/XCUIElementTypeStaticText[1]').catch(() => null) : await driver.findElementRaw('id', `${PKG}:id/tvStatus`).catch(() => null); if (el) txt = ((await driver.getElementAttribute(el, isIOS() ? 'label' : 'text').catch(() => '')) || '') as string; // 兜底:Lock Pro 等 RN 页状态无 tvStatus id → 直接从页面源取 Locked/Unlocked 文字(\b 防误中 "Dual Lock") if (!/Unlocked|Locked/i.test(txt)) { const src = await driver.getSource().catch(() => ''); const m = src.match(/\b(Unlocked|Locked)\b/); if (m) txt = m[1]; } return /Unlocked/i.test(txt) ? 'Unlocked' : /Locked/i.test(txt) ? 'Locked' : 'unknown'; } // 操作:点锁圈解锁/上锁。Lock 6D 原生页用 lavAnimate id;Lock Pro 等 RN 页无 id → 点锁圈坐标(顶部中心 ~W/2,H*0.30) async function operate(): Promise { const el = isIOS() ? await driver.findElementRaw('xpath', '//XCUIElementTypeScrollView/*/XCUIElementTypeOther[1]').catch(() => null) : await driver.findElementRaw('id', `${PKG}:id/lavAnimate`).catch(() => null); if (el) { await driver.tapElement(el); await sleep(1500); return true; } // 兜底:无 id → 点锁圈中心坐标 const wh = await driver.getWindowSize().catch(() => ({ width: 1080, height: 2400 })); await driver.tap(Math.round(wh.width * 0.5), Math.round(wh.height * 0.30)); await sleep(1500); return true; } // 远程解锁二次确认框:勾"Don't show again"(可选)+ 点 Unlock(蓝牙直连时无此框)。 // 修复:确认框可能**慢出** → 旧版首帧无框就 break、永不确认(Lock Pro DE 解锁失败根因)。 // 改为轮询几秒等框出现;未出现(蓝牙直连)则空转到超时后返回。Unlock 键与 checkbox 解耦。 async function confirmRemoteUnlock(): Promise { for (let i = 0; i < 6; i++) { const src = await driver.getSource().catch(() => ''); if (/Unlock|解锁|show again|不再/i.test(src)) { if (isIOS()) { for (const t of ["Don't show again", 'Do not show again', '不再显示']) { const cb = await driver.findElementRaw('predicate string', `label CONTAINS "${t}" OR name CONTAINS "${t}"`).catch(() => null); if (cb) { await driver.tapElement(cb).catch(() => {}); await sleep(400); break; } } const ub = await driver.findElementRaw('predicate string', 'name == "Unlock" OR label == "Unlock"').catch(() => null); if (ub) { await driver.tapElement(ub).catch(() => {}); await sleep(1500); return; } } else { const cb = await driver.findElementRaw('-android uiautomator', 'new UiSelector().checkable(true)').catch(() => null); if (cb) { await driver.tapElement(cb).catch(() => {}); await sleep(400); } const ub = await driver.findElementRaw('id', `${PKG}:id/btnUnlock`).catch(() => null) || await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Unlock")').catch(() => null); if (ub) { await driver.tapElement(ub).catch(() => {}); await sleep(1500); return; } } } await sleep(800); } } // 进锁功能页:点卡 → 点掉 Got it 引导 → 等 tvStatus 出现 async function enterLockFeature(name: string): Promise { const card = await findHomeCard(driver, name); if (!card) { console.log(`[LockControl] 首页找不到 ${name} 卡`); return false; } await driver.tapElement(card); await sleep(2500); for (let i = 0; i < 3; i++) { const g = isIOS() ? await driver.findElementRaw('predicate string', 'name == "Got it" OR label == "Got it"').catch(() => null) : await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")').catch(() => null); if (!g) break; await driver.tapElement(g); await sleep(1500); console.log('[LockControl] 点掉 Got it 引导'); } // Lock Pro 等:首次进功能页弹"Home Sharing"底部弹层(无 Got it,盖住锁圈/状态)→ 点弹层右上角 × 关闭。 // × 是无 id 的 ImageView(实测 1080×2400 在 ~(1005,1183) = W*0.93,H*0.49)→ 检测到 Home Sharing 文案就按相对坐标点关。 for (let i = 0; i < 3; i++) { const src = await driver.getSource().catch(() => ''); if (!/About Home Sharing|allow other users to access your/i.test(src)) break; const wh = await driver.getWindowSize().catch(() => ({ width: 1080, height: 2400 })); await driver.tap(Math.round(wh.width * 0.93), Math.round(wh.height * 0.49)); await sleep(1500); console.log('[LockControl] 关闭 Home Sharing 弹层(×)'); } for (let i = 0; i < 6; i++) { if ((await readStatus()) !== 'unknown') return true; await sleep(1000); } return (await readStatus()) !== 'unknown'; } for (const lock of LOCKS) { const ANCHOR = onesCtrl('lock', lock.model) || '[P0]'; it(`${ANCHOR} 功能页控制${lock.name}(解锁+上锁)`, { timeout: 150000 }, async () => { const start = Date.now(); try { const entered = await enterLockFeature(lock.name); if (!entered) { // 首页有该锁却进不去功能页 = 未正确执行 → FAIL(交给 catch);首页确无该锁(未添加/本平台无实体)→ 合法 SKIP。 await driver.goBackToHomepage().catch(() => {}); await sleep(1200); if (await findHomeCard(driver, lock.name).catch(() => null)) throw new Error('进功能页失败(首页有该锁但进不去功能页,未正确执行)'); reporter.record(`${ANCHOR} 功能页控制${lock.name}`, 'SKIP', Date.now() - start, '首页无该锁(未添加/本平台无实体),跳过'); return; } const s0 = await readStatus(); console.log(`[LockControl] ${lock.name} 初始状态: ${s0}`); // 操作1:切到相反态 const target1 = s0 === 'Locked' ? 'Unlocked' : 'Locked'; expect(await operate()).toBe(true); if (target1 === 'Unlocked') await confirmRemoteUnlock(); let s1: 'Locked' | 'Unlocked' | 'unknown' = 'unknown'; for (let i = 0; i < 8; i++) { s1 = await readStatus(); if (s1 === target1) break; await sleep(1500); } console.log(`[LockControl] 操作1后: ${s1}(目标${target1})`); expect(s1).toBe(target1); // 操作2:切回 const target2 = target1 === 'Locked' ? 'Unlocked' : 'Locked'; await operate(); if (target2 === 'Unlocked') await confirmRemoteUnlock(); let s2: 'Locked' | 'Unlocked' | 'unknown' = 'unknown'; for (let i = 0; i < 8; i++) { s2 = await readStatus(); if (s2 === target2) break; await sleep(1500); } console.log(`[LockControl] 操作2后: ${s2}(目标${target2})`); expect(s2).toBe(target2); reporter.record(`${ANCHOR} 功能页控制${lock.name}`, 'PASS', Date.now() - start, `功能页解锁/上锁均生效(${s0}→${s1}→${s2})`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${ANCHOR} 功能页控制${lock.name}`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); } });