AI_UIAutomation/tests/bot/bot_card.test.ts

605 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest';
import { DeviceDriver } from '../../drivers/types';
import { createDriver } from '../../drivers/factory';
import { BotHelper } from '../../utils/bot-helper';
import { BOT_LOCATORS } from '../../locators/bot-locators';
import { TestReporter } from '../../utils/test-reporter';
import { getDeviceName } from '../../config/device.config';
import { sleep } from '../../utils/common';
import { applyProtoNetwork } from '../../utils/common';
import * as dotenv from 'dotenv';
import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('bot', 'BOT_DEVICE');
// 必测项控制步骤锚点(协议相关,由 PROTO 环境变量切换): BLE→15975#6e4uZSVe / WiFi→15974#Qt4hcgB4
// 该 step「点击控制Bot 不加密开&不加密关&加密按压」由 ON/OFF切换 + 加密按压 两个用例共同覆盖,回写时按 step 聚合
const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble';
const CTRL = PROTO === 'wifi' ? '[P0][ONES:15974#Qt4hcgB4][wifi]' : '[P0][ONES:15975#6e4uZSVe][ble]';
describe('Bot Card - 首页卡片操作', () => {
let driver: DeviceDriver;
let bot: BotHelper;
let reporter: TestReporter;
beforeAll(async () => {
driver = createDriver();
await driver.createSession();
bot = new BotHelper(driver);
reporter = new TestReporter('Bot_Card', driver.platform.toUpperCase());
// 双协议前置:按 PROTO 切手机蓝牙/WiFi(ble→开蓝牙关WiFi / wifi→关蓝牙开WiFi),无人值守自动切
await applyProtoNetwork(driver, PROTO);
});
beforeEach(async () => {
await driver.dismissPopupIfPresent();
await driver.goBackToHomepage();
await sleep(500);
await driver.dismissPopupIfPresent();
});
afterAll(async () => {
reporter.generate();
await driver.destroySession();
});
async function findBot(): Promise<string> {
let botId = await driver.findDeviceCard(deviceName);
if (!botId) {
await driver.scrollDown(250);
await sleep(1000);
botId = await driver.findDeviceCard(deviceName);
}
if (!botId) throw new Error(`找不到${deviceName}卡片`);
return botId;
}
async function captureScreenshot(): Promise<string | undefined> {
try { return await driver.screenshot(); } catch { return undefined; }
}
async function tapBotAndWaitPopup(): Promise<void> {
const botId = await findBot();
await driver.tapElement(botId);
await sleep(1500);
if (driver.platform === 'android') return;
const check = await driver.findElement(BOT_LOCATORS.settingsButton);
if (!check) {
const botId2 = await driver.findDeviceCard(deviceName);
if (botId2) {
await driver.tapElement(botId2);
await sleep(1500);
}
}
}
async function navigateToModeSettings(): Promise<void> {
await tapBotAndWaitPopup();
if (driver.platform === 'ios') {
const settingsId = await driver.findElement(BOT_LOCATORS.settingsButton);
if (!settingsId) throw new Error('Settings弹窗未出现');
await driver.tapElement(settingsId);
await sleep(2000);
}
const modeId = await driver.findElement(BOT_LOCATORS.modeItem);
if (!modeId) throw new Error('Mode菜单未找到');
await driver.tapElement(modeId);
await sleep(2000);
await dismissPasswordDialogIfPresent();
}
async function getCurrentMode(): Promise<'Press' | 'Switch' | 'Unknown'> {
const source = await driver.getSource();
if (source.includes('In Switch Mode')) return 'Switch';
if (source.includes('In Press Mode')) return 'Press';
if (source.includes('Switch Mode')) return 'Switch';
if (source.includes('Press Mode')) return 'Press';
return 'Unknown';
}
async function dismissPasswordDialogIfPresent(): Promise<void> {
const source = await driver.getSource();
if (source.includes('Enter password') || source.includes('Enter Password')) {
const fields = await driver.findElementsRaw('class name', driver.platform === 'android' ? 'android.widget.EditText' : 'XCUIElementTypeSecureTextField');
if (fields.length > 0) {
await driver.typeText(fields[0], '1234');
await sleep(500);
const okEl = await driver.findElementRaw('name', 'OK') || await driver.findElementRaw('name', 'Confirm');
if (okEl) await driver.tapElement(okEl);
await sleep(2000);
}
}
}
async function switchToMode(targetMode: 'Press Mode' | 'Switch Mode'): Promise<boolean> {
if (driver.platform === 'android') {
// Tap the mode selector container to open mode dialog
const sivMode = await driver.findElementRaw('-android uiautomator',
'new UiSelector().resourceId("com.theswitchbot.switchbot:id/sivMode")');
if (!sivMode) return false;
await driver.tapElement(sivMode);
await sleep(1500);
const targetId = await driver.findElementRaw('name', targetMode);
if (!targetId) {
const cancelId = await driver.findElementRaw('name', 'Cancel');
if (cancelId) await driver.tapElement(cancelId);
return false;
}
await driver.tapElement(targetId);
await sleep(1500);
const confirmId = await driver.findElementRaw('name', 'Confirm') || await driver.findElementRaw('name', 'OK');
if (confirmId) {
await driver.tapElement(confirmId);
await sleep(2000);
}
return true;
}
const modeLabel = await driver.findElementRaw('predicate string', 'name == "Mode" AND type == "XCUIElementTypeStaticText" AND visible == true');
if (!modeLabel) return false;
const modeRect = await driver.getElementRect(modeLabel);
await driver.tap(modeRect.x + modeRect.width + 50, modeRect.y + modeRect.height / 2);
await sleep(1500);
const targetId = await driver.findElementRaw('predicate string', `name == "${targetMode}" AND visible == true`);
if (!targetId) {
const cancelId = await driver.findElementRaw('name', 'Cancel');
if (cancelId) await driver.tapElement(cancelId);
return false;
}
await driver.tapElement(targetId);
await sleep(1500);
const confirmId = await driver.findElementRaw('predicate string', 'name == "Confirm" OR name == "OK"');
if (confirmId) {
await driver.tapElement(confirmId);
await sleep(2000);
}
return true;
}
it(`首页找到${deviceName}卡片`, async () => {
const start = Date.now();
try {
const botId = await findBot();
const rect = await driver.getElementRect(botId);
const detail = `位置: (${rect.x}, ${rect.y}) 尺寸: ${rect.width}x${rect.height}`;
console.log(`${deviceName} ${detail}`);
expect(rect.width).toBeGreaterThan(0);
expect(rect.height).toBeGreaterThan(0);
reporter.record(`首页找到${deviceName}卡片`, 'PASS', Date.now() - start, detail);
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record(`首页找到${deviceName}卡片`, 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('点击Bot卡片 → 弹出菜单包含Settings', async () => {
const start = Date.now();
try {
await tapBotAndWaitPopup();
if (driver.platform === 'android') {
const modeId = await driver.findElement(BOT_LOCATORS.modeItem);
const passcodeId = await driver.findElement(BOT_LOCATORS.passcodeItem);
const settingsText = await driver.findElementRaw('name', 'Settings');
const detail = `Settings页: Settings=${!!settingsText}, Mode=${!!modeId}, Passcode=${!!passcodeId}`;
console.log(`弹窗按钮: ${detail}`);
expect(settingsText || modeId).not.toBeNull();
reporter.record('弹出菜单验证', 'PASS', Date.now() - start, detail);
} else {
const settingsId = await driver.findElement(BOT_LOCATORS.settingsButton);
const onId = await driver.findElement(BOT_LOCATORS.onButton);
const offId = await driver.findElement(BOT_LOCATORS.offButton);
const pressBtn = await driver.findElementRaw('name', 'Press');
const pressOnce = await driver.findElementRaw('predicate string', 'name CONTAINS "Press"');
const detail = `Settings=${!!settingsId}, ON=${!!onId}, OFF=${!!offId}, Press=${!!pressBtn || !!pressOnce}`;
console.log(`弹窗按钮: ${detail}`);
expect(settingsId).not.toBeNull();
reporter.record('弹出菜单验证', 'PASS', Date.now() - start, detail);
}
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('弹出菜单验证', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('切换设备模式: Switch → Press → Switch', async () => {
const start = Date.now();
try {
await navigateToModeSettings();
const modeBefore = await getCurrentMode();
console.log('切换前模式:', modeBefore);
const targetMode = modeBefore === 'Switch' ? 'Press Mode' : 'Switch Mode';
const switched1 = await switchToMode(targetMode as any);
expect(switched1).toBe(true);
await sleep(1000);
const modeAfter = await getCurrentMode();
console.log('切换后模式:', modeAfter);
expect(modeAfter).not.toBe(modeBefore);
// Switch back to original
const restoreMode = modeBefore === 'Switch' ? 'Switch Mode' : 'Press Mode';
await switchToMode(restoreMode as any);
await sleep(1000);
const modeFinal = await getCurrentMode();
console.log('还原模式:', modeFinal);
const detail = `${modeBefore}${modeAfter}${modeFinal}`;
reporter.record('切换设备模式', 'PASS', Date.now() - start, detail);
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('切换设备模式', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('Switch模式 - ON/OFF切换', async () => {
const start = Date.now();
try {
// Ensure device is in Switch Mode before attempting ON/OFF
let statusBefore = await bot.getBotStatus();
console.log('初始状态:', statusBefore);
if (statusBefore === 'unknown') {
console.log('设备可能非Switch Mode先切换到Switch Mode...');
await navigateToModeSettings();
const currentMode = await getCurrentMode();
console.log('当前模式:', currentMode);
if (currentMode !== 'Switch') {
const switched = await switchToMode('Switch Mode');
console.log('切换到Switch Mode:', switched);
await sleep(2000);
}
// Go back to homepage and re-check status
await driver.goBackToHomepage();
await sleep(2000);
statusBefore = await bot.getBotStatus();
console.log('切换后状态:', statusBefore);
if (statusBefore === 'unknown') {
reporter.record(`${CTRL} 不加密开/关`, 'SKIP', Date.now() - start, '切换Switch Mode后仍无法识别状态');
return;
}
}
await tapBotAndWaitPopup();
if (statusBefore === 'off') {
const onId = await driver.findElement(BOT_LOCATORS.onButton);
expect(onId).not.toBeNull();
await driver.tapElement(onId!);
} else {
const offId = await driver.findElement(BOT_LOCATORS.offButton);
expect(offId).not.toBeNull();
await driver.tapElement(offId!);
}
// Wait for BLE command to complete, then go back to homepage to read updated card status
await sleep(5000);
await driver.goBackToHomepage();
await sleep(2000);
const statusAfter = await bot.getBotStatus();
console.log('操作后:', statusAfter);
expect(statusAfter).not.toBe('unknown');
expect(statusAfter).not.toBe(statusBefore);
const detail = `${statusBefore}${statusAfter}`;
reporter.record(`${CTRL} 不加密开/关`, 'PASS', Date.now() - start, detail);
// Restore state
await tapBotAndWaitPopup();
if (statusBefore === 'off') {
const offId = await driver.findElement(BOT_LOCATORS.offButton);
if (offId) await driver.tapElement(offId);
} else {
const onId = await driver.findElement(BOT_LOCATORS.onButton);
if (onId) await driver.tapElement(onId);
}
await sleep(5000);
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record(`${CTRL} 不加密开/关`, 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('设置页 - 验证所有菜单项', async () => {
const start = Date.now();
try {
await tapBotAndWaitPopup();
const settingsId = await driver.findElement(BOT_LOCATORS.settingsButton);
expect(settingsId).not.toBeNull();
await driver.tapElement(settingsId!);
await sleep(2000);
const items = ['modeItem', 'passcodeItem', 'schedulesItem', 'logsItem', 'nfcItem'] as const;
const found: string[] = [];
const missing: string[] = [];
for (const key of items) {
const id = await driver.findElement(BOT_LOCATORS[key]);
if (id) found.push(BOT_LOCATORS[key].name);
else missing.push(BOT_LOCATORS[key].name);
}
await driver.scrollDown(400);
await sleep(1000);
const bottomItems = ['firmwareItem', 'deviceInfoItem', 'deleteItem'] as const;
for (const key of bottomItems) {
const id = await driver.findElement(BOT_LOCATORS[key]);
if (id) found.push(BOT_LOCATORS[key].name);
else missing.push(BOT_LOCATORS[key].name);
}
const detail = `找到 ${found.length}/${found.length + missing.length}`;
console.log(`${detail}: ${found.join(', ')}`);
expect(found.length).toBeGreaterThanOrEqual(6);
reporter.record('设置页菜单项验证', 'PASS', Date.now() - start, detail);
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('设置页菜单项验证', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
// ==================== 加密相关 ====================
async function enterSettings(): Promise<void> {
await tapBotAndWaitPopup();
if (driver.platform === 'ios') {
const settingsId = await driver.findElement(BOT_LOCATORS.settingsButton);
if (!settingsId) throw new Error('Settings按钮未找到');
await driver.tapElement(settingsId);
await sleep(1500);
}
}
async function scrollToAndTap(name: string): Promise<boolean> {
if (driver.platform === 'android') {
const el = await driver.findElementRaw('-android uiautomator',
`new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("${name}"))`);
if (!el) return false;
await driver.tapElement(el);
return true;
}
let el = await driver.findElementRaw('name', name);
if (el) {
const rect = await driver.getElementRect(el);
if (rect.y > 750) {
await driver.scrollDown(300);
await sleep(500);
el = await driver.findElementRaw('name', name);
}
} else {
await driver.scrollDown(300);
await sleep(500);
el = await driver.findElementRaw('name', name);
}
if (!el) return false;
const rect = await driver.getElementRect(el);
await driver.tap(rect.x + rect.width / 2, rect.y + rect.height / 2);
return true;
}
it('设置加密密码', async () => {
const start = Date.now();
try {
await enterSettings();
const tapped = await scrollToAndTap('Passcode');
expect(tapped).toBe(true);
await sleep(2000);
// On Android, may need to dismiss existing password dialog first
await dismissPasswordDialogIfPresent();
// Check if password already set (Delete visible) — if so, delete first
let deleteEl = await driver.findElementRaw('name', 'Delete');
if (deleteEl) {
await driver.tapElement(deleteEl);
await sleep(1500);
const okEl = await driver.findElementRaw('name', 'OK') || await driver.findElementRaw('name', 'Confirm') || await driver.findElementRaw('name', 'Done');
if (okEl) await driver.tapElement(okEl);
await sleep(2000);
}
if (driver.platform === 'android') {
// Android: tap passwordItemView to open set password dialog
const pwdItem = await driver.findElementRaw('-android uiautomator',
'new UiSelector().resourceId("com.theswitchbot.switchbot:id/passwordItemView")');
if (pwdItem) {
await driver.tapElement(pwdItem);
await sleep(2000);
}
} else {
// iOS: tap "Set Password" or "Set Passcode"
const setEl = await driver.findElementRaw('name', 'Set Password') || await driver.findElementRaw('name', 'Set Passcode');
if (setEl) {
await driver.tapElement(setEl);
await sleep(2000);
}
}
// Input password via text fields
const editFields = await driver.findElementsRaw('class name', driver.platform === 'android' ? 'android.widget.EditText' : 'XCUIElementTypeTextField');
const secureFields = driver.platform === 'ios' ? await driver.findElementsRaw('class name', 'XCUIElementTypeSecureTextField') : [];
const fields = editFields.length > 0 ? editFields : secureFields;
if (fields.length >= 2) {
await driver.typeText(fields[0], '1234');
await sleep(500);
await driver.typeText(fields[1], '1234');
await sleep(500);
} else if (fields.length === 1) {
await driver.typeText(fields[0], '1234');
await sleep(500);
}
// Tap OK/Confirm
const confirmEl = await driver.findElementRaw('name', 'OK') || await driver.findElementRaw('name', 'Confirm');
if (confirmEl) {
await driver.tapElement(confirmEl);
await sleep(3000);
}
// Verify password is set
let source = await driver.getSource();
const isSet = source.includes('Delete') || source.includes('Change Password') || source.includes('****') || !source.includes('Not set');
console.log('密码设置结果:', isSet ? '已设置' : '未设置');
expect(isSet).toBe(true);
reporter.record('设置加密密码', 'PASS', Date.now() - start, `密码1234设置=${isSet}`);
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('设置加密密码', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('加密Bot-切换为Press模式', async () => {
const start = Date.now();
try {
await sleep(1000);
const botId = await findBot();
let source = await driver.getSource();
if (source.includes('Press Mode')) {
console.log('当前已是Press模式');
reporter.record('加密-切换Press', 'PASS', Date.now() - start, '当前已是Press Mode');
return;
}
await driver.tapElement(botId);
await sleep(1000);
const settingsId = await driver.findElement(BOT_LOCATORS.settingsButton);
if (!settingsId) throw new Error('Settings按钮未找到');
await driver.tapElement(settingsId);
await sleep(1500);
const modeEl = await driver.findElementRaw('name', 'Mode');
expect(modeEl).not.toBeNull();
await driver.tapElement(modeEl!);
await sleep(2000);
const switched = await switchToMode('Press Mode');
expect(switched).toBe(true);
await sleep(3000);
source = await driver.getSource();
const isPress = source.includes('Press Mode');
console.log('加密后模式:', isPress ? 'Press Mode' : 'other');
reporter.record('加密-切换Press', 'PASS', Date.now() - start, `Press Mode=${isPress}`);
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('加密-切换Press', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('加密Bot-按压操作', async () => {
const start = Date.now();
try {
const botId = await findBot();
const rect = await driver.getElementRect(botId);
// Tap button area on card
await driver.tap(rect.x + rect.width - 30, rect.y + rect.height / 2);
await sleep(2000);
// Check if passcode input appears
let source = await driver.getSource();
if (source.includes('Passcode') || source.includes('Password') || source.includes('Enter')) {
// Try text field input
const textFields = await driver.findElementsRaw('class name', 'XCUIElementTypeTextField');
const secureFields = await driver.findElementsRaw('class name', 'XCUIElementTypeSecureTextField');
const field = textFields[0] || secureFields[0];
if (field) {
await driver.typeText(field, '1234');
await sleep(1000);
const confirmEl = await driver.findElementRaw('name', 'Confirm') || await driver.findElementRaw('name', 'OK');
if (confirmEl) await driver.tapElement(confirmEl);
}
await sleep(5000);
console.log('输入密码1234后执行按压');
} else {
await sleep(8000);
console.log('直接执行按压(无密码弹窗)');
}
source = await driver.getSource();
const stillPress = source.includes('Press Mode');
console.log('按压后状态:', stillPress ? 'Press Mode' : '已执行');
reporter.record(`${CTRL} 加密按压`, 'PASS', Date.now() - start, `加密按压完成, Press=${stillPress}`);
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record(`${CTRL} 加密按压`, 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
it('删除加密密码', async () => {
const start = Date.now();
try {
await enterSettings();
const tapped = await scrollToAndTap('Passcode');
expect(tapped).toBe(true);
await sleep(2000);
// On Android, entering Passcode page may require password
await dismissPasswordDialogIfPresent();
await sleep(1000);
// Check if password is set (Delete/Clear button visible)
let deleteEl = await driver.findElementRaw('name', 'Delete');
if (!deleteEl && driver.platform === 'android') {
deleteEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Clear")');
if (!deleteEl) {
await driver.scrollDown(200);
await sleep(500);
deleteEl = await driver.findElementRaw('name', 'Delete');
}
}
if (deleteEl) {
await driver.tapElement(deleteEl);
await sleep(2000);
// Confirmation dialog
const confirmButtons = ['OK', 'Confirm', 'Delete', 'Done'];
for (const btn of confirmButtons) {
const el = await driver.findElementRaw('name', btn);
if (el) { await driver.tapElement(el); await sleep(3000); break; }
}
} else {
console.log('Delete按钮未找到密码可能未设置');
}
await sleep(1000);
const source = await driver.getSource();
const isDeleted = source.includes('Not set') || source.includes('Set Password') ||
source.includes('Set Passcode') || source.includes('Passcode') ||
!source.includes('Delete');
console.log('密码删除:', isDeleted ? '成功' : '未确认');
expect(isDeleted).toBe(true);
reporter.record('删除加密密码', 'PASS', Date.now() - start, `密码删除, Not set=${isDeleted}`);
} catch (e: any) {
const ss = await captureScreenshot();
reporter.record('删除加密密码', 'FAIL', Date.now() - start, e.message, ss);
throw e;
}
});
});