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 { 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 { try { return await driver.screenshot(); } catch { return undefined; } } async function tapBotAndWaitPopup(): Promise { 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 { 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 { 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 { 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 { 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 { 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; } }); });