import { describe, it, beforeAll, afterAll, expect } from 'vitest'; import { DeviceDriver } from '../../drivers/types'; import { createDriver } from '../../drivers/factory'; import { TestReporter } from '../../utils/test-reporter'; import { sleep, addDeviceViaBLE, isDeviceOnHomepage, enterPairingMode, configureHubWifi, } from '../../utils/common'; import { getWifiCredentials } from '../../config/wifi.config'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const AIHUB_NAME = process.env.AIHUB_NAME || 'AI Hub 6C'; describe('AIHub Connect - 添加AI Hub设备', () => { let driver: DeviceDriver; let reporter: TestReporter; beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter('AIHub_Connect', driver.platform.toUpperCase()); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); function getAIHubAddOptions() { const wifi = getWifiCredentials(); return { categoryName: 'AI Hub', deviceKeyword: 'AI Hub', scanTimeout: 30000, skipScanStep: true, skipNextButton: true, wizardButtons: ['Start now', 'Return Home', 'Done', 'Use now', 'Start Using', 'Got it', 'OK', 'Skip', 'Next', 'Save'], connectionKeywords: [ 'Initial Setup', 'Start Using', 'Done', 'added successfully', 'Added successfully', 'Got it', 'cloud service', 'Pick a room', 'Display Type', 'Select a room', 'Wi-Fi', 'WiFi', 'SSID', 'Network', 'Configure Wi-Fi', 'Connecting', 'Start now', ], preSelectSteps: async (d: DeviceDriver) => { // 选品类后可能有 Agree/免责页,先过掉 const agreeEl = await d.findElementRaw('name', 'Agree'); if (agreeEl) { await d.clickElement(agreeEl); await sleep(2000); } // 继电器 ch43 控制供电 Plug 的开/关状态,**短按一下即可**(holdMs 400,结束释放不保持)。 // AIHUB_NO_RELAY=1:设备已手动进配对,跳过继电器。 if (!process.env.AIHUB_NO_RELAY) { await enterPairingMode('aihub').catch((e) => console.log(`relay ch43 短按失败: ${e.message}`)); console.log('AI Hub: 已短按继电器(切 Plug),等待设备启动...'); await sleep(5000); } else { console.log('AI Hub: 跳过继电器(设备已手动进配对)'); } // 点 "Connect Device"(链接设备)开始搜索 for (const t of ['Connect device', 'Connect Device', 'Connect devices', 'Connect Devices']) { const b = await d.findElementRaw('-android uiautomator', `new UiSelector().text("${t}")`).catch(() => null); if (b) { console.log(`AI Hub: 点击 "${t}" 开始搜索`); await d.clickElement(b); await sleep(2000); break; } } // 点连接设备后会弹隐私协议 → 同意(可能先勾选框再点 Agree) await sleep(1500); { const cb = await d.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.CheckBox")').catch(() => null); if (cb) { await d.clickElement(cb).catch(() => {}); await sleep(500); } for (const t of ['Agree', 'Agree and Continue', 'I Agree', 'Accept', 'Continue', '同意', '同意并继续']) { const a = await d.findElementRaw('-android uiautomator', `new UiSelector().text("${t}")`).catch(() => null) || await d.findElementRaw('-android uiautomator', `new UiSelector().textContains("${t}")`).catch(() => null); if (a) { console.log(`AI Hub: 同意隐私协议 "${t}"`); await d.clickElement(a); await sleep(2500); break; } } } // 等待发现设备 → 进配网(上电后约 1 分钟才出现,最长等 ~100s) console.log('AI Hub: 等待发现设备...'); for (let i = 0; i < 33; i++) { await sleep(3000); const s = await d.getSource().catch(() => ''); if (/Configure Wi-?Fi|Wi-?Fi Settings|SSID|Network name|Pick a room|Select a room|Setting up|Connecting to your router|Verifying|Name your|added successfully|Start now/i.test(s)) { console.log('AI Hub: 已进入配网/后续页'); break; } } }, postConnectionSteps: async (d: DeviceDriver) => { await configureHubWifi(d, wifi); }, }; } it('通过BLE添加AI Hub 6C设备', { timeout: 360000 }, async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, AIHUB_NAME); if (alreadyExists) { console.log(`${AIHUB_NAME}已在首页,跳过重新添加`); reporter.record(`添加${AIHUB_NAME}`, 'SKIP', Date.now() - start, `${AIHUB_NAME}已存在, 无需重新添加`); return; } const result = await addDeviceViaBLE(driver, getAIHubAddOptions()); expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); reporter.record(`添加${AIHUB_NAME}`, 'PASS', Date.now() - start, `${AIHUB_NAME}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`添加${AIHUB_NAME}`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); const SETTINGS_ICON = (process.env.PLATFORM || 'android').toLowerCase() !== 'ios' ? { x: 999, y: 175 } : { x: 361, y: 70 }; const BACK_BTN = (process.env.PLATFORM || 'android').toLowerCase() !== 'ios' ? { x: 50, y: 88 } : { x: 39, y: 70 }; async function navigateToHubFunctionPage(): Promise { await driver.goBackToHomepage(); await sleep(1000); if (driver.platform === 'android') { const hubEl = await (driver as any).findDeviceCard(AIHUB_NAME); if (!hubEl) return false; const rect = await driver.getElementRect(hubEl); await driver.tap(rect.x + 100, rect.y + 30); await sleep(6000); await driver.dismissPopupIfPresent(); await sleep(1000); const s = await driver.getSource(); if (s.includes('Try OpenClaw') || (s.includes('Cameras') && s.includes('AI Events'))) { return true; } return false; } // iOS path const maxScroll = 5; for (let i = 0; i <= maxScroll; i++) { let hubEl: string | null = null; hubEl = await driver.findElementRaw('predicate string', `name CONTAINS "${AIHUB_NAME}" AND type == "XCUIElementTypeCell"`); if (!hubEl) { hubEl = await driver.findElementRaw('predicate string', `label CONTAINS "${AIHUB_NAME}"`); } if (hubEl) { await driver.clickElement(hubEl); await sleep(5000); const s = await driver.getSource(); if (s.includes('Try OpenClaw') || (s.includes('Cameras') && s.includes('AI Events'))) { return true; } const rect = await driver.getElementRect(hubEl); await driver.tap(rect.x + rect.width / 2, rect.y + rect.height / 2); await sleep(5000); const s2 = await driver.getSource(); if (s2.includes('Try OpenClaw') || (s2.includes('Cameras') && s2.includes('AI Events'))) { return true; } } if (i < maxScroll) { await driver.swipe(195, 650, 195, 300, 0.5); await sleep(1500); } } return false; } async function removeHubFromSettings(): Promise { // Enter Hub function page const entered = await navigateToHubFunctionPage(); if (!entered) return false; // Tap settings icon (top-right) await driver.tap(SETTINGS_ICON.x, SETTINGS_ICON.y); await sleep(3000); // Find and tap Delete (scroll down to find it) let delEl: string | null = null; for (let i = 0; i < 8; i++) { if (driver.platform === 'ios') { delEl = await driver.findElementRaw('predicate string', 'name == "Delete" AND visible == true'); } else { delEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")'); } if (delEl) break; await driver.scrollDown(300); await sleep(800); } if (!delEl) return false; await driver.tapElement(delEl); await sleep(3000); // Confirm deletion - tap OK button in confirmation dialog const confirmNames = ['OK', 'Confirm', 'Delete', 'Yes']; for (const name of confirmNames) { let el: string | null = null; if (driver.platform === 'ios') { el = await driver.findElementRaw('name', name); } else { el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${name}")`); } if (el) { await driver.tapElement(el); await sleep(5000); break; } } // Navigate back to homepage manually (avoid goBackToHomepage misdetecting "Home Assistant" as homepage) for (let i = 0; i < 8; i++) { const s = await driver.getSource(); const isHome = s.includes('Add') && s.includes('More') && s.includes('Home') && !s.includes('Home Assistant') && !s.includes('Device Settings'); if (isHome) break; await driver.tap(BACK_BTN.x, BACK_BTN.y); await sleep(2000); } await sleep(2000); const homeSource = await driver.getSource(); return !homeSource.includes(AIHUB_NAME); } it('删除AI Hub 6C设备并恢复', { timeout: 180000 }, async () => { const start = Date.now(); try { const exists = await isDeviceOnHomepage(driver, AIHUB_NAME); if (!exists) { console.log(`${AIHUB_NAME}不在首页,跳过删除测试`); reporter.record(`删除${AIHUB_NAME}`, 'PASS', Date.now() - start, `${AIHUB_NAME}不存在, 无需删除`); return; } const removed = await removeHubFromSettings(); expect(removed).toBe(true); // Data recovery: re-add the device const reAdded = await addDeviceViaBLE(driver, getAIHubAddOptions()); expect(reAdded).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); reporter.record(`删除${AIHUB_NAME}`, 'PASS', Date.now() - start, `删除并重新添加${AIHUB_NAME}成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`删除${AIHUB_NAME}`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); });