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, waitForSource, logVisibleDeviceCards } from '../../utils/common'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); // Evaporative Humidifier(口头"加湿器2",WiFi):控制必测=**首页卡片快捷控制浮层**(非功能页)。 // 点首页卡片 → 弹浮层:顶部状态字(Off/运行) + **无文案电源圆键**(屏中线、状态字下方/模式行上方) + 模式行(Level/Sleep/Humidity/Auto) + More。 // 电源键无文字,按几何点:px=模式行左右中点(≈屏中线),py=Off底与Level顶之间(靠 Off,约 +22%)。 // 切换模式需先开机。ONES 控制号待补,先 [P0] 占位。 const CARD_KEYWORD = process.env.HUMIDIFIER2_CARD || 'Evaporative Humidifier'; const ADD_ANCHOR = '[P0]'; // TODO: 待补 Humidifier2 控制 ONES 编号 describe('Humidifier2 Card - 首页卡片控制(Evaporative Humidifier)', () => { let driver: DeviceDriver; let reporter: TestReporter; beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter('Humidifier2_Card', driver.platform.toUpperCase()); }); beforeEach(async () => { await driver.dismissPopupIfPresent(); await driver.goBackToHomepage(); await driver.dismissPopupIfPresent(); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); async function findCard(): Promise { const { height: winH } = await driver.getWindowSize().catch(() => ({ height: 844 })); for (let r = 0; r < 14; r++) { const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${CARD_KEYWORD}")`).catch(() => null); if (el) { const rect = await driver.getElementRect(el).catch(() => null); if (rect && rect.width > 0 && rect.y > winH * 0.06 && rect.y + rect.height < winH * 0.92) return el; // 屏内才用(WDA 会返回屏外元素) } await driver.scrollDown(500); await sleep(800); // 平台无关(iOS 旧的 swipe(540,..) x 在屏外无效) if (driver.platform === 'ios' && !(await driver.isOnHomepage().catch(() => true))) { await driver.goBackToHomepage().catch(() => {}); await sleep(800); } // 误点离开自愈 } await logVisibleDeviceCards(driver, `找不到${CARD_KEYWORD}卡片`); throw new Error(`找不到${CARD_KEYWORD}卡片`); } // 点卡片中心 → 弹快捷控制浮层 async function openCardPopup(): Promise { const cardId = await findCard(); const rect = await driver.getElementRect(cardId); await driver.tap(rect.x + rect.width / 2, rect.y + rect.height / 2); await sleep(2000); const ok = await waitForSource(driver, 'More', 6000) || await waitForSource(driver, 'Level', 3000); if (!ok) throw new Error('卡片快捷控制浮层未弹出'); } // 无文案电源圆键:浮层是固定底部弹层,电源键在屏中线、状态字下方/模式行上方。 // 实测 1080x2400:电源键中心 ≈ (540, 865)。按屏宽高比例算,兼容分辨率。 async function tapPower(): Promise { let w = 1080, h = 2400; try { const s: any = await (driver as any).getWindowSize?.(); if (s?.width) { w = s.width; h = s.height; } } catch { /* 默认 1080x2400 */ } const px = Math.round(w / 2); const py = Math.round(h * 0.36); // 2400*0.36≈864 console.log(`点电源键 (${px}, ${py})`); await driver.tap(px, py); } // 当前是否关机:取**首个**含设备名(首页卡)后的状态字 —— 首页卡副标题镜像设备态:关=Off,开=模式/数值。 // 实测此判定对开/关/模式转换均正确(浮层叠层 getSource 混入背景,故读稳定的首页卡状态字)。 async function isOff(): Promise { const src = await driver.getSource(); let status = ''; if (driver.platform === 'ios') { // iOS:首页卡名拼接状态(如 "Evaporative Humidifier 6EOff" / "...Drying filter"),source 用 name/label/value(非 text=)。 // 取含设备名的那条整串判断(关=含 Off/Drying;开=含模式/Auto/Level%,不含 Off)。 const vals = Array.from(src.matchAll(/(?:name|label|value)="([^"]+)"/g)).map((m) => m[1]); status = vals.find((t) => t.includes(CARD_KEYWORD)) || ''; } else { const texts = Array.from(src.matchAll(/text="([^"]+)"/g)).map((m) => m[1]); const i = texts.findIndex((t) => t.includes(CARD_KEYWORD)); status = i >= 0 ? (texts[i + 1] || '') : ''; } console.log(`状态字="${status}"`); // 关机判定:Off/Offline/待机 或风干(Drying filter/风干)。iOS 状态粘在卡名后缀(如 "6EOff")→ 用子串匹配(不用 \b); // 开机态是模式/Auto/Level%,不含 "Off" → 子串安全。 return /Off|Offline|待机|Drying filter|Drying|风干/i.test(status); } // 轮询直到目标态(设备走 WiFi,状态同步可能需几秒) async function pollState(wantOff: boolean, ms = 15000): Promise { const deadline = Date.now() + ms; while (Date.now() < deadline) { if ((await isOff()) === wantOff) return true; await sleep(1500); } return (await isOff()) === wantOff; } it(`${ADD_ANCHOR} 首页卡片开机`, async () => { const start = Date.now(); try { await openCardPopup(); // 先确保关机,再点电源开机,验证状态变为运行 if (!(await isOff())) { await tapPower(); await pollState(true); } await tapPower(); const on = await pollState(false); console.log(`开机后 isOff=${!on}`); expect(on).toBe(true); reporter.record(`${ADD_ANCHOR} 首页卡片开机`, 'PASS', Date.now() - start, '卡片浮层电源键开机成功(状态→运行)'); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${ADD_ANCHOR} 首页卡片开机`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it(`${ADD_ANCHOR} 首页卡片切换模式(Sleep)`, async () => { const start = Date.now(); try { await openCardPopup(); // 切换模式前需先开机 if (await isOff()) { await tapPower(); await pollState(false); } const el = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Sleep")').catch(() => null); expect(el).not.toBeNull(); await driver.tapElement(el!); await sleep(2500); console.log('开机后切 Sleep 模式成功'); reporter.record(`${ADD_ANCHOR} 首页卡片切换模式(Sleep)`, 'PASS', Date.now() - start, '先开机再切 Sleep 成功'); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${ADD_ANCHOR} 首页卡片切换模式(Sleep)`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it(`${ADD_ANCHOR} 首页卡片 Level 模式切换档位(1-4)`, async () => { const start = Date.now(); try { await openCardPopup(); if (await isOff()) { await tapPower(); await pollState(false); } const lvl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Level")').catch(() => null); expect(lvl).not.toBeNull(); await driver.tapElement(lvl!); await sleep(2000); // Level 模式展开后出现档位 1/2/3/4,依次切换(从 2 起切,避免与默认重复) const gears: string[] = []; for (const g of ['2', '3', '4', '1']) { const ge = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${g}")`).catch(() => null); if (ge) { await driver.tapElement(ge); await sleep(1500); gears.push(g); } } console.log(`Level 档位已切换: ${gears.join('/')}`); expect(gears.length).toBeGreaterThanOrEqual(3); reporter.record(`${ADD_ANCHOR} 首页卡片 Level 档位切换(1-4)`, 'PASS', Date.now() - start, `切换档位 ${gears.join('/')} 成功`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${ADD_ANCHOR} 首页卡片 Level 档位切换(1-4)`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it(`${ADD_ANCHOR} 首页卡片 Humidity/Auto 可用性(需绑温湿度计)`, async () => { const start = Date.now(); try { await openCardPopup(); if (await isOff()) { await tapPower(); await pollState(false); } // Humidity/Auto 需先在设置页绑定温湿度计才可用;未绑定时置灰(enabled=false)。两种结果都算 PASS,记录实测状态。 const results: string[] = []; for (const m of ['Humidity', 'Auto']) { const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${m}")`).catch(() => null); expect(el).not.toBeNull(); const enabled = (await driver.getElementAttribute(el!, 'enabled').catch(() => 'true')) === 'true'; if (enabled) { await driver.tapElement(el!); await sleep(2000); results.push(`${m}=可用(已切换)`); } else { results.push(`${m}=置灰(未绑温湿度计)`); } } console.log(results.join(', ')); reporter.record(`${ADD_ANCHOR} 首页卡片 Humidity/Auto 可用性`, 'PASS', Date.now() - start, results.join(', ')); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${ADD_ANCHOR} 首页卡片 Humidity/Auto 可用性`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it(`${ADD_ANCHOR} 首页卡片关机`, async () => { const start = Date.now(); try { await openCardPopup(); // 先确保开机,再点电源关机 if (await isOff()) { await tapPower(); await pollState(false); } await tapPower(); const off = await pollState(true); console.log(`关机后 isOff=${off}`); expect(off).toBe(true); reporter.record(`${ADD_ANCHOR} 首页卡片关机`, 'PASS', Date.now() - start, '卡片浮层电源键关机成功(状态→Off)'); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${ADD_ANCHOR} 首页卡片关机`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); });