157 lines
8.5 KiB
TypeScript
157 lines
8.5 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') });
|
|
|
|
// 百叶帘功能页控制(在首页弹窗 blind_tilt_control 之外补充):点卡 → 弹窗点 "More" 进完整功能页 → Close Down/Fully open + 校验。
|
|
// 功能页(实测):顶部状态栏 "Fully open"/"Fully closed"/"X% Opened up|down";Customized Actions 行三按钮
|
|
// Close Down | Fully open | Close Up(图标在文字上方)。注意 "Fully open" 出现两次(顶部状态 + 中间按钮)。
|
|
const deviceName = process.env.BT_DEVICE || 'Blind Tilt 42';
|
|
const BT_MODEL = process.env.BT_MODEL || 'BlindTilt 3C';
|
|
const CTRL = onesCtrl('curtain', BT_MODEL) || '[P0]';
|
|
|
|
describe('Blind Tilt 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('BlindTilt_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 的匹配,避开按钮行的 "Fully open")。
|
|
// 状态词表(实测):Fully open / Closed down / Closed up / Fully closed / "X% Opened up|down"。
|
|
// 注意区分状态 "Closed down"(有 d、小写 down)与按钮 "Close Down"(无 d、大写 Down)。
|
|
function readStatus(src: string): string {
|
|
const pat = /^(Fully open|Fully closed|Closed up|Closed down|[0-9]{1,3}% (?:Opened|Closed)[^"<]*)$/;
|
|
if (isIOS()) {
|
|
// iOS:状态是顶部独立 StaticText(如 "Closed down",**不带 deviceName 前缀**);按钮 "Close Down/Fully open/Close Up" 是 Other 元素。
|
|
// 只匹配 **StaticText** 的状态词 → 自动排除同名 Other 按钮(如 "Fully open" 按钮)。
|
|
const m = src.match(/<XCUIElementTypeStaticText[^>]*?(?:name|label)="(Fully open|Fully closed|Closed up|Closed down|[0-9]{1,3}% (?:Opened|Closed)[^"]*)"/);
|
|
return m ? m[1].trim() : 'unknown';
|
|
}
|
|
// Android:扫所有 text="<状态>" 带 bounds,取最顶部(min y)那个=状态栏
|
|
let best: { y: number; s: string } | null = null;
|
|
for (const m of src.matchAll(/text="([^"]*)"[^>]*bounds="\[\d+,(\d+)\]\[\d+,\d+\]"/g)) {
|
|
if (!pat.test(m[1])) continue;
|
|
const y = +m[2];
|
|
if (!best || y < best.y) best = { y, s: m[1] };
|
|
}
|
|
return best ? best.s : 'unknown';
|
|
}
|
|
|
|
// 点功能页按钮(Close Down/Fully open/Close Up)。
|
|
// Android:真正可点的是**含该文字的 clickable 容器**(直接点文字/图标坐标命中不可点子元素,不触发)→
|
|
// 用 clickable(true).childSelector(text) 选容器 tapElement;兜底点文字上方图标。
|
|
// iOS:按钮 "Close Down/Fully open/Close Up" 是 **Other 元素(name=标签)**,直接 **tapElement** 即生效(坐标 tap 不 actuate);
|
|
// "Fully open" 可能既是按钮(Other)又是状态(StaticText)→ 取 y 最大(按钮行)那个 tapElement。
|
|
async function tapBtn(label: string): Promise<boolean> {
|
|
if (!isIOS()) {
|
|
const cont = await driver.findElementRaw('-android uiautomator',
|
|
`new UiSelector().clickable(true).childSelector(new UiSelector().text("${label}"))`).catch(() => null);
|
|
if (cont) { await driver.tapElement(cont); return true; }
|
|
// 兜底:取按钮行(y 最大)文字,点其上方图标
|
|
const els = await driver.findElementsRaw('-android uiautomator', `new UiSelector().text("${label}")`).catch(() => [] as string[]);
|
|
let t: { cx: number; y: number } | null = null;
|
|
for (const el of els) { const r = await driver.getElementRect(el).catch(() => null); if (r && r.width > 0 && (!t || r.y > t.y)) t = { cx: Math.round(r.x + r.width / 2), y: r.y }; }
|
|
if (!t) return false;
|
|
await driver.tap(t.cx, Math.round(t.y - 100));
|
|
return true;
|
|
}
|
|
// iOS:按钮是 Other/Button 元素(name=标签),tapElement 即生效(实测 Closed down→Fully open)。
|
|
// 只选 Other/Button 类型 → 排除同名内层 StaticText(取最大y会误选不可点的内层,导致 no-op)。
|
|
const e = await driver.findElementRaw('predicate string',
|
|
`(type == "XCUIElementTypeButton" OR type == "XCUIElementTypeOther") AND (name == "${label}" OR label == "${label}")`).catch(() => null);
|
|
if (!e) return false;
|
|
await driver.tapElement(e);
|
|
return true;
|
|
}
|
|
|
|
async function tapBtnAndWait(label: string, pred: (s: string) => boolean): Promise<string> {
|
|
if (!(await tapBtn(label))) throw new Error(`功能页找不到按钮 ${label}`);
|
|
await sleep(1500);
|
|
let st = readStatus(await driver.getSource());
|
|
for (let i = 0; i < 20 && !pred(st); i++) { await sleep(2000); st = readStatus(await driver.getSource()); }
|
|
return st;
|
|
}
|
|
|
|
// 进百叶帘功能页:点卡(弹快捷窗)→ 点 "More" 进功能页 →(点掉 Got it)→ 等 Close Down 按钮出现
|
|
async function enterFeature(): Promise<boolean> {
|
|
const card = await findHomeCard(driver, deviceName);
|
|
if (!card) { console.log(`[BTFeat] 首页找不到 ${deviceName}`); return false; }
|
|
await driver.tapElement(card);
|
|
await sleep(1800);
|
|
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('[BTFeat] 点 More 进功能页'); break; }
|
|
await sleep(800);
|
|
}
|
|
for (let i = 0; i < 3; i++) {
|
|
const g = isIOS()
|
|
? await driver.findElementRaw('predicate string', 'name == "Got it" OR label == "Got it"').catch(() => null)
|
|
: await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")').catch(() => null);
|
|
if (!g) break; await driver.tapElement(g); await sleep(1200);
|
|
}
|
|
for (let i = 0; i < 8; i++) {
|
|
const cd = isIOS()
|
|
? await driver.findElementRaw('predicate string', 'name == "Close Down" OR label == "Close Down"').catch(() => null)
|
|
: await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Close Down")').catch(() => null);
|
|
if (cd) return true;
|
|
await sleep(1000);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
it(`${CTRL} 功能页控制${deviceName}(开+关Down)`, { timeout: 240000 }, 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;
|
|
}
|
|
console.log(`[BTFeat] 初始状态: ${readStatus(await driver.getSource())}`);
|
|
|
|
// 完全打开 → 等 "Fully open"(状态栏)
|
|
const sOpen = await tapBtnAndWait('Fully open', s => /fully open/i.test(s));
|
|
console.log(`[BTFeat] Fully open 后: ${sOpen}`);
|
|
expect(/fully open/i.test(sOpen)).toBe(true);
|
|
|
|
// 向下关闭 → 等 "Closed down"/"Fully closed"(状态栏)
|
|
const sClose = await tapBtnAndWait('Close Down', s => /closed/i.test(s));
|
|
console.log(`[BTFeat] Close Down 后: ${sClose}`);
|
|
expect(/closed/i.test(sClose)).toBe(true);
|
|
|
|
reporter.record(`${CTRL} 功能页控制${deviceName}`, 'PASS', Date.now() - start, `功能页开/关均生效(开→${sOpen} 关→${sClose})`);
|
|
} catch (e: any) {
|
|
const ss = await driver.screenshot().catch(() => '');
|
|
reporter.record(`${CTRL} 功能页控制${deviceName}`, 'FAIL', Date.now() - start, e.message, ss);
|
|
throw e;
|
|
}
|
|
});
|
|
});
|