192 lines
10 KiB
TypeScript
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;
|
|
}
|
|
});
|
|
});
|