AI_UIAutomation/tests/air_purifier/air_purifier_card.test.ts

183 lines
9.4 KiB
TypeScript
Raw 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, afterEach, expect } from 'vitest';
import { DeviceDriver } from '../../drivers/types';
import { createDriver } from '../../drivers/factory';
import { TestReporter } from '../../utils/test-reporter';
import { sleep, waitForSource, tapHomeCard, tapHomeCardTopRight, getDeviceStatus, FindHomeCardOptions } from '../../utils/common';
import * as dotenv from 'dotenv';
import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
// Air Purifier(口头"空净",WiFi):控制必测=首页卡片快捷控制。流程同加湿器2(Evaporative Humidifier)。
// 开/关:直接点首页卡片右上角开关钮(通用方法 tapHomeCardTopRight,定位卡容器内 controlBtn,无需开浮层)。
// 切模式:点卡片弹浮层(顶部状态字 + 无文案电源圆键 + 模式行 Level/Auto/Pet/Sleep + More),模式键有文字按 text 定位;切模式需先开机。
// 状态读取:首页卡副标题镜像设备态(关=Off/待机;开=模式名),用通用入口 getDeviceStatus 两端统一读取+结构化。ONES 控制号待补,先 [P0] 占位。
const CARD_KEYWORD = process.env.AIR_PURIFIER_CARD || 'Air Purifier';
const MODES = ['Level', 'Auto', 'Pet', 'Sleep']; // 实测浮层模式集合
// 读状态轻量 options:不回顶/不重复清弹框/不重点 Home tab —— 供轮询与"浮层开着读背景卡态"用(默认 options 会回顶并清掉控制浮层)。
const LIGHT_READ: FindHomeCardOptions = { resetPosition: 'none', ensureHome: false, dismissPopups: false };
const ADD_ANCHOR = '[P0]'; // it() 标题显示用;回写锚点由 rec()/apNames 双发(美规+日规)
// 空净不分美规/日规:step 29 美规 7qxA1ajE / 30 日规 R5PDp8BR(均 15975 BLE)都写同一结果;wifi 轮无对应 step → [P0]。
const AP_BLE_STEPS = ['7qxA1ajE', 'R5PDp8BR'];
const apNames = (base: string): string[] =>
process.env.PROTO === 'wifi' ? [`[P0] ${base}`] : AP_BLE_STEPS.map(u => `[P0][ONES:15975#${u}][ble] ${base}`);
describe('AirPurifier Card - 首页卡片控制(Air Purifier)', () => {
let driver: DeviceDriver;
let reporter: TestReporter;
let W = 1080, H = 2400;
const isIOS = () => driver.platform === 'ios';
const rec = (base: string, status: 'PASS' | 'FAIL' | 'SKIP', dur: number, detail: string, ss?: string) => {
for (const n of apNames(base)) reporter.record(n, status, dur, detail, ss);
};
beforeAll(async () => {
driver = createDriver();
await driver.createSession();
reporter = new TestReporter('AirPurifier_Card', driver.platform.toUpperCase());
try { const s = await driver.getWindowSize(); if (s?.width) { W = s.width; H = s.height; } } catch { /* 默认 1080x2400 */ }
});
// iOS 浮层是底部 sheet(android back 关不掉),需点顶部暗区收起。但**无脑点会误触首页卡**——
// 每条 it 开头根本没浮层,(W/2,H*0.12) 正落在首页顶部卡片上 → 误进设备页。
// 改为**条件收起**:仅当 findPowerBtnIOS 检测到浮层电源键(=浮层确实开着)时才点暗区,否则跳过。
async function dismissSheetIfOpenIOS(): Promise<void> {
if (!isIOS()) return;
const p = await findPowerBtnIOS().catch(() => null);
if (!p) return; // 没浮层 → 不点,根除首页误触
await driver.tap(Math.round(W * 0.5), Math.round(H * 0.12)).catch(() => {});
await sleep(600);
}
beforeEach(async () => {
await dismissSheetIfOpenIOS();
await driver.dismissPopupIfPresent();
await driver.goBackToHomepage();
await driver.dismissPopupIfPresent();
});
afterEach(async () => {
await dismissSheetIfOpenIOS();
await driver.dismissPopupIfPresent().catch(() => {});
});
afterAll(async () => {
reporter.generate();
await driver.destroySession();
});
// 点卡片 → 弹快捷控制浮层(未弹回首页重试)。用通用 tapHomeCard(findHomeCard 回顶+robust 滚动定位 +
// settleCardRect 等列表停稳 + tapElement),替代手搓 getRect+tap 中心:首页设备多/列表惯性滚动时手搓坐标会点偏 → 浮层不弹/误进功能页。
async function openCardPopup(): Promise<void> {
for (let attempt = 0; attempt < 3; attempt++) {
await tapHomeCard(driver, CARD_KEYWORD);
if (await waitForSource(driver, 'More', 5000)) return;
console.log(`未弹快捷浮层,回首页重试(第 ${attempt + 1}/3)`);
await driver.dismissPopupIfPresent().catch(() => {});
await driver.goBackToHomepage(); await sleep(800);
}
throw new Error('卡片快捷控制浮层未弹出');
}
// 动态找电源键(iOS):浮层里**最大的居中方键**(实测 72×72,正中;上方有 48px 干扰键需排除)。
async function findPowerBtnIOS(): Promise<{ cx: number; cy: number } | null> {
const btns = await driver.findElementsRaw('predicate string', 'type == "XCUIElementTypeButton"').catch(() => [] as string[]);
const cands: { cx: number; cy: number; w: number }[] = [];
for (const b of btns as string[]) {
const r = await driver.getElementRect(b).catch(() => null);
if (!r) continue;
if (r.width >= 45 && r.width <= 120 && Math.abs(r.width - r.height) < 24 && Math.abs(r.x + r.width / 2 - W / 2) < W * 0.08 && r.y > H * 0.4 && r.y < H * 0.78) {
cands.push({ cx: Math.round(r.x + r.width / 2), cy: Math.round(r.y + r.height / 2), w: r.width });
}
}
if (!cands.length) return null;
cands.sort((a, b) => b.w - a.w || a.cy - b.cy); // 取最大(电源键 72 > 控制键 48);同宽取最靠上
return { cx: cands[0].cx, cy: cands[0].cy };
}
async function tapPower(): Promise<void> {
if (isIOS()) {
const p = await findPowerBtnIOS();
const px = p ? p.cx : Math.round(W / 2);
const py = p ? p.cy : Math.round(H * 0.52);
console.log(`点电源键(iOS) ${px},${py}${p ? '' : ' [回退]'}`);
return driver.tap(px, py);
}
const px = Math.round(W / 2), py = Math.round(H * 0.52);
console.log(`点电源键 (${px}, ${py})`);
await driver.tap(px, py);
}
// 当前是否关机:通用入口 getDeviceStatus(定位卡片→读副标题→结构化)。关=power off / 离线 / 待机;开=模式名。
// 轮询或浮层开着读背景卡态时传 LIGHT_READ,避免回顶滚动/清掉控制浮层。
async function isOff(opts: FindHomeCardOptions = {}): Promise<boolean> {
const st = await getDeviceStatus(driver, CARD_KEYWORD, opts);
console.log(`状态字="${st.raw}" power=${st.power} online=${st.online}`);
return st.power === 'off' || !st.online || /待机/i.test(st.raw);
}
async function pollState(wantOff: boolean, ms = 15000): Promise<boolean> {
const deadline = Date.now() + ms;
while (Date.now() < deadline) {
if ((await isOff(LIGHT_READ)) === wantOff) return true;
await sleep(1500);
}
return (await isOff(LIGHT_READ)) === wantOff;
}
it(`${ADD_ANCHOR} 首页卡片开机`, async () => {
const start = Date.now();
try {
// isOff() 默认 options 会滚动定位卡片;找不到卡片时下面的 tapHomeCardTopRight 会抛"找不到卡片"。
if (!(await isOff())) { await tapHomeCardTopRight(driver, CARD_KEYWORD); await pollState(true); }
await tapHomeCardTopRight(driver, CARD_KEYWORD);
const on = await pollState(false);
console.log(`开机后 isOff=${!on}`);
expect(on).toBe(true);
rec(`首页卡片开机`, 'PASS', Date.now() - start, '首页卡片开关钮开机成功(状态→运行)');
} catch (e: any) {
const ss = await driver.screenshot().catch(() => '');
rec(`首页卡片开机`, 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it(`${ADD_ANCHOR} 首页卡片切换模式(Level/Auto/Pet/Sleep)`, async () => {
const start = Date.now();
try {
await openCardPopup();
// 切模式前需先开机(关机时模式键置灰)。浮层已开,读状态用 LIGHT_READ 避免清掉浮层/回顶。
if (await isOff(LIGHT_READ)) { await tapPower(); await pollState(false); }
const switched: string[] = [];
for (const m of MODES) {
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${m}")`).catch(() => null);
if (el) { await driver.tapElement(el); await sleep(2000); switched.push(m); }
else console.log(`未找到模式键: ${m}`);
}
console.log(`已切换模式: ${switched.join('/')}`);
expect(switched.length).toBe(MODES.length);
rec(`首页卡片切换模式`, 'PASS', Date.now() - start, `依次切换 ${switched.join('/')} 成功`);
} catch (e: any) {
const ss = await driver.screenshot().catch(() => '');
rec(`首页卡片切换模式`, 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it(`${ADD_ANCHOR} 首页卡片关机`, async () => {
const start = Date.now();
try {
if (await isOff()) { await tapHomeCardTopRight(driver, CARD_KEYWORD); await pollState(false); }
await tapHomeCardTopRight(driver, CARD_KEYWORD);
const off = await pollState(true);
console.log(`关机后 isOff=${off}`);
expect(off).toBe(true);
rec(`首页卡片关机`, 'PASS', Date.now() - start, '首页卡片开关钮关机成功(状态→Off)');
} catch (e: any) {
const ss = await driver.screenshot().catch(() => '');
rec(`首页卡片关机`, 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
}, 90000); // 关机轮询链路偶发超 60s(状态回写延迟);在全局 60s 基础上 +30s
});