183 lines
9.4 KiB
TypeScript
183 lines
9.4 KiB
TypeScript
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
|
||
});
|