import { DeviceDriver } from '../../drivers/types'; import { sleep, waitForElement, waitForSource, scrollUntilFound } from './element.helper'; import { ensureHomeTab, navigateToAddPage, navigateThroughWizard } from './navigation.helper'; export interface AddDeviceOptions { categoryName: string; deviceKeyword: string; scanTimeout?: number; wizardButtons?: string[]; connectionKeywords?: string[]; preSelectSteps?: (driver: DeviceDriver) => Promise; postConnectionSteps?: (driver: DeviceDriver) => Promise; skipNextButton?: boolean; skipScanStep?: boolean; } const DEFAULT_CONNECTION_KEYWORDS = [ 'Initial Setup', 'Start Using', 'Done', 'added successfully', 'Got it', 'cloud service', 'Pick a room', 'Display Type', ]; const DEFAULT_WIZARD_BUTTONS = ['Use now', 'Start Using', 'Done', 'Got it', 'OK', 'Skip', 'Next']; export async function isDeviceOnHomepage(driver: DeviceDriver, keyword: string): Promise { await ensureHomeTab(driver); let source = await driver.getSource(); if (source.includes(keyword)) return true; await driver.scrollDown(300); await sleep(800); source = await driver.getSource(); return source.includes(keyword); } export async function selectDeviceCategory(driver: DeviceDriver, categoryName: string): Promise { if (driver.platform === 'android') { let el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${categoryName}")`); if (!el) { await driver.scrollDown(300); await sleep(800); el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${categoryName}")`); } if (!el) { console.log(`FAIL: no ${categoryName} option`); return false; } await driver.tapElement(el); await sleep(3000); const src = await driver.getSource(); return src.includes(categoryName) || src.includes('Select') || src.includes('Scanning') || src.includes('searching'); } // iOS - scroll to reveal category, check visibility, then click for (let i = 0; i < 8; i++) { let el = await driver.findElementRaw('predicate string', `name == "${categoryName}" AND type == "XCUIElementTypeStaticText"`); if (!el) { el = await driver.findElementRaw('predicate string', `name == "${categoryName}" AND type == "XCUIElementTypeOther"`); } if (!el) { el = await driver.findElementRaw('name', categoryName); } if (el) { const visible = await driver.getElementAttribute(el, 'visible'); if (String(visible) !== 'true' && String(visible) !== '1') { await driver.scrollDown(400); await sleep(800); continue; } await driver.clickElement(el); await sleep(3000); return true; } await driver.scrollDown(400); await sleep(800); } console.log(`FAIL: no ${categoryName} option`); return false; } export async function waitForDeviceInScan( driver: DeviceDriver, deviceKeyword: string, timeout = 30000 ): Promise { if (driver.platform === 'android') { const start = Date.now(); while (Date.now() - start < timeout) { const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${deviceKeyword}")`); if (el) return el; await sleep(1000); } return null; } // iOS return await waitForElement(driver, 'name', deviceKeyword, timeout); } export async function waitForConnection( driver: DeviceDriver, keywords?: string[], timeout = 30000 ): Promise { const connectionKeywords = keywords || DEFAULT_CONNECTION_KEYWORDS; const start = Date.now(); while (Date.now() - start < timeout) { const source = await driver.getSource(); for (const kw of connectionKeywords) { if (source.includes(kw)) return true; } await sleep(2000); } return false; } export async function addDeviceViaBLE(driver: DeviceDriver, options: AddDeviceOptions): Promise { const { categoryName, deviceKeyword, scanTimeout = 30000, wizardButtons = DEFAULT_WIZARD_BUTTONS, connectionKeywords, preSelectSteps, postConnectionSteps, skipNextButton = false, skipScanStep = false, } = options; // Step 1: Navigate to Add Device page const addDevicePage = await navigateToAddPage(driver, 'Device'); if (!addDevicePage) { console.log('FAIL: cannot navigate to Add Device'); return false; } // Step 2: Select device category const catSelected = await selectDeviceCategory(driver, categoryName); if (!catSelected) { console.log(`FAIL: cannot select category ${categoryName}`); return false; } // Optional: product-specific pre-select steps if (preSelectSteps) await preSelectSteps(driver); // Step 3: Wait for device in BLE scan (skip for auto-connect devices like AI Hub) if (!skipScanStep) { const deviceEl = await waitForDeviceInScan(driver, deviceKeyword, scanTimeout); if (!deviceEl) { console.log(`FAIL: ${deviceKeyword} not found in BLE scan`); return false; } await driver.tapElement(deviceEl); await sleep(500); // Step 4: Tap Next (some products skip this) if (!skipNextButton) { if (driver.platform === 'android') { const nextEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")'); if (nextEl) await driver.tapElement(nextEl); } else { const nextEl = await driver.findElementRaw('name', 'Next'); if (nextEl) await driver.tapElement(nextEl); } await sleep(2000); } } // Step 5: Wait for connection const connected = await waitForConnection(driver, connectionKeywords); if (!connected) { console.log('FAIL: connection timeout'); return false; } // Step 5.5: Post-connection steps (e.g. WiFi setup for cameras) if (postConnectionSteps) await postConnectionSteps(driver); // Step 6: Handle Initial Setup (iOS) if (driver.platform === 'ios') { const source = await driver.getSource(); if (source.includes('Initial Setup')) { const setupNext = await driver.findElementRaw('name', 'Next'); if (setupNext) { await driver.tapElement(setupNext); await sleep(2000); } } } // Step 7: Navigate through wizard await navigateThroughWizard(driver, wizardButtons); await driver.dismissPopupIfPresent(); // Step 8: Verify on homepage await ensureHomeTab(driver); await sleep(2000); for (let i = 0; i < 5; i++) { const source = await driver.getSource(); if (source.includes(deviceKeyword)) return true; await driver.scrollDown(400); await sleep(800); } return false; } export async function removeDeviceFromAccount(driver: DeviceDriver, keyword: string): Promise { await ensureHomeTab(driver); if (driver.platform === 'android') { let botCard = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${keyword}")`); if (!botCard) { await driver.scrollDown(300); await sleep(800); botCard = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${keyword}")`); } if (!botCard) return false; await driver.tapElement(botCard); await sleep(2000); // Scroll to find Delete button let delEl: string | null = null; for (let i = 0; i < 5; i++) { delEl = await driver.findElementRaw('-android uiautomator', 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("Delete"))'); if (delEl) break; await driver.scrollDown(300); await sleep(800); } if (!delEl) { console.log('Delete button not found'); return false; } await driver.tapElement(delEl); await sleep(3000); // Confirmation dialog let confirmed = false; for (let retry = 0; retry < 3; retry++) { const src = await driver.getSource(); if (src.includes('Cancel') && src.includes('deleting this device')) { const confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")'); if (confirmEl) { await driver.tapElement(confirmEl); confirmed = true; break; } } await sleep(1000); } if (!confirmed) { console.log('Delete confirmation not found'); return false; } await sleep(3000); await driver.goBackToHomepage(); await sleep(1000); const homeSource = await driver.getSource(); return !homeSource.includes(keyword); } // iOS let botCard = await driver.findElementRaw('predicate string', `name CONTAINS "${keyword}"`); if (!botCard) { await driver.scrollDown(300); await sleep(800); botCard = await driver.findElementRaw('predicate string', `name CONTAINS "${keyword}"`); } if (!botCard) return false; await driver.tapElement(botCard); await sleep(1500); const settingsEl = await waitForElement(driver, 'name', 'Settings', 5000); if (!settingsEl) return false; await driver.tapElement(settingsEl); await sleep(2000); let delEl: string | null = null; for (let i = 0; i < 3; i++) { delEl = await driver.findElementRaw('predicate string', 'name == "Delete" AND visible == true'); if (delEl) break; await driver.scrollDown(300); await sleep(800); } if (!delEl) { console.log('Delete button not found'); return false; } await driver.tapElement(delEl); await sleep(2000); const okEl = await waitForElement(driver, 'name', 'OK', 5000); if (!okEl) { console.log('Delete confirmation not found'); return false; } await driver.tapElement(okEl); await sleep(3000); await driver.goBackToHomepage(); await sleep(1000); const homeSource = await driver.getSource(); return !homeSource.includes(keyword); }