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 + Lock Lite 三台:首页卡片解锁/上锁,各自按型号取 ONES 锚点。 // Lock Lite 控制 UI 与 Lock 相似,ONES 无独立控制 step(仅 add:304132)→ 锚点用 [P0] 兜底。 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 { name: process.env.LOCK_LITE_DEVICE || 'Lock Lite', model: 'Lock Lite' }, // Lock Lite → 无控制 step,锚点 [P0] ]; 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> { if (driver.platform === 'ios') { // iOS:Android 的 UiScrollable.scrollIntoView 在 iOS 不滚动 → 锁卡片在下方就找不到。改用滚动查找(参考其它 iOS 找卡片)。 // 锁卡片是 Cell(name 形如 "Lock 6DLocked"),右上角有内联开关钮(≈ cell.x+cell.w-32, cell.y+24),状态读 cell name。 const { height: winH } = await driver.getWindowSize().catch(() => ({ height: 844 })); let lastSig = ''; for (let r = 0; r < 14; r++) { const cell = await driver.findElementRaw('predicate string', `type == "XCUIElementTypeCell" AND name CONTAINS "${name}"`).catch(() => null); if (cell) { const rr = await driver.getElementRect(cell).catch(() => null); if (rr && rr.width > 0 && rr.y > winH * 0.06 && rr.y + rr.height < winH * 0.92) { const cname = await driver.getElementAttribute(cell, 'name').catch(() => ''); const state: 'Locked' | 'Unlocked' | 'unknown' = /Unlocked/.test(cname) ? 'Unlocked' : /Locked/.test(cname) ? 'Locked' : 'unknown'; return { state, toggleX: Math.round(rr.x + rr.width - 32), toggleY: Math.round(rr.y + 24) }; } } const sig = (await driver.getSource()).slice(-2500); if (sig === lastSig) break; lastSig = sig; await driver.scrollDown(500); await sleep(800); } return 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') { if (driver.platform === 'ios') { // iOS:确认框按 name 找"Don't show again/Do not show"勾选 + "Unlock"按钮(参考 Android 流程) for (const t of ["Don't show again", 'Do not show again', "Don't show this again"]) { const cb = await driver.findElementRaw('predicate string', `name CONTAINS "${t}" OR label CONTAINS "${t}"`).catch(() => null); if (cb) { await driver.tapElement(cb); await sleep(500); break; } } const ubtn = await driver.findElementRaw('predicate string', 'name == "Unlock" OR label == "Unlock"').catch(() => null); if (ubtn) { await driver.tapElement(ubtn); console.log('已确认远程解锁(iOS)'); } } else { 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) || '[P0]'; // Lock Lite 等无控制 step → [P0] 兜底 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) { // 首页无该锁(本平台无实体/未添加)= 无设备 → 记 SKIP(N·A),不算失败;真控制失败才 FAIL。 if (/找不到.*卡片|读不到.*状态|not found/.test(e.message)) { reporter.record(`${ANCHOR} ${lock.name} 解锁`, 'SKIP', Date.now() - start, `首页无 ${lock.name}(未添加/本平台无实体),跳过`); return; } 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) { if (/找不到.*卡片|读不到.*状态|not found/.test(e.message)) { reporter.record(`${ANCHOR} ${lock.name} 上锁`, 'SKIP', Date.now() - start, `首页无 ${lock.name}(未添加/本平台无实体),跳过`); return; } const ss = await driver.screenshot().catch(() => ''); reporter.record(`${ANCHOR} ${lock.name} 上锁`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); } });