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') }); // Plug 功能页控制(在首页卡片控制 plug_card 之外补充):点卡**直接进功能页**(无弹窗/无 More) → 点大圆形电源键 开/关 → 读 On/Off 校验翻转。 // 功能页(实测 Plug EU):Power/Current/Voltage 统计 + 大圆形电源键(clickable ViewGroup 中心 ~540,849,内含 "On"/"Off" 状态字)。 // 覆盖三台:卡名按添加时区域改名 "Plug JP/US/EU"(见 plug_mini_connect)。锚点:JP/US 同 Plug Mini(PlugMini 5E),EU 为 eu Plug。 const PLUGS = [ { name: process.env.PLUG_JP_DEVICE || 'Plug JP', model: 'PlugMini 5E' }, { name: process.env.PLUG_US_DEVICE || 'Plug US', model: 'PlugMini 5E' }, { name: process.env.PLUG_EU_DEVICE || 'Plug EU', model: 'eu Plug' }, ]; describe('Plug Feature Control - 功能页控制(进设备页 开/关电源)', () => { let driver: DeviceDriver; let reporter: TestReporter; setupResilientHooks(() => driver); const isIOS = () => driver.platform === 'ios'; beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter('Plug_Feature_Control', driver.platform.toUpperCase()); }); beforeEach(async () => { await driver.dismissPopupIfPresent(); await driver.goBackToHomepage(); await sleep(500); await driver.dismissPopupIfPresent(); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); // 读电源状态:电源键区域(y 600-1100)的 "On"/"Off" 文字。iOS 同名标签。 function readState(src: string): 'On' | 'Off' | 'unknown' { if (isIOS()) { const m = src.match(/(?:name|label|value)="(On|Off)"/); return m ? (m[1] as 'On' | 'Off') : 'unknown'; } for (const m of src.matchAll(/text="(On|Off)"[^>]*bounds="\[\d+,(\d+)\]\[\d+,\d+\]"/g)) { const y = +m[2]; if (y > 550 && y < 1150) return m[1] as 'On' | 'Off'; } return 'unknown'; } // 点大圆形电源键:Android 用 clickable 容器(内含 On/Off 字)tapElement;兜底点中心坐标。iOS 找 Power/电源键。 async function tapPower(): Promise { if (!isIOS()) { const cont = await driver.findElementRaw('-android uiautomator', 'new UiSelector().clickable(true).childSelector(new UiSelector().textMatches("On|Off"))').catch(() => null); if (cont) { await driver.tapElement(cont); return true; } const wh = await driver.getWindowSize().catch(() => ({ width: 1080, height: 2400 })); await driver.tap(Math.round(wh.width * 0.5), Math.round(wh.height * 0.354)); // 电源键中心 ~540,849 return true; } // iOS:电源钮=中央 On/Off 圆形 tile(name/label 为 "On"/"Off")。**不能匹配 "Power"** —— 顶部有个 // "Power 0.0W" 统计标签(StaticText name="Power"),它在树里排在电源钮之前,谓词含 Power 会先命中它 → // tapElement 点统计标签是空操作 → 状态永不翻转(旧 bug 根因)。故谓词只取 On/Off tile。 const p = await driver.findElementRaw('predicate string', 'name == "On" OR name == "Off"').catch(() => null); if (p) { await driver.tapElement(p); return true; } const wh = await driver.getWindowSize().catch(() => ({ width: 390, height: 844 })); await driver.tap(Math.round(wh.width * 0.5), Math.round(wh.height * 0.42)); return true; } async function waitState(want: 'On' | 'Off'): Promise<'On' | 'Off' | 'unknown'> { let st = readState(await driver.getSource()); for (let i = 0; i < 12 && st !== want; i++) { await sleep(1500); st = readState(await driver.getSource()); } return st; } // 进插座功能页:点卡直接进(无弹窗)→ 等电源键状态(On/Off)出现 async function enterFeature(name: string): Promise { const card = await findHomeCard(driver, name); if (!card) { console.log(`[PlugFeat] 首页找不到 ${name}`); return false; } await driver.tapElement(card); await sleep(2200); for (let i = 0; i < 8; i++) { if (readState(await driver.getSource()) !== 'unknown') return true; await sleep(1000); } return readState(await driver.getSource()) !== 'unknown'; } for (const plug of PLUGS) { const CTRL = onesCtrl('plug', plug.model) || '[P0]'; it(`${CTRL} 功能页控制${plug.name}(开+关)`, { timeout: 120000 }, async () => { const start = Date.now(); try { if (!(await enterFeature(plug.name))) { // 首页有该插座却进不去功能页 = 未正确执行 → FAIL(交给 catch);首页确无该插座(未添加/本平台无实体)→ 合法 SKIP。 await driver.goBackToHomepage().catch(() => {}); await sleep(1200); if (await findHomeCard(driver, plug.name).catch(() => null)) throw new Error('进功能页失败(首页有该插座但进不去功能页,未正确执行)'); reporter.record(`${CTRL} 功能页控制${plug.name}`, 'SKIP', Date.now() - start, '首页无该插座(未添加/本平台无实体),跳过'); return; } const s0 = readState(await driver.getSource()); console.log(`[PlugFeat] ${plug.name} 初始: ${s0}`); // 切到相反态 const target1 = s0 === 'On' ? 'Off' : 'On'; expect(await tapPower()).toBe(true); const s1 = await waitState(target1); console.log(`[PlugFeat] ${plug.name} 操作1后: ${s1}(目标${target1})`); expect(s1).toBe(target1); // 切回 const target2 = target1 === 'On' ? 'Off' : 'On'; await tapPower(); const s2 = await waitState(target2); console.log(`[PlugFeat] ${plug.name} 操作2后: ${s2}(目标${target2})`); expect(s2).toBe(target2); reporter.record(`${CTRL} 功能页控制${plug.name}`, 'PASS', Date.now() - start, `功能页开/关均生效(${s0}→${s1}→${s2})`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${CTRL} 功能页控制${plug.name}`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); } });