158 lines
7.5 KiB
TypeScript
158 lines
7.5 KiB
TypeScript
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;
|
|
}
|
|
});
|
|
});
|