287 lines
10 KiB
TypeScript
287 lines
10 KiB
TypeScript
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<void> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
// 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;
|
|
}
|
|
});
|
|
});
|