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 { 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 { 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 { if (isOff(readCenter(await driver.getSource()))) { await tapPower(); await waitUntil(s => !isOff(s) && s !== 'unknown'); } } // 进功能页:点卡 → 弹浮层点 "More" 进功能页(点掉 Got it)→ 等中央状态字出现 async function enterFeature(): Promise { 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; } }); });