172 lines
9.1 KiB
TypeScript
172 lines
9.1 KiB
TypeScript
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;
|
||
}
|
||
});
|
||
}
|
||
});
|