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 } from '../../utils/common'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); // 覆盖 Lock + Lock Pro 两台:首页卡片解锁/上锁,各自按型号取 ONES 锚点。 const LOCKS = [ { name: process.env.LOCK_DEVICE || 'Lock 6D', model: 'Lock 6F' }, // 普通 Lock → 15975#5YEfmpJz { name: process.env.LOCK_PRO_DEVICE || 'Lock Pro DE', model: 'LockPro 7G' }, // Lock Pro → 15975#LnPNEJmS ]; describe('Lock Card - 首页卡片控制(Lock + Lock Pro)', () => { let driver: DeviceDriver; let reporter: TestReporter; setupResilientHooks(() => driver); beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter('Lock_Card', driver.platform.toUpperCase()); }); beforeEach(async () => { await driver.dismissPopupIfPresent(); await driver.goBackToHomepage(); await sleep(500); await driver.dismissPopupIfPresent(); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); function parseStateAtColumn(src: string, colX: number): 'Locked' | 'Unlocked' | 'unknown' { for (const n of src.split('<')) { const t = (n.match(/text="(Locked|Unlocked)"/) || [])[1] as 'Locked' | 'Unlocked' | undefined; const bx = (n.match(/bounds="\[(\d+),/) || [])[1]; if (t && bx && Math.abs(parseInt(bx, 10) - colX) < 120) return t; } return 'unknown'; } async function homeLockInfo(name: string): Promise<{ state: 'Locked' | 'Unlocked' | 'unknown'; toggleX: number; toggleY: number } | null> { const el = await driver.findElementRaw('-android uiautomator', `new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textContains("${name}"))`); if (!el) return null; const r = await driver.getElementRect(el); const state = parseStateAtColumn(await driver.getSource(), r.x); return { state, toggleX: r.x + r.width - 40, toggleY: r.y - 60 }; } async function homeReadState(name: string): Promise<'Locked' | 'Unlocked' | 'unknown'> { return (await homeLockInfo(name))?.state ?? 'unknown'; } async function controlLockTo(name: string, target: 'Locked' | 'Unlocked'): Promise<'Locked' | 'Unlocked' | 'unknown'> { const info = await homeLockInfo(name); if (!info) throw new Error(`首页找不到${name}卡片`); if (info.state === 'unknown') throw new Error(`读不到${name}卡片状态`); if (info.state === target) return target; await driver.tap(info.toggleX, info.toggleY); // 点开关钮 await sleep(2500); // 仅"远程解锁"(未蓝牙直连)会弹二次确认框:勾"Do not show again"后点 Unlock。 if (target === 'Unlocked') { const cb = await driver.findElementRaw('id', 'com.theswitchbot.switchbot:id/cbNotShowAgain'); if (cb) { await driver.tapElement(cb); await sleep(500); const btn = await driver.findElementRaw('id', 'com.theswitchbot.switchbot:id/btnUnlock') || await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Unlock")'); if (btn) { await driver.tapElement(btn); console.log('已确认远程解锁'); } } } await sleep(3000); let st: 'Locked' | 'Unlocked' | 'unknown' = info.state; for (let i = 0; i < 6 && st !== target; i++) { st = await homeReadState(name); if (st !== target) await sleep(2500); } return st; } // 对每台锁(Lock / Lock Pro)各跑 解锁 + 上锁,硬断言首页卡片状态翻转 for (const lock of LOCKS) { const ANCHOR = onesCtrl('lock', lock.model); it(`${ANCHOR} ${lock.name} 解锁`, { timeout: 120000 }, async () => { const start = Date.now(); try { const before = await homeReadState(lock.name); expect(before).not.toBe('unknown'); const after = await controlLockTo(lock.name, 'Unlocked'); console.log(`${lock.name} 解锁: ${before} → ${after}`); expect(after).toBe('Unlocked'); reporter.record(`${ANCHOR} ${lock.name} 解锁`, 'PASS', Date.now() - start, `首页卡片解锁,状态=${after}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${ANCHOR} ${lock.name} 解锁`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it(`${ANCHOR} ${lock.name} 上锁`, { timeout: 120000 }, async () => { const start = Date.now(); try { const before = await homeReadState(lock.name); expect(before).not.toBe('unknown'); const after = await controlLockTo(lock.name, 'Locked'); console.log(`${lock.name} 上锁: ${before} → ${after}`); expect(after).toBe('Locked'); reporter.record(`${ANCHOR} ${lock.name} 上锁`, 'PASS', Date.now() - start, `首页卡片上锁,状态=${after}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${ANCHOR} ${lock.name} 上锁`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); } });