import { describe, it, beforeAll, afterAll, expect } from 'vitest'; import { DeviceDriver } from '../../drivers/types'; import { createDriver } from '../../drivers/factory'; import { TestReporter } from '../../utils/test-reporter'; import { sleep, onesCtrl } from '../../utils/common'; import { getDeviceName } from '../../config/device.config'; import { setupResilientHooks } from "../../utils/common"; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); // Plug Mini 控制(ONES step12 2jSFfY4k):首页卡片开关钮直接开/关,卡名下方 desText 显示 On/Off。 // **设备名取添加时注册的实际名**(getDeviceName: env PLUG_CTRL_DEVICE > 注册表"plug"(如 "Plug EU") > 默认), // 不写死——解决"Plug Mini/Plug EU"卡名不一致找不到卡片的问题。 const deviceName = getDeviceName('plug', 'PLUG_CTRL_DEVICE'); const CTRL_PLUG = onesCtrl('plug', 'PlugMini 5E'); // 15975#2jSFfY4k describe('Plug Mini Control - 首页卡片开关', () => { let driver: DeviceDriver; let reporter: TestReporter; setupResilientHooks(() => driver); beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter(`PlugMini_Control_${deviceName}`, driver.platform.toUpperCase()); }); beforeEach(async () => { await driver.dismissPopupIfPresent(); await driver.goBackToHomepage(); await sleep(500); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); // 找 plug 卡片:状态 = 卡名正下方同列的 "On"/"Off";开关钮在卡名右上方。 // RN 首页 UiScrollable.scrollIntoView 对部分卡片失效 → 手动滚动 + 从 getSource 解析卡名 bounds(滚到时卡片在视口下方,开关钮 y-95 自然不落顶栏)。 function findCardRect(src: string): { x: number; y: number; w: number } | null { for (const n of src.split('<')) { if (!/resource-id="[^"]*\/nameText"/.test(n)) continue; const t = (n.match(/text="([^"]*)"/) || [])[1]; if (!t || !t.includes(deviceName)) continue; const b = n.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/); if (b) return { x: +b[1], y: +b[2], w: +b[3] - +b[1] }; } return null; } async function plugInfo(): Promise<{ state: 'On' | 'Off' | 'unknown'; btnX: number; btnY: number; nameX: number; nameY: number } | null> { await driver.goBackToHomepage(); await sleep(800); await driver.dismissPopupIfPresent(); if (driver.platform === 'ios') { // iOS:plug 卡片同 lock —— Cell(name 形如 "Plug JPOff"),右上角内联开关钮(≈ cell.x+cell.w-32, cell.y+24)。滚动找。 const { height: winH } = await driver.getWindowSize().catch(() => ({ height: 844 })); let lastNames = ''; for (let r = 0; r < 16; r++) { const cell = await driver.findElementRaw('predicate string', `type == "XCUIElementTypeCell" AND name CONTAINS "${deviceName}"`).catch(() => null); if (cell) { const rr = await driver.getElementRect(cell).catch(() => null); if (rr && rr.width > 0 && rr.y > winH * 0.06 && rr.y + rr.height < winH * 0.92) { const cname = await driver.getElementAttribute(cell, 'name').catch(() => ''); const state: 'On' | 'Off' | 'unknown' = /Off/.test(cname) ? 'Off' : /On/.test(cname) ? 'On' : 'unknown'; return { state, btnX: Math.round(rr.x + rr.width - 32), btnY: Math.round(rr.y + 24), nameX: rr.x, nameY: rr.y }; } } // 到底检测:可见 cell 名集合不再变(不用 source 末尾切片——会被静态尾部误判到底,导致 EU 等底部卡片没滚到) const src = await driver.getSource(); const names = (src.match(/XCUIElementTypeCell[^>]*?name="([^"]*)"/g) || []).join('|'); if (names && names === lastNames) break; lastNames = names; await driver.scrollDown(500); await sleep(800); } return null; } // 34 设备首页里 plug 常在第 7~8 屏(实测 Plug EU 第7屏)→ 原 8 次固定滚动够不着 → "找不到卡片"。 // 改:滚到**找到 or 到底为止**(最多 16 屏)。到底检测用**可见卡片名集合**(滚动时真实变化; // 不能用 source 末尾切片——RN 首页末尾是静态 footer,滚动时不变 → 会误判到底提前停)。 let rect: { x: number; y: number; w: number } | null = null; let lastNames = ''; for (let i = 0; i < 16 && !rect; i++) { const src = await driver.getSource(); rect = findCardRect(src); if (rect) break; const names = src.split('<').filter((n) => /nameText"/.test(n)).map((n) => (n.match(/text="([^"]*)"/) || [])[1] || '').join('|'); if (names && names === lastNames) break; // 可见卡片集不再变 = 到底 lastNames = names; await driver.scrollDown(600); await sleep(700); } if (!rect) return null; // 开关钮 y-95 若落进顶栏(~y248)会误触 +/... → 卡片太靠顶时下滑一屏让它下移再读 if (rect.y - 95 < 300) { await driver.scrollDown(400); await sleep(800); const r2 = findCardRect(await driver.getSource()); if (r2) rect = r2; } const state = readPlugState(await driver.getSource(), rect.x, rect.y); return { state, btnX: rect.x + rect.w - 48, btnY: rect.y - 95, nameX: rect.x, nameY: rect.y }; } function readPlugState(src: string, nameX: number, nameY: number): 'On' | 'Off' | 'unknown' { for (const n of src.split('<')) { const t = (n.match(/text="(On|Off)"/) || [])[1] as 'On' | 'Off' | undefined; const b = n.match(/bounds="\[(\d+),(\d+)\]/); if (t && b && Math.abs(+b[1] - nameX) < 200 && +b[2] > nameY && +b[2] < nameY + 150) return t; } return 'unknown'; } async function controlTo(target: 'On' | 'Off'): Promise<'On' | 'Off' | 'unknown'> { const info = await plugInfo(); if (!info) throw new Error(`首页找不到${deviceName}卡片`); if (info.state === 'unknown') throw new Error(`读不到${deviceName}开关状态`); if (info.state === target) return target; await driver.tap(info.btnX, info.btnY); // 点开关钮 let st = info.state; for (let i = 0; i < 8 && st !== target; i++) { await sleep(2000); st = (await plugInfo())?.state ?? 'unknown'; } return st; } it(`${CTRL_PLUG} 关闭开关`, { timeout: 90000 }, async () => { const start = Date.now(); try { const before = (await plugInfo())?.state; expect(before).not.toBe('unknown'); const after = await controlTo('Off'); console.log(`关闭: ${before} → ${after}`); expect(after).toBe('Off'); // 硬断言:已关 reporter.record(`${CTRL_PLUG} 关闭开关`, 'PASS', Date.now() - start, `状态=${after}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${CTRL_PLUG} 关闭开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it(`${CTRL_PLUG} 打开开关`, { timeout: 90000 }, async () => { const start = Date.now(); try { const before = (await plugInfo())?.state; expect(before).not.toBe('unknown'); const after = await controlTo('On'); console.log(`打开: ${before} → ${after}`); expect(after).toBe('On'); // 硬断言:已开 reporter.record(`${CTRL_PLUG} 打开开关`, 'PASS', Date.now() - start, `状态=${after}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${CTRL_PLUG} 打开开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); });