280 lines
9.5 KiB
TypeScript
280 lines
9.5 KiB
TypeScript
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<void>;
|
|
postConnectionSteps?: (driver: DeviceDriver) => Promise<void>;
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<string | null> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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);
|
|
}
|