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, setupResilientHooks, tapHomeCard, findHomeCard, onesCtrlByCard } from '../../utils/common'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); // 通用灯功能页控制(在首页卡片控制 eave_light_control 之外补充):点卡 → 弹浮层(读状态)→ 点 "More" 进功能页 → 底部圆形电源键 开/关。 // ★ 提速:功能页 on/off **无任何可读信号**(电源键 selected/enabled/亮度% 切换前后都不变)→ 状态只能靠弹窗读。 // 故全程**只 findHomeCard 2 次**:开弹窗读初态(顺势从该弹窗进 More)→ 功能页点电源 → 再开弹窗读终态。不反复回首页搜卡。 const CARD_KEYWORD = process.env.LIGHT_CARD || 'Color Bulb'; const CTRL = onesCtrlByCard(CARD_KEYWORD) || '[P0]'; // 按卡片名取灯类控制锚点(15975/15974 step);未命中回退 [P0] describe('Light 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(`LightFeat_${CARD_KEYWORD}`, 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); }); // 收尾:把灯切回关闭态(hookTimeout 给足 90s) afterAll(async () => { try { if ((await popupState()) === 'On') { await moreToFeature(); await tapPower(); console.log('[LightFeat] 收尾关灯'); } await driver.dismissPopupIfPresent(); } catch (e: any) { console.log('[LightFeat] 收尾关灯失败:', e.message); } reporter.generate(); await driver.destroySession(); }, 90000); function boundsOf(src: string, label: string): { cx: number; cy: number } | null { const esc = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const m = src.match(new RegExp(`(?:text|content-desc|name|label)="${esc}"[^>]*bounds="\\[(\\d+),(\\d+)\\]\\[(\\d+),(\\d+)\\]"`)); return m ? { cx: Math.round((+m[1] + +m[3]) / 2), cy: Math.round((+m[2] + +m[4]) / 2) } : null; } // 【1 次 findHomeCard】回首页 → 点卡开快捷弹窗 → 读状态("On | NN% ..."/"Off"),**弹窗保持打开**(供随后点 More)。 async function popupState(): Promise<'On' | 'Off' | 'unknown'> { await driver.dismissPopupIfPresent(); await driver.goBackToHomepage(); await sleep(800); try { await tapHomeCard(driver, CARD_KEYWORD, { resetPosition: 'top', logOnFail: true }); } catch { return 'unknown'; } await sleep(1200); const src = await driver.getSource(); if (/(?:text|name|label)="On ?\|/.test(src)) return 'On'; if (/(?:text|name|label)="Off"/.test(src)) return 'Off'; return 'unknown'; } // 从**已打开的弹窗**点 More 进功能页(关 Attention/OK,灯未加载点 Reconnect),等功能页就绪。不再 findHomeCard。 // 就绪检测放宽:各灯功能页内容不同(color bulb=White/Color tabs;Ceiling Pro=场景/Brightness;融蜡灯=仅 Brightness)→ // 只要出现 Brightness/Schedules/Delay/%/Favorites/Color Library/White 任一即视为进了功能页。 async function moreToFeature(): Promise { 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); } for (let i = 0; i < 12; i++) { const src = await driver.getSource().catch(() => ''); // 灯未正确加载会显示 "Reconnect" → 点它重连 const rc = boundsOf(src, 'Reconnect'); if (rc) { await driver.tap(rc.cx, rc.cy); await sleep(2500); console.log('[LightFeat] 点 Reconnect 重连'); continue; } const ok = boundsOf(src, 'OK'); if (ok) { await driver.tap(ok.cx, ok.cy); await sleep(1000); continue; } if (boundsOf(src, 'White') || /COLOR|Favorites|Color Library|Brightness|Schedules|Delay|\d+%/.test(src)) return true; await sleep(800); } return false; } // 点底部圆形电源键:动态找底部居中可点元素(cy>H*0.85),兜底 (W*0.5, H*0.935) async function tapPower(): Promise { const src = await driver.getSource().catch(() => ''); let best: { cx: number; cy: number } | null = null; for (const m of src.matchAll(/clickable="true"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g)) { const cx = (+m[1] + +m[3]) / 2, cy = (+m[2] + +m[4]) / 2, w = +m[3] - +m[1]; if (cy > H * 0.85 && cy < H * 0.99 && cx > W * 0.35 && cx < W * 0.65 && w < W * 0.5 && (!best || cy > best.cy)) best = { cx: Math.round(cx), cy: Math.round(cy) }; } const p = best || { cx: Math.round(W * 0.5), cy: Math.round(H * 0.935) }; await driver.tap(p.cx, p.cy); await sleep(2500); } it(`${CTRL} 功能页控制${CARD_KEYWORD}(开/关)`, { timeout: 120000 }, async () => { const start = Date.now(); try { // 搜索#1:开弹窗读初态(弹窗保持打开) const s0 = await popupState(); if (s0 === 'unknown') { // 首页有该灯却读不到状态/开不了弹窗 = 未正确执行 → FAIL(交给 catch);首页确无该灯(未添加/本平台无实体)→ 合法 SKIP。 await driver.goBackToHomepage().catch(() => {}); await sleep(1200); if (await findHomeCard(driver, CARD_KEYWORD).catch(() => null)) throw new Error('功能页控制失败(首页有该灯但读不到状态/进不去,未正确执行)'); reporter.record(`${CTRL} 功能页控制${CARD_KEYWORD}`, 'SKIP', Date.now() - start, '首页无该灯(未添加/本平台无实体),跳过'); return; } console.log(`[LightFeat] ${CARD_KEYWORD} 初始: ${s0}`); // 从该弹窗进功能页 → 点电源键(切到相反态) if (!(await moreToFeature())) throw new Error('从弹窗进功能页失败'); await tapPower(); // 搜索#2:再开弹窗读终态 const s1 = await popupState(); const target = s0 === 'On' ? 'Off' : 'On'; console.log(`[LightFeat] 功能页电源后: ${s1}(目标${target})`); expect(s1).toBe(target); reporter.record(`${CTRL} 功能页控制${CARD_KEYWORD}`, 'PASS', Date.now() - start, `功能页电源切换生效(${s0}→${s1})`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${CTRL} 功能页控制${CARD_KEYWORD}`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); });