AI_UIAutomation/tests/lock/lock_control.test.ts

172 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<boolean> {
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<void> {
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<boolean> {
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;
}
});
}
});