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, } from '../../utils/common'; 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'; const WIFI_SSID = process.env.WIFI_SSID || 'SwitchBot_Office'; const WIFI_PASSWORD = process.env.WIFI_PASSWORD || ''; 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(); }); async function inputWiFiCredentials(driver: DeviceDriver): Promise { await sleep(3000); const source = await driver.getSource(); if (!source.includes('Wi-Fi') && !source.includes('WiFi') && !source.includes('SSID') && !source.includes('Network')) { return; } if (driver.platform === 'ios') { const ssidField = await driver.findElementRaw('predicate string', 'type == "XCUIElementTypeTextField" AND (value CONTAINS "SSID" OR value CONTAINS "Wi-Fi" OR value CONTAINS "Network" OR value == "")'); if (ssidField) { await driver.tapElement(ssidField); await sleep(500); await driver.clearText(ssidField); await driver.typeText(ssidField, WIFI_SSID); await sleep(500); } const pwdField = await driver.findElementRaw('predicate string', 'type == "XCUIElementTypeSecureTextField" OR (type == "XCUIElementTypeTextField" AND value CONTAINS "Password")'); if (pwdField) { await driver.tapElement(pwdField); await sleep(500); await driver.clearText(pwdField); await driver.typeText(pwdField, WIFI_PASSWORD); await sleep(500); } } else { const ssidField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(0)'); if (ssidField) { await driver.tapElement(ssidField); await sleep(300); await driver.clearText(ssidField); await driver.typeText(ssidField, WIFI_SSID); await sleep(500); } const pwdField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(1)'); if (pwdField) { await driver.tapElement(pwdField); await sleep(300); await driver.clearText(pwdField); await driver.typeText(pwdField, WIFI_PASSWORD); await sleep(500); } } const nextBtnNames = ['Next', 'Connect', 'Join', 'OK', 'Confirm']; for (const btn of nextBtnNames) { let el: string | null = null; if (driver.platform === 'ios') { el = await driver.findElementRaw('name', btn); } else { el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${btn}")`); } if (el) { await driver.tapElement(el); await sleep(5000); break; } } } function getAIHubAddOptions() { return { categoryName: 'AI Hub', deviceKeyword: AIHUB_NAME, scanTimeout: 30000, skipScanStep: true, skipNextButton: true, connectionKeywords: [ 'Initial Setup', 'Start Using', 'Done', 'added successfully', 'Got it', 'cloud service', 'Pick a room', 'Display Type', 'Wi-Fi', 'WiFi', 'SSID', 'Network', 'Configure Wi-Fi', ], preSelectSteps: async (d: DeviceDriver) => { const el = await d.findElementRaw('name', 'Connect Device'); if (el) { await d.clickElement(el); await sleep(2000); } const agreeEl = await d.findElementRaw('name', 'Agree'); if (agreeEl) { await d.clickElement(agreeEl); await sleep(3000); } }, postConnectionSteps: async (d: DeviceDriver) => { await inputWiFiCredentials(d); }, }; } it('通过BLE添加AI Hub 6C设备', { timeout: 180000 }, async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, AIHUB_NAME); if (alreadyExists) { console.log(`${AIHUB_NAME}已在首页,跳过重新添加`); reporter.record(`添加${AIHUB_NAME}`, 'PASS', 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 = driver.platform === 'android' ? { x: 999, y: 175 } : { x: 361, y: 70 }; const BACK_BTN = driver.platform === 'android' ? { 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; } }); });