AI_UIAutomation/tests/curtain/curtain_feature_control.tes...

236 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, parseHomeCardStatus } from '../../utils/common';
import * as dotenv from 'dotenv';
import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
// Curtain 功能页控制(在首页弹窗控制 curtain_control 之外补充):点卡 → 弹窗点 "More" 进完整功能页 → Open/Close + 校验状态。
// 功能页(实测):Open/Pause/Close 文字按钮 + 位置"X%"/"Fully open/closed" + Slide 滑条 + Light Sensing/Delay/Schedules。
const deviceName = process.env.CURTAIN_DEVICE || 'Curtain Auto';
const CTRL = onesCtrl('curtain', deviceName) || '[P0]'; // 按卡名(Curtain Auto/Curtain3 Auto/Curtain3 2025 Auto)取对应 step,不再硬编码 Curtain 1A
describe('Curtain 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('Curtain_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();
});
// 读窗帘状态:Fully open / Fully closed / "X% Opened..."(复用 curtain_control 逻辑)
function readState(src: string): string {
if (isIOS()) {
const m = src.match(new RegExp(`"${deviceName}(Fully open|Fully closed|[0-9]{1,3}% [^"]*)"`));
return m ? m[1].trim() : 'unknown';
}
const m = src.match(/text="(Fully open|Fully closed|[0-9]{1,3}% [^"]*)"/);
return m ? m[1] : 'unknown';
}
// 点功能页 Open/Close/Pause:文字只是 label,真正可点的圆形图标在文字**正上方**(iOS ~50px / Android ~100px)。
// 实测 Curtain3 1080×2400:Open 文字 top=1914,其上图标 ViewGroup [*,1727][*,1888] 中心 ~1807 → 文字 top100 命中。
async function tapBtn(label: string): Promise<boolean> {
if (isIOS()) {
// iOS:Open/Pause/Close 是文字标签(StaticText),上方有圆形图标 Button。**坐标 tap 不 actuate**(实测点了不动)→
// 找标签 cx,再找其**上方**(y 更小)cx 匹配的图标 Button,**tapElement** 它(实测能驱动:Slide 0%→100%)。
const lbls = await driver.findElementsRaw('predicate string', `name == "${label}" OR label == "${label}"`).catch(() => [] as string[]);
let lcx = -1, ly = -1;
for (const l of lbls) { const r = await driver.getElementRect(l).catch(() => null); if (r && r.width > 0 && r.y > ly) { lcx = r.x + r.width / 2; ly = r.y; } }
if (lcx < 0) return false;
const btns = await driver.findElementsRaw('predicate string', 'type == "XCUIElementTypeButton"').catch(() => [] as string[]);
let best: string | null = null, bestDx = 999;
for (const b of btns) {
const r = await driver.getElementRect(b).catch(() => null);
if (r && r.y < ly && r.y > ly - 130) { const dx = Math.abs(r.x + r.width / 2 - lcx); if (dx < 40 && dx < bestDx) { bestDx = dx; best = b; } }
}
if (best) { await driver.tapElement(best); return true; }
await driver.tap(Math.round(lcx), Math.round(ly - 50)); // 兜底
return true;
}
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${label}")`).catch(() => null);
if (!el) return false;
const r = await driver.getElementRect(el).catch(() => null);
if (!r) { await driver.tapElement(el); return true; }
await driver.tap(Math.round(r.x + r.width / 2), Math.round(r.y - 100)); // 点文字正上方图标
return true;
}
// 进窗帘功能页:点卡(弹快捷窗)→ 点 "More" 进完整功能页 →(点掉 Got it 引导)→ 等 Open 按钮出现
async function enterCurtainFeature(): Promise<boolean> {
const card = await findHomeCard(driver, deviceName);
if (!card) { console.log(`[CurtainFeat] 首页找不到 ${deviceName}`); return false; }
await driver.tapElement(card);
await sleep(1800);
// 弹窗里点 More 进功能页
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('[CurtainFeat] 点 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);
}
// 等功能页 Open 按钮出现
for (let i = 0; i < 6; i++) {
const open = isIOS()
? await driver.findElementRaw('predicate string', 'name == "Open" OR label == "Open"').catch(() => null)
: await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Open")').catch(() => null);
if (open) return true;
await sleep(1000);
}
return false;
}
// 直接读**功能页**实时位置(不回首页,省导航):"Fully open"/"Fully closed"/"Partially open XX%"。
// 返回开合百分比(高=开 低=关);Fully open=100 Fully closed=0;读不到=-1。
function readFeaturePct(src: string): number {
const m = src.match(/(?:text|name|label|value)="(Fully open|Fully closed|Partially open \d+%)[^"]*"/);
if (!m) return -1;
if (/Fully open/.test(m[1])) return 100;
if (/Fully closed/.test(m[1])) return 0;
const p = m[1].match(/(\d+)%/);
return p ? +p[1] : -1;
}
// 点按钮后原地轮询功能页位置,**检测到方向移动即返回**(开→%增大 / 关→%减小,不要求到端点)。
// 窗帘运动较慢且单次点击偶发不触发 → 中途(~20s)若仍无位移,补点一次。
async function actuateAndWait(label: string, baseline: number, dir: 'up' | 'down'): Promise<number> {
await tapBtn(label); await sleep(1500);
let pct = readFeaturePct(await driver.getSource());
for (let i = 0; i < 28; i++) {
if (dir === 'up' && pct > baseline) return pct;
if (dir === 'down' && pct >= 0 && pct < baseline) return pct;
if (i === 8 && pct === baseline) { await tapBtn(label); await sleep(800); } // 没动→补点一次
await sleep(2500);
pct = readFeaturePct(await driver.getSource());
}
return pct;
}
// 非 Curtain3:功能页无实时 % → 回首页读卡片 desText 文字(Fully open/closed/Partially open)校验。
async function verifyViaCard(pred: (s: string) => boolean): Promise<string> {
await driver.dismissPopupIfPresent();
await driver.goBackToHomepage(); await sleep(1200);
await findHomeCard(driver, deviceName);
let st = parseHomeCardStatus(await driver.getSource(), deviceName, isIOS());
for (let i = 0; i < 16 && !pred(st); i++) {
await sleep(2500);
await findHomeCard(driver, deviceName);
st = parseHomeCardStatus(await driver.getSource(), deviceName, isIOS());
}
return st;
}
// 仅 Curtain3 / Curtain3 2025 功能页有实时位置 % → 原地校验方向移动;其他窗帘回首页读卡片状态。
const isC3 = /Curtain3/i.test(deviceName);
it(`${CTRL} 功能页控制${deviceName}(开+关)`, { timeout: 240000 }, async () => {
const start = Date.now();
try {
if (!(await enterCurtainFeature())) {
// 首页有该窗帘却进不去功能页 = 未正确执行 → 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;
}
if (isC3) {
// Curtain3/2025:功能页实时 %,有方向移动即通过。
// 实测 Curtain3 Open 方向从近关位不响应(设备/校准问题)→ 按指示**跳过开方向断言**,
// 仅硬断言 Close 方向(% 下降或已关到底);Open 做 best-effort(动了记录,不动不判失败)。
const p0 = readFeaturePct(await driver.getSource());
console.log(`[CurtainFeat] 初始: ${p0}%`);
// Open:best-effort,不断言
await tapBtn('Open');
const pOpen = p0 >= 99 ? p0 : await actuateAndWait('Open', p0, 'up');
const openMoved = pOpen > p0;
console.log(`[CurtainFeat] Open 后: ${pOpen}%(${openMoved ? '开方向生效' : '开方向未响应,跳过'})`);
// Close:硬断言——% 下降,或已关到底(≤8%)
const base = Math.max(pOpen, p0);
expect(await tapBtn('Close')).toBe(true);
const pClose = base <= 8 ? base : await actuateAndWait('Close', base, 'down'); // 已近关位则不空轮询
console.log(`[CurtainFeat] Close 后: ${pClose}%(关前${base}%)`);
expect(pClose >= 0 && (pClose < base || pClose <= 8)).toBe(true); // 关:下降 或 已到底
reporter.record(`${CTRL} 功能页控制${deviceName}`, 'PASS', Date.now() - start,
`Close方向生效(${base}%→${pClose}%);开方向${openMoved ? `生效(→${pOpen}%)` : '设备不响应已跳过'}`);
} else if (isIOS()) {
// iOS 普通窗帘:功能页有 "Slide to X%"(0=全开,100=全关)实时位置 → **原地校验**(不回首页/不重进,窗帘慢也能等)。
// 坐标 tap 不 actuate,tapBtn 已改为 tapElement(图标 Button)。
const slidePct = (src: string): number => {
if (/Fully open/.test(src)) { /* 端点标签同时存在,以 Slide to 为准 */ }
const m = src.match(/"Slide to (\d+)%"/);
if (m) return +m[1];
if (/"Fully closed"/.test(src)) return 100;
if (/"Fully open"/.test(src)) return 0;
return -1;
};
const waitSlide = async (pred: (p: number) => boolean): Promise<number> => {
let p = slidePct(await driver.getSource());
for (let i = 0; i < 28 && !pred(p); i++) { await sleep(2500); p = slidePct(await driver.getSource()); }
return p;
};
expect(await tapBtn('Open')).toBe(true);
const pOpen = await waitSlide(p => p >= 0 && p <= 10); // 开 → Slide 到 ~0%
console.log(`[CurtainFeat] Open 后(Slide): ${pOpen}%`);
expect(pOpen >= 0 && pOpen <= 10).toBe(true);
expect(await tapBtn('Close')).toBe(true);
const pClose = await waitSlide(p => p >= 90); // 关 → Slide 到 ~100%
console.log(`[CurtainFeat] Close 后(Slide): ${pClose}%`);
expect(pClose >= 90).toBe(true);
reporter.record(`${CTRL} 功能页控制${deviceName}`, 'PASS', Date.now() - start, `功能页开/关均生效(开→Slide ${pOpen}% 关→Slide ${pClose}%)`);
} else {
// Android 其他窗帘:功能页打开 → 回首页读卡片;重进功能页关闭 → 回首页读卡片
expect(await tapBtn('Open')).toBe(true);
await sleep(2000);
const sOpen = await verifyViaCard(s => /fully open/i.test(s));
console.log(`[CurtainFeat] Open 后(卡片): ${sOpen}`);
expect(/fully open/i.test(sOpen)).toBe(true);
if (!(await enterCurtainFeature())) throw new Error('关闭前重进功能页失败');
expect(await tapBtn('Close')).toBe(true);
await sleep(2000);
const sClose = await verifyViaCard(s => /fully closed/i.test(s));
console.log(`[CurtainFeat] Close 后(卡片): ${sClose}`);
expect(/fully 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;
}
});
});