AI_UIAutomation/tests/humidifier/humidifier2_feature_control...

192 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, onesCtrl, setupResilientHooks, findHomeCard } from '../../utils/common';
import * as dotenv from 'dotenv';
import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
// Evaporative Humidifier(口头"加湿器2",WiFi)功能页控制(在首页卡片 humidifier2_card 之外补充):
// 点卡 → 弹快捷浮层 → 点 "More" 进完整功能页。
// 功能页(实测 6E 1080x2400):中央大圈状态字(y~900)。
// ★关键语义:**关机态 = "Drying filter / Remaining HH:MM:SS"(关机自动风干滤芯,模式置灰)**;开机态 = "On standby"(待机) 或运行模式。
// 从关机(风干)点电源 → 弹 "Stop drying filter and turn on the device?"(Cancel/Confirm)→ Confirm 才开机。
// 独立电源圆键(无文案,中心 ~540,1541);模式行 Level/Sleep/Humidity/Auto(clickable 容器,需先开机)。
const CARD_KEYWORD = process.env.HUMIDIFIER2_CARD || 'Evaporative Humidifier';
const MODES = ['Level', 'Sleep', 'Humidity', 'Auto'];
const CTRL = onesCtrl('humidifier', 'Evaporative Humidifier') || '[P0]'; // 控制锚点未建(必测=绑温湿度计)→ [P0] 占位
describe('Humidifier2 Feature Control - 功能页控制(进设备页 电源/模式)', () => {
let driver: DeviceDriver;
let reporter: TestReporter;
let W = 1080, H = 2400;
setupResilientHooks(() => driver);
const isIOS = () => driver.platform === 'ios';
beforeAll(async () => {
driver = createDriver();
await driver.createSession();
reporter = new TestReporter('Humidifier2_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-1100 首条):"On standby"(开/待机) / "Drying filter"|"Remaining ..."(关/风干) / 运行读数。
function readCenter(src: string): string {
if (isIOS()) {
const m = src.match(/(?:name|label|value)="(On standby|Drying filter[^"]*|Remaining[^"]*|Off[^"]*|[0-9]{1,3}%[^"]*)"/i);
return m ? m[1].trim() : 'unknown';
}
for (const m of src.matchAll(/text="([^"]+)"[^>]*bounds="\[\d+,(\d+)\]\[\d+,\d+\]"/g)) {
const y = +m[2];
if (y > 700 && y < 1150 && m[1].trim().length >= 1) return m[1].trim(); // 开机档位数字在 y~771,放宽下界
}
return 'unknown';
}
// 关机/待机态(非运行):"Drying filter"(关机自动风干) / "Remaining" / "On standby"(待机,点电源→运行) / Off。运行态=档位数字/湿度。
// 实测电源循环:"1"(运行) ↔ "Drying filter"(关);"On standby" 也属非运行(点它→"1")→ 一并计 OFF,否则误判为开、流程错乱。
const isOff = (s: string) => /Drying filter|Drying|Remaining|On standby|^Off|Offline|风干|待机/i.test(s);
// 电源圆键坐标:动态找居中可点 ViewGroup(y 1380-1720,x~540),兜底 W*0.5,H*0.642
async function powerXY(): Promise<{ x: number; y: number }> {
const src = await driver.getSource().catch(() => '');
for (const m of src.matchAll(/class="[^"]*ViewGroup"[^>]*clickable="true"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g)) {
const cx = (+m[1] + +m[3]) / 2, cy = (+m[2] + +m[4]) / 2;
if (cy > 1380 && cy < 1720 && cx > 420 && cx < 660) return { x: Math.round(cx), y: Math.round(cy) };
}
return { x: Math.round(W * 0.5), y: Math.round(H * 0.642) };
}
// 点电源键 + 自动确认"停止风干并开机"弹框(开机方向才有)
async function tapPower(): Promise<void> {
const p = await powerXY();
await driver.tap(p.x, p.y);
await sleep(1500);
// 弹框:含 "Stop drying filter and turn on" → 点 Confirm
const src = await driver.getSource().catch(() => '');
if (/turn on the device|Stop drying/i.test(src)) {
const c = isIOS()
? await driver.findElementRaw('predicate string', 'name == "Confirm" OR label == "Confirm"').catch(() => null)
: await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")').catch(() => null);
if (c) { await driver.tapElement(c); await sleep(1500); console.log('[HM2Feat] 确认"停止风干并开机"弹框'); }
}
}
async function waitUntil(pred: (s: string) => boolean, ms = 18000): Promise<string> {
const deadline = Date.now() + ms;
let st = readCenter(await driver.getSource());
while (Date.now() < deadline && !pred(st)) { await sleep(1500); st = readCenter(await driver.getSource()); }
return st;
}
async function ensureOn(): Promise<void> {
if (isOff(readCenter(await driver.getSource()))) { await tapPower(); await waitUntil(s => !isOff(s) && s !== 'unknown'); }
}
// 进功能页:点卡 → 弹浮层点 "More" 进功能页(点掉 Got it)→ 等中央状态字出现
async function enterFeature(): Promise<boolean> {
const card = await findHomeCard(driver, CARD_KEYWORD);
if (!card) { console.log(`[HM2Feat] 首页找不到 ${CARD_KEYWORD}`); return false; }
const r = await driver.getElementRect(card);
await driver.tap(r.x + r.width / 2, r.y + r.height / 2);
await sleep(2000);
for (let i = 0; i < 8; i++) {
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('[HM2Feat] 点 More 进功能页'); break; }
await sleep(800);
}
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(1200);
}
for (let i = 0; i < 8; i++) {
if (readCenter(await driver.getSource()) !== 'unknown') return true;
await sleep(1000);
}
return readCenter(await driver.getSource()) !== 'unknown';
}
it(`${CTRL} 功能页电源开/关(加湿器2)`, { timeout: 150000 }, 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('进功能页失败(首页有该加湿器但进不去功能页,未正确执行)');
reporter.record(`${CTRL} 功能页电源开/关`, 'SKIP', Date.now() - start, '首页无该加湿器(未添加/本平台无实体),跳过');
return;
}
console.log(`[HM2Feat] 初始: ${readCenter(await driver.getSource())}`);
// 先确保开机态(开机=非风干)
await ensureOn();
const sOnStart = readCenter(await driver.getSource());
console.log(`[HM2Feat] 开机态: ${sOnStart}`);
expect(isOff(sOnStart)).toBe(false);
// 关机:开机 → 点电源 → 风干(Drying filter/Remaining)
await tapPower();
const sOff = await waitUntil(isOff);
console.log(`[HM2Feat] 关机后: ${sOff}`);
expect(isOff(sOff)).toBe(true);
// 开机:风干 → 点电源 → 确认弹框 → 开机(非风干)
await tapPower();
const sOn = await waitUntil(s => !isOff(s) && s !== 'unknown');
console.log(`[HM2Feat] 再开机后: ${sOn}`);
expect(isOff(sOn)).toBe(false);
reporter.record(`${CTRL} 功能页电源开/关`, 'PASS', Date.now() - start, `电源开/关均生效(开:${sOnStart}→关:${sOff}→开:${sOn})`);
} catch (e: any) {
const ss = await driver.screenshot().catch(() => '');
reporter.record(`${CTRL} 功能页电源开/关`, 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it(`${CTRL} 功能页切换模式(Level/Sleep/Humidity/Auto)`, { timeout: 150000 }, 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('进功能页失败(首页有该加湿器但进不去功能页,未正确执行)');
reporter.record(`${CTRL} 功能页切换模式`, 'SKIP', Date.now() - start, '首页无该加湿器(未添加/本平台无实体),跳过');
return;
}
await ensureOn(); // 切模式前需先开机(关机/风干时模式置灰)
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(`[HM2Feat] 未找到模式键: ${m}`);
}
console.log(`[HM2Feat] 已切换模式: ${switched.join('/')}`);
expect(switched.length).toBe(MODES.length);
reporter.record(`${CTRL} 功能页切换模式`, 'PASS', Date.now() - start, `依次切换 ${switched.join('/')} 成功`);
} catch (e: any) {
const ss = await driver.screenshot().catch(() => '');
reporter.record(`${CTRL} 功能页切换模式`, 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
});