AI_UIAutomation/tests/ceiling_light/light_feature_control.test.ts

133 lines
7.0 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, setupResilientHooks, tapHomeCard, findHomeCard, onesCtrlByCard } from '../../utils/common';
import * as dotenv from 'dotenv';
import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
// 通用灯功能页控制(在首页卡片控制 eave_light_control 之外补充):点卡 → 弹浮层(读状态)→ 点 "More" 进功能页 → 底部圆形电源键 开/关。
// ★ 提速:功能页 on/off **无任何可读信号**(电源键 selected/enabled/亮度% 切换前后都不变)→ 状态只能靠弹窗读。
// 故全程**只 findHomeCard 2 次**:开弹窗读初态(顺势从该弹窗进 More)→ 功能页点电源 → 再开弹窗读终态。不反复回首页搜卡。
const CARD_KEYWORD = process.env.LIGHT_CARD || 'Color Bulb';
const CTRL = onesCtrlByCard(CARD_KEYWORD) || '[P0]'; // 按卡片名取灯类控制锚点(15975/15974 step);未命中回退 [P0]
describe('Light Feature 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(`LightFeat_${CARD_KEYWORD}`, 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);
});
// 收尾:把灯切回关闭态(hookTimeout 给足 90s)
afterAll(async () => {
try {
if ((await popupState()) === 'On') { await moreToFeature(); await tapPower(); console.log('[LightFeat] 收尾关灯'); }
await driver.dismissPopupIfPresent();
} catch (e: any) { console.log('[LightFeat] 收尾关灯失败:', e.message); }
reporter.generate();
await driver.destroySession();
}, 90000);
function boundsOf(src: string, label: string): { cx: number; cy: number } | null {
const esc = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const m = src.match(new RegExp(`(?:text|content-desc|name|label)="${esc}"[^>]*bounds="\\[(\\d+),(\\d+)\\]\\[(\\d+),(\\d+)\\]"`));
return m ? { cx: Math.round((+m[1] + +m[3]) / 2), cy: Math.round((+m[2] + +m[4]) / 2) } : null;
}
// 【1 次 findHomeCard】回首页 → 点卡开快捷弹窗 → 读状态("On | NN% ..."/"Off"),**弹窗保持打开**(供随后点 More)。
async function popupState(): Promise<'On' | 'Off' | 'unknown'> {
await driver.dismissPopupIfPresent();
await driver.goBackToHomepage(); await sleep(800);
try {
await tapHomeCard(driver, CARD_KEYWORD, { resetPosition: 'top', logOnFail: true });
} catch {
return 'unknown';
}
await sleep(1200);
const src = await driver.getSource();
if (/(?:text|name|label)="On ?\|/.test(src)) return 'On';
if (/(?:text|name|label)="Off"/.test(src)) return 'Off';
return 'unknown';
}
// 从**已打开的弹窗**点 More 进功能页(关 Attention/OK,灯未加载点 Reconnect),等功能页就绪。不再 findHomeCard。
// 就绪检测放宽:各灯功能页内容不同(color bulb=White/Color tabs;Ceiling Pro=场景/Brightness;融蜡灯=仅 Brightness)→
// 只要出现 Brightness/Schedules/Delay/%/Favorites/Color Library/White 任一即视为进了功能页。
async function moreToFeature(): Promise<boolean> {
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); }
for (let i = 0; i < 12; i++) {
const src = await driver.getSource().catch(() => '');
// 灯未正确加载会显示 "Reconnect" → 点它重连
const rc = boundsOf(src, 'Reconnect');
if (rc) { await driver.tap(rc.cx, rc.cy); await sleep(2500); console.log('[LightFeat] 点 Reconnect 重连'); continue; }
const ok = boundsOf(src, 'OK');
if (ok) { await driver.tap(ok.cx, ok.cy); await sleep(1000); continue; }
if (boundsOf(src, 'White') || /COLOR|Favorites|Color Library|Brightness|Schedules|Delay|\d+%/.test(src)) return true;
await sleep(800);
}
return false;
}
// 点底部圆形电源键:动态找底部居中可点元素(cy>H*0.85),兜底 (W*0.5, H*0.935)
async function tapPower(): Promise<void> {
const src = await driver.getSource().catch(() => '');
let best: { cx: number; cy: number } | null = null;
for (const m of src.matchAll(/clickable="true"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g)) {
const cx = (+m[1] + +m[3]) / 2, cy = (+m[2] + +m[4]) / 2, w = +m[3] - +m[1];
if (cy > H * 0.85 && cy < H * 0.99 && cx > W * 0.35 && cx < W * 0.65 && w < W * 0.5 && (!best || cy > best.cy)) best = { cx: Math.round(cx), cy: Math.round(cy) };
}
const p = best || { cx: Math.round(W * 0.5), cy: Math.round(H * 0.935) };
await driver.tap(p.cx, p.cy);
await sleep(2500);
}
it(`${CTRL} 功能页控制${CARD_KEYWORD}(开/关)`, { timeout: 120000 }, async () => {
const start = Date.now();
try {
// 搜索#1:开弹窗读初态(弹窗保持打开)
const s0 = await popupState();
if (s0 === 'unknown') {
// 首页有该灯却读不到状态/开不了弹窗 = 未正确执行 → FAIL(交给 catch);首页确无该灯(未添加/本平台无实体)→ 合法 SKIP。
await driver.goBackToHomepage().catch(() => {}); await sleep(1200);
if (await findHomeCard(driver, CARD_KEYWORD).catch(() => null)) throw new Error('功能页控制失败(首页有该灯但读不到状态/进不去,未正确执行)');
reporter.record(`${CTRL} 功能页控制${CARD_KEYWORD}`, 'SKIP', Date.now() - start, '首页无该灯(未添加/本平台无实体),跳过'); return;
}
console.log(`[LightFeat] ${CARD_KEYWORD} 初始: ${s0}`);
// 从该弹窗进功能页 → 点电源键(切到相反态)
if (!(await moreToFeature())) throw new Error('从弹窗进功能页失败');
await tapPower();
// 搜索#2:再开弹窗读终态
const s1 = await popupState();
const target = s0 === 'On' ? 'Off' : 'On';
console.log(`[LightFeat] 功能页电源后: ${s1}(目标${target})`);
expect(s1).toBe(target);
reporter.record(`${CTRL} 功能页控制${CARD_KEYWORD}`, 'PASS', Date.now() - start, `功能页电源切换生效(${s0}${s1})`);
} catch (e: any) {
const ss = await driver.screenshot().catch(() => '');
reporter.record(`${CTRL} 功能页控制${CARD_KEYWORD}`, 'FAIL', Date.now() - start, e.message, ss); throw e;
}
});
});