166 lines
9.3 KiB
TypeScript
166 lines
9.3 KiB
TypeScript
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') });
|
|
|
|
// Relay Switch 2PM 控制(双路):点卡**直接进功能页**(无弹窗) → 功能页有 **Switch 1(左) / Switch 2(右)** 两个独立电源圆键 + 各自 "On/Off"。
|
|
// 逐个开关:读状态 → 点电源键 → 校验翻转 → 切回。既覆盖"首页发起控制"(点卡进)也覆盖"功能页控制"。
|
|
// 实测 1080x2400:Switch1 电源键 (W*0.262,H*0.313)、Switch2 (W*0.738,H*0.313);"On/Off" 在电源圈下方 y~909(左 x~110 / 右 x~624)。
|
|
const deviceName = process.env.RELAY_SWITCH_DEVICE || 'Relay Switch 2PM';
|
|
const CTRL = onesCtrl('plug', 'Relay Switch 2PM') || '[P0]'; // 2PM 双路开关控制 → 15975#PG62s725(step42 Relay2pm开关)
|
|
const SWITCHES = [
|
|
{ label: 'Switch 1', px: 0.262, side: 'left' as const },
|
|
{ label: 'Switch 2', px: 0.738, side: 'right' as const },
|
|
];
|
|
|
|
describe('Relay Switch 2PM 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('RelaySwitch2PM_Control', 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);
|
|
await driver.dismissPopupIfPresent();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
reporter.generate();
|
|
await driver.destroySession();
|
|
});
|
|
|
|
// 读某一路开关状态:电源圈下方 y~850-1000 的 "On/Off",按 x 分左(Switch1)/右(Switch2)。
|
|
function readState(src: string, side: 'left' | 'right'): 'On' | 'Off' | 'unknown' {
|
|
if (isIOS()) {
|
|
// iOS 功能页:两个独立电源圈(XCUIElementTypeOther)在页面上方 y~206、w~125,name/label 同时=On/Off。
|
|
// 坑:每个圈的 name 和 label 都命中 On|Off → 全局正则每元素匹配 2 次,按出现顺序取 arr[0]/arr[1] 时
|
|
// "right" 实际取到左圈的 label(而非右圈)→ 右路永远读左路状态 → 点击生效但校验必失败。
|
|
// 正解:按元素几何(中心 x 分左右 + 只认上方电源圈 y<H*0.6)取,和 Android 分支一致。
|
|
for (const m of src.matchAll(/<XCUIElementType\w+\b([^>]*?)\/?>/g)) {
|
|
const a = m[1];
|
|
const st = (a.match(/name="(On|Off)"/) || [])[1] as 'On' | 'Off' | undefined;
|
|
if (!st) continue;
|
|
const x = +((a.match(/\bx="(-?\d+)"/) || [])[1] ?? NaN);
|
|
const w = +((a.match(/\bwidth="(\d+)"/) || [])[1] ?? NaN);
|
|
const y = +((a.match(/\by="(-?\d+)"/) || [])[1] ?? NaN);
|
|
if (!isFinite(x) || !isFinite(w)) continue;
|
|
if (isFinite(y) && y > H * 0.6) continue; // 排除下方列表里的 On/Off,只认上方电源圈
|
|
const isLeft = (x + w / 2) < W / 2;
|
|
if ((side === 'left') === isLeft) return st;
|
|
}
|
|
return 'unknown';
|
|
}
|
|
for (const m of src.matchAll(/text="(On|Off)"[^>]*bounds="\[(\d+),(\d+)\]\[\d+,\d+\]"/g)) {
|
|
const x = +m[2], y = +m[3];
|
|
if (y > 820 && y < 1050) { // 功能页:状态在电源圈下方 y~909(左 Switch1 / 右 Switch2)
|
|
const isLeft = x < W / 2;
|
|
if ((side === 'left') === isLeft) return m[1] as 'On' | 'Off';
|
|
}
|
|
}
|
|
return 'unknown';
|
|
}
|
|
|
|
// 点某一路开关:切换目标是**电源圈(clickable ViewGroup)**,圈心 (W*px, H*0.313)。
|
|
// 坑1:内含 On/Off TextView 是 clickable=false 且在圈**底部**(y~0.39);用 childSelector(On|Off) 会返回该文字元素,tapElement 点到圈底缘 → 圆按钮死角,时中时不中(飘)。
|
|
// 坑2:driver.tap(W3C pointer)对该 RN 圈按钮**不 actuate**(实测点圈心坐标不翻转,但 adb input / 元素 click 能翻转)。
|
|
// 正解:几何筛出电源圈 ViewGroup(clickable + 圈心 y≈H*0.313 + 圈宽 0.22~0.45*W),按 x 选侧,tapElement 点其真实中心(=圈心)。
|
|
async function tapPower(side: 'left' | 'right'): Promise<void> {
|
|
const els = await driver.findElementsRaw('-android uiautomator', 'new UiSelector().clickable(true)').catch(() => [] as string[]);
|
|
const cands: { e: string; cx: number }[] = [];
|
|
for (const e of els) {
|
|
const r = await driver.getElementRect(e).catch(() => null); if (!r) continue;
|
|
const cx = r.x + r.width / 2, cy = r.y + r.height / 2;
|
|
if (Math.abs(cy - H * 0.313) < H * 0.06 && r.width > W * 0.22 && r.width < W * 0.45) cands.push({ e, cx });
|
|
}
|
|
const pick = cands.find(c => (side === 'left') === (c.cx < W / 2));
|
|
if (pick) { await driver.tapElement(pick.e); await sleep(1500); return; }
|
|
console.log(`[RS2PM] tapPower(${side}) 未找到电源圈(候选${cands.length}),走兜底坐标`);
|
|
await driver.tap(Math.round(W * (side === 'left' ? 0.262 : 0.738)), Math.round(H * 0.313)); await sleep(1500); // 兜底圈心坐标(注:W3C tap 可能不 actuate,仅作最后退路)
|
|
}
|
|
|
|
// 翻转到目标态,带漏点重试:tapPower 后短窗口(~5s)内校验;未翻转即判漏点重试(已生效的会在~2.5s内反映,不会误触回弹)。最多 3 次。
|
|
async function toggleTo(side: 'left' | 'right', want: 'On' | 'Off'): Promise<'On' | 'Off' | 'unknown'> {
|
|
for (let a = 0; a < 3; a++) {
|
|
await tapPower(side); // 内含 sleep 1500
|
|
let st = readState(await driver.getSource(), side);
|
|
for (let i = 0; i < 3 && st !== want; i++) { await sleep(1200); st = readState(await driver.getSource(), side); }
|
|
if (st === want) return st;
|
|
console.log(`[RS2PM] ${side} 第${a + 1}次点击未翻转(仍${st}),重试`);
|
|
}
|
|
return readState(await driver.getSource(), side);
|
|
}
|
|
|
|
// 进 2PM 功能页:点卡**弹浮层**(含 Switch1/2 + More)→ 点 "More" 进完整功能页(功能页电源圈在 y~751)。
|
|
// 用功能页特有的 History Data/Schedules 确认已进功能页(浮层没有),否则会停在浮层、tapPower 点到后面的首页卡。
|
|
async function enterFeature(): Promise<boolean> {
|
|
const card = await findHomeCard(driver, deviceName);
|
|
if (!card) { console.log(`[RS2PM] 首页找不到 ${deviceName}`); return false; }
|
|
await driver.tapElement(card);
|
|
await sleep(2000);
|
|
for (let i = 0; i < 8; i++) {
|
|
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); console.log('[RS2PM] 点 More 进功能页'); break; }
|
|
await sleep(700);
|
|
}
|
|
for (let i = 0; i < 8; i++) {
|
|
const src = await driver.getSource();
|
|
if (/History Data|Schedules/.test(src) && /Switch 1/.test(src)) return true; // 功能页特有,区分浮层
|
|
await sleep(1000);
|
|
}
|
|
return /History Data|Schedules/.test(await driver.getSource().catch(() => ''));
|
|
}
|
|
|
|
it(`${CTRL} 功能页控制${deviceName}(双路 开+关)`, { timeout: 180000 }, async () => {
|
|
const start = Date.now();
|
|
try {
|
|
if (!(await enterFeature())) {
|
|
// 首页有该开关却进不去功能页 = 未正确执行 → FAIL(交给 catch);首页确无该开关(未添加/本平台无实体)→ 合法 SKIP。
|
|
await driver.goBackToHomepage().catch(() => {}); await sleep(1200);
|
|
if (await findHomeCard(driver, deviceName).catch(() => null)) throw new Error('进功能页失败(首页有该开关但进不去功能页,未正确执行)');
|
|
reporter.record(`${CTRL} 功能页控制${deviceName}`, 'SKIP', Date.now() - start, '首页无该开关(未添加/本平台无实体),跳过');
|
|
return;
|
|
}
|
|
// 进一次功能页,在同一页依次控制两路开关(各 开→校验→关→校验,覆盖 on/off 两向)。
|
|
const summary: string[] = [];
|
|
for (const sw of SWITCHES) {
|
|
const s0 = readState(await driver.getSource(), sw.side);
|
|
console.log(`[RS2PM] ${sw.label} 初始: ${s0}`);
|
|
|
|
const target1 = s0 === 'On' ? 'Off' : 'On';
|
|
const s1 = await toggleTo(sw.side, target1);
|
|
console.log(`[RS2PM] ${sw.label} 操作1后: ${s1}(目标${target1})`);
|
|
expect(s1, `${sw.label} 操作1 未翻转到 ${target1}`).toBe(target1);
|
|
|
|
const target2 = target1 === 'On' ? 'Off' : 'On';
|
|
const s2 = await toggleTo(sw.side, target2);
|
|
console.log(`[RS2PM] ${sw.label} 操作2后: ${s2}(目标${target2})`);
|
|
expect(s2, `${sw.label} 操作2 未翻转回 ${target2}`).toBe(target2);
|
|
|
|
summary.push(`${sw.label}:${s0}→${s1}→${s2}`);
|
|
}
|
|
reporter.record(`${CTRL} 功能页控制${deviceName}`, 'PASS', Date.now() - start, `双路开/关均生效(${summary.join(' | ')})`);
|
|
} catch (e: any) {
|
|
const ss = await driver.screenshot().catch(() => '');
|
|
reporter.record(`${CTRL} 功能页控制${deviceName}`, 'FAIL', Date.now() - start, e.message, ss);
|
|
throw e;
|
|
}
|
|
});
|
|
});
|