AI_UIAutomation/tests/lock/lock_card.test.ts

166 lines
8.3 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 } 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;
}
});
}
});