AI_UIAutomation/tests/air_purifier/air_purifier_feature_contro...

190 lines
10 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, setupResilientHooks, findHomeCard } from '../../utils/common';
import * as dotenv from 'dotenv';
import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
// Air Purifier(空净)功能页控制(在首页卡片控制 air_purifier_card 之外补充):点卡**弹浮层 → 点 "More" 进完整功能页**(两端都需要,之前漏点 More 一直在浮层操作)。
// 功能页(实测):中央状态圈 待机="On standby"/运行=空气质量("Very Good"等);独立无名电源圆键(Android 坐标 W*0.5,H*0.694;
// iOS 是居中无名 Other ~74x74 @ cy~H*0.712,**坐标 tap 不 actuate → 必须 tapElement**);模式行 Level/Auto/Pet/Sleep;
// 底部 Environmental Data / Schedules(功能页特有,用于判定"已进功能页而非浮层")。
const CARD_KEYWORD = process.env.AIR_PURIFIER_CARD || 'Air Purifier';
const MODES = ['Level', 'Auto', 'Pet', 'Sleep'];
const CTRL = '[P0]'; // it() 标题显示用;实际回写锚点由 rec()/apNames 双发(美规+日规)
// 空净**不分美规/日规**:两个 step(29 美规 7qxA1ajE / 30 日规 R5PDp8BR,均在 15975 BLE 超级用例)都写同一结果。
// wifi 轮无对应 15974 step(uuid 未知)→ 回退 [P0]。
const AP_BLE_STEPS = ['7qxA1ajE', 'R5PDp8BR'];
const apNames = (base: string): string[] =>
process.env.PROTO === 'wifi' ? [`[P0] ${base}`] : AP_BLE_STEPS.map(u => `[P0][ONES:15975#${u}][ble] ${base}`);
describe('AirPurifier Feature Control - 功能页控制(进设备页 电源/模式)', () => {
let driver: DeviceDriver;
let reporter: TestReporter;
let W = 1080, H = 2400;
setupResilientHooks(() => driver);
const isIOS = () => driver.platform === 'ios';
// 双发回写:空净不分区,同一结果写 美规+日规 两个 step。
const rec = (base: string, status: 'PASS' | 'FAIL' | 'SKIP', dur: number, detail: string, ss?: string) => {
for (const n of apNames(base)) reporter.record(n, status, dur, detail, ss);
};
beforeAll(async () => {
driver = createDriver();
await driver.createSession();
reporter = new TestReporter('AirPurifier_Feature_Control', driver.platform.toUpperCase());
try { const s = await driver.getWindowSize(); if (s?.width) { W = s.width; H = s.height; } } catch { /* 默认 */ }
});
beforeEach(async () => {
await driver.dismissPopupIfPresent();
await driver.goBackToHomepage();
await sleep(500);
await driver.dismissPopupIfPresent();
});
afterAll(async () => {
reporter.generate();
await driver.destroySession();
});
// 读中央状态字(y 800-1150):"On standby"(待机/关) 或 空气质量读数(运行)。
function readStatus(src: string): string {
if (isIOS()) {
const m = src.match(/(?:name|label|value)="(On standby|Off|Very Good|Good|Moderate|Poor|Very Poor|Unhealthy[^"]*)"/i);
return m ? m[1] : 'unknown';
}
for (const m of src.matchAll(/text="([^"]+)"[^>]*bounds="\[\d+,(\d+)\]\[\d+,\d+\]"/g)) {
const y = +m[2];
if (y > 800 && y < 1150 && m[1].trim().length >= 2) return m[1].trim();
}
return 'unknown';
}
// 待机/关机态:"On standby" / "Off" / 离线
const isStandby = (s: string) => /On standby|^Off$|Offline|待机/i.test(s);
// 点电源圆键:Android 坐标 (W*0.5,H*0.694);iOS 无名 Other 坐标 tap 不生效 → 找居中方形 Other(cy~H*0.71)tapElement。
async function tapPower(): Promise<void> {
if (isIOS()) {
const els = await driver.findElementsRaw('predicate string', 'type == "XCUIElementTypeOther" OR type == "XCUIElementTypeButton"').catch(() => [] as string[]);
let best: { e: string; d: number } | null = null;
for (const e of els) {
const r = await driver.getElementRect(e).catch(() => null); if (!r) continue;
const cx = r.x + r.width / 2, cy = r.y + r.height / 2;
// 居中、55-100px 近方形、cy 在 H*0.62~0.82(状态圈在上、模式行在下之间)
if (Math.abs(cx - W / 2) < 30 && r.width >= 55 && r.width <= 100 && Math.abs(r.width - r.height) < 25 && cy > H * 0.62 && cy < H * 0.82) {
const d = Math.abs(cy - H * 0.712);
if (!best || d < best.d) best = { e, d };
}
}
if (best) { await driver.tapElement(best.e); await sleep(1500); return; }
await driver.tap(Math.round(W * 0.5), Math.round(H * 0.712)); await sleep(1500); // 兜底
return;
}
await driver.tap(Math.round(W * 0.5), Math.round(H * 0.694)); await sleep(1500);
}
async function waitUntil(pred: (s: string) => boolean, ms = 16000): Promise<string> {
const deadline = Date.now() + ms;
let st = readStatus(await driver.getSource());
while (Date.now() < deadline && !pred(st)) { await sleep(1500); st = readStatus(await driver.getSource()); }
return st;
}
// 进空净功能页:点卡(弹浮层)→ 点 "More" 进完整功能页 →(关 Got it/OK)→ 等功能页就绪(Environmental Data/Schedules,浮层没有)。
async function enterFeature(): Promise<boolean> {
const card = await findHomeCard(driver, CARD_KEYWORD);
if (!card) { console.log(`[APFeat] 首页找不到 ${CARD_KEYWORD}`); return false; }
await driver.tapElement(card); // tapElement 更可靠(拥挤首页坐标易误点)
await sleep(2000);
for (let i = 0; i < 8; i++) {
const src = await driver.getSource().catch(() => '');
if (/Environmental Data|Schedules/.test(src)) break; // 已在功能页(Android 点卡直进,无 More)→ 不空等
const more = isIOS()
? await driver.findElementRaw('predicate string', 'name == "More" OR label == "More"').catch(() => null)
: await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("More")').catch(() => null);
if (more) { await driver.tapElement(more); await sleep(2500); console.log('[APFeat] 点 More 进功能页'); break; }
await sleep(700);
}
for (let i = 0; i < 3; i++) {
const g = isIOS()
? await driver.findElementRaw('predicate string', 'name == "Got it" OR name == "OK"').catch(() => null)
: await driver.findElementRaw('-android uiautomator', 'new UiSelector().textMatches("Got it|OK")').catch(() => null);
if (!g) break; await driver.tapElement(g); await sleep(1000);
}
// 就绪:功能页特有的 Environmental Data / Schedules 出现(区分浮层),且能读到状态
for (let i = 0; i < 8; i++) {
const src = await driver.getSource();
if (/Environmental Data|Schedules/.test(src) && readStatus(src) !== 'unknown') return true;
await sleep(1000);
}
return /Environmental Data|Schedules/.test(await driver.getSource().catch(() => ''));
}
it(`${CTRL} 功能页电源开/关(Air Purifier)`, { timeout: 120000 }, async () => {
const start = Date.now();
try {
if (!(await enterFeature())) {
// 首页有该空净却进不去功能页 = 未正确执行 → FAIL(交给 catch);首页确无该空净(未添加/本平台无实体)→ 合法 SKIP。
await driver.goBackToHomepage().catch(() => {}); await sleep(1200);
if (await findHomeCard(driver, CARD_KEYWORD).catch(() => null)) throw new Error('进功能页失败(首页有该空净但进不去功能页,未正确执行)');
rec('功能页电源开/关', 'SKIP', Date.now() - start, '首页无该空净(未添加/本平台无实体),跳过');
return;
}
const s0 = readStatus(await driver.getSource());
console.log(`[APFeat] 初始: ${s0}`);
// 先确保待机态,再开机:待机 → 点电源 → 运行(状态≠待机,显示空气质量)
if (!isStandby(s0)) { await tapPower(); await waitUntil(isStandby); }
await tapPower();
const sOn = await waitUntil(s => !isStandby(s) && s !== 'unknown');
console.log(`[APFeat] 开机后: ${sOn}`);
expect(isStandby(sOn)).toBe(false); // 已运行(状态翻出待机)
// 关机:运行 → 点电源 → "On standby"
await tapPower();
const sOff = await waitUntil(isStandby);
console.log(`[APFeat] 关机后: ${sOff}`);
expect(isStandby(sOff)).toBe(true);
rec('功能页电源开/关', 'PASS', Date.now() - start, `电源开/关均生效(${s0}→开:${sOn}→关:${sOff})`);
} catch (e: any) {
const ss = await driver.screenshot().catch(() => '');
rec('功能页电源开/关', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it(`${CTRL} 功能页切换模式(Level/Auto/Pet/Sleep)`, { timeout: 120000 }, async () => {
const start = Date.now();
try {
if (!(await enterFeature())) {
// 首页有该空净却进不去功能页 = 未正确执行 → FAIL(交给 catch);首页确无该空净(未添加/本平台无实体)→ 合法 SKIP。
await driver.goBackToHomepage().catch(() => {}); await sleep(1200);
if (await findHomeCard(driver, CARD_KEYWORD).catch(() => null)) throw new Error('进功能页失败(首页有该空净但进不去功能页,未正确执行)');
rec('功能页切换模式', 'SKIP', Date.now() - start, '首页无该空净(未添加/本平台无实体),跳过');
return;
}
// 切模式前需先开机(待机时模式置灰)
if (isStandby(readStatus(await driver.getSource()))) { await tapPower(); await waitUntil(s => !isStandby(s) && s !== 'unknown'); }
const switched: string[] = [];
for (const m of MODES) {
const cont = await driver.findElementRaw('-android uiautomator', `new UiSelector().clickable(true).childSelector(new UiSelector().text("${m}"))`).catch(() => null)
|| await driver.findElementRaw(isIOS() ? 'predicate string' : '-android uiautomator', isIOS() ? `name == "${m}" OR label == "${m}"` : `new UiSelector().text("${m}")`).catch(() => null);
if (cont) { await driver.tapElement(cont); await sleep(2000); switched.push(m); }
else console.log(`[APFeat] 未找到模式键: ${m}`);
}
console.log(`[APFeat] 已切换模式: ${switched.join('/')}`);
expect(switched.length).toBe(MODES.length);
rec('功能页切换模式', 'PASS', Date.now() - start, `依次切换 ${switched.join('/')} 成功`);
} catch (e: any) {
const ss = await driver.screenshot().catch(() => '');
rec('功能页切换模式', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
});