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, navigateToTimerPage, addTimer, deleteAllTimers, enterDeviceSettings, scrollToAndTap, waitForSource, findHomeCard, } from '../../utils/common'; import { getDeviceName } from '../../config/device.config'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('curtain', 'CURTAIN_DEVICE'); // 真实卡名(注册表,如 "Curtain 4D") // 设备维度动态控制锚点:锚点按型号解析(设备名≠型号)。base Curtain=Curtain 1A(step3) import { onesCtrl } from '../../utils/common'; import { setupResilientHooks } from "../../utils/common"; const CURTAIN_MODEL = process.env.CURTAIN_MODEL || deviceName; // 默认取卡名(Curtain3 Auto/2025 Auto…)→ 各变体对应各自 step;不再恒 Curtain 1A const CTRL_CURTAIN = onesCtrl('curtain', CURTAIN_MODEL) || '[P0]'; describe('Curtain Control - 窗帘控制功能', () => { let driver: DeviceDriver; let reporter: TestReporter; setupResilientHooks(() => driver); beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter(`Curtain_Control_${deviceName}`, driver.platform.toUpperCase()); }); beforeEach(async () => { await driver.dismissPopupIfPresent(); await driver.goBackToHomepage(); await sleep(500); await driver.dismissPopupIfPresent(); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); // 点首页卡片 → 快捷弹窗(Open/Pause/Close + 状态)。 // 统一用共享 findHomeCard 定位卡片(两端一致:iOS 谓词 / Android uiautomator + 原生滚到可见兜底), // 再按平台点卡开快捷弹窗:iOS 点卡中心,Android 点卡名处(卡片可点区)。 async function openCurtainPopup(): Promise { await driver.goBackToHomepage(); await sleep(800); await driver.dismissPopupIfPresent(); const card = await findHomeCard(driver, deviceName); if (!card) throw new Error(`找不到${deviceName}卡片`); const rect = await driver.getElementRect(card).catch(() => null); if (!rect) throw new Error(`找不到${deviceName}卡片`); if (driver.platform === 'ios') { await driver.tap(Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2)); } else { await driver.tap(rect.x + Math.min(rect.width - 10, 60), rect.y); // 点卡名处(卡片可点区)开快捷弹窗 } // 等弹窗控件渲染完再返回:**首个用例**(新建 session 冷启动)首弹窗渲染慢,原死等 2500ms 不够 → // 紧接着查 "Open" 拿到 null(打开窗帘稳定失败)。改为轮询等控件(Open/Pause/Close/More)出现。 for (let k = 0; k < 10; k++) { await sleep(600); const s = await driver.getSource(); if (/\bPause\b|\bMore\b/.test(s)) break; // 弹窗控件行已渲染 } } const enterCurtainControl = openCurtainPopup; // 延迟开关/光线感应 等辅助步骤复用(经"More"进功能页找控件) // 读窗帘状态:Fully open / Fully closed / "X% Opened..." 等 function readCurtainState(src: string): string { if (driver.platform === 'ios') { // iOS:状态在 cell 名"<设备名><状态>"(如 "Curtain AutoFully closed")→ 取设备名后的状态。 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):iOS 是文字标签+**上方约 50px 圆形图标**(点图标才生效),Android 是文字按钮。 async function tapCurtainBtn(label: string): Promise { if (driver.platform === 'ios') { const lbls = await driver.findElementsRaw('predicate string', `name == "${label}" OR label == "${label}"`).catch(() => [] as string[]); let target: { cx: number; y: number } | null = null; for (const l of lbls) { const r = await driver.getElementRect(l).catch(() => null); if (r && r.width > 0 && (!target || r.y > target.y)) target = { cx: Math.round(r.x + r.width / 2), y: r.y }; } if (!target) return false; await driver.tap(target.cx, Math.round(target.y - 50)); return true; } const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${label}")`).catch(() => null); if (!el) return false; await driver.tapElement(el); return true; } // 点按钮后轮询等状态满足 pred(最多 ~24s,窗帘物理移动慢) async function waitCurtainState(pred: (s: string) => boolean): Promise { let st = readCurtainState(await driver.getSource()); for (let i = 0; i < 12 && !pred(st); i++) { await sleep(2000); st = readCurtainState(await driver.getSource()); } return st; } it('打开窗帘', { timeout: 90000 }, async () => { const start = Date.now(); try { await openCurtainPopup(); expect(await tapCurtainBtn('Open')).toBe(true); const st = await waitCurtainState(s => /open/i.test(s)); // 等到 "Fully open" console.log('打开后状态:', st); expect(/open/i.test(st)).toBe(true); // 硬断言:已打开 expect(st).not.toBe('Fully closed'); reporter.record(`${CTRL_CURTAIN} 打开窗帘`, 'PASS', Date.now() - start, `状态=${st}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${CTRL_CURTAIN} 打开窗帘`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('关闭窗帘', { timeout: 90000 }, async () => { const start = Date.now(); try { await openCurtainPopup(); expect(await tapCurtainBtn('Close')).toBe(true); const st = await waitCurtainState(s => s === 'Fully closed'); // 等到 "Fully closed" console.log('关闭后状态:', st); expect(st).toBe('Fully closed'); // 硬断言:已关闭 reporter.record(`${CTRL_CURTAIN} 关闭窗帘`, 'PASS', Date.now() - start, `状态=${st}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${CTRL_CURTAIN} 关闭窗帘`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); it('暂停窗帘', { timeout: 90000 }, async () => { const start = Date.now(); try { // Pause 非 ONES P0 step(step3 仅开/关);此处验证 Pause 控制可达:移动中点 Pause。 await openCurtainPopup(); // 启动移动(Open)后尽快点 Pause(裸窗帘行程快,缩短间隔) await tapCurtainBtn('Open'); await sleep(800); expect(await tapCurtainBtn('Pause')).toBe(true); // 硬断言:Pause 控制可达并点击 await sleep(2500); const st = readCurtainState(await driver.getSource()); console.log('暂停后状态:', st); reporter.record('暂停窗帘', 'PASS', Date.now() - start, `Pause 控制可达并执行, 状态=${st}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record('暂停窗帘', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); });