import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'; import { DeviceDriver } from '../../drivers/types'; import { createDriver } from '../../drivers/factory'; import { TestReporter } from '../../utils/test-reporter'; import { sleep } from '../../utils/common/element.helper'; import { execSync } from 'child_process'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); describe('【通用自动化】- 创建/删除流程', () => { let driver: DeviceDriver; let reporter: TestReporter; beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter('Automation_General', driver.platform.toUpperCase()); }); beforeEach(async () => { await driver.dismissPopupIfPresent(); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); function isAndroid() { return driver.platform === 'android'; } async function ensureAppRunning(): Promise { if (!isAndroid()) return; try { const src = await driver.getSource(); if (src.includes('com.theswitchbot.switchbot') || src.includes('SwitchBot') || src.includes('Home') || src.includes('Automation') || src.includes('Scene')) return; } catch { /* app likely crashed */ } try { execSync('adb shell am force-stop com.theswitchbot.switchbot'); await sleep(2000); execSync('adb shell am start -n com.theswitchbot.switchbot/.index.ui.SplashActivity'); await sleep(10000); await driver.dismissPopupIfPresent(); } catch { /* ignore */ } } async function logPageElements(source: string): void { if (isAndroid()) { const textRe = /text="([^"]{1,80})"/g; const descRe = /content-desc="([^"]{1,80})"/g; const texts: string[] = []; const descs: string[] = []; let m; while ((m = textRe.exec(source)) !== null) { if (m[1] && !texts.includes(m[1])) texts.push(m[1]); } while ((m = descRe.exec(source)) !== null) { if (m[1] && !descs.includes(m[1])) descs.push(m[1]); } console.log('Page texts:', texts.join(' | ')); if (descs.length) console.log('Page descs:', descs.join(' | ')); } } async function navigateToAutomationTab(steps: string[]): Promise { await ensureAppRunning(); const src = await driver.getSource(); // Check if already on the Automations list with My Automations visible if (src.includes('My Automations') && (src.includes('Recommended') || src.includes('新自动化'))) { steps.push('已在Automation页面'); return true; } // On creation sub-page — force restart app to clear state if (src.includes('Add condition') || src.includes('Add action')) { if (isAndroid()) { execSync('adb shell am force-stop com.theswitchbot.switchbot'); await sleep(2000); execSync('adb shell am start -n com.theswitchbot.switchbot/.index.ui.SplashActivity'); await sleep(10000); await driver.dismissPopupIfPresent(); steps.push('强制重启App清除创建状态'); // After restart, we're on home page — find and tap Automations tab directly let tabEl: string | null = null; tabEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Automations")'); if (!tabEl) tabEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Automations")'); if (tabEl) { await driver.tapElement(tabEl); await sleep(3000); // 处理引导弹框 await driver.dismissPopupIfPresent(); let guideBtn: string | null = null; guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")'); if (!guideBtn) guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("OK")'); if (!guideBtn) guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Skip")'); if (!guideBtn) guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("I know")'); if (guideBtn) { await driver.tapElement(guideBtn); await sleep(2000); steps.push('关闭引导弹框'); } steps.push('点击Automation tab'); const pageSrc = await driver.getSource(); await logPageElements(pageSrc); return pageSrc.includes('My Automations') || pageSrc.includes('Recommended'); } return false; } else { for (let i = 0; i < 5; i++) { await driver.goBack(); await sleep(1000); } } } // Go home and click Automations tab await driver.goBackToHomepage(); await sleep(2000); await driver.dismissPopupIfPresent(); // After goBackToHomepage, check the current state const homeSrc = await driver.getSource(); console.log('After goBackToHomepage, checking for Automations tab...'); let tabEl: string | null = null; if (isAndroid()) { tabEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Automations")'); if (!tabEl) tabEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Automations")'); if (!tabEl) tabEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Automation")'); if (!tabEl) tabEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Automation")'); } else { tabEl = await driver.findElementRaw('predicate string', 'label == "Automations" OR label == "Automation"'); } if (!tabEl) { steps.push('未找到Automation tab'); await logPageElements(homeSrc); return false; } await driver.tapElement(tabEl); await sleep(3000); steps.push('点击Automation tab'); // 处理引导弹框 (弹框按钮文案可能为 "Next", "Got it", "OK", "Skip") await driver.dismissPopupIfPresent(); if (isAndroid()) { // Check if the page shows a guide/tutorial overlay (not the creation "Add condition" page) const guideSrc = await driver.getSource(); const isGuidePopup = !guideSrc.includes('Add what will trigger'); let guideBtn: string | null = null; if (isGuidePopup) { guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")'); } if (!guideBtn) guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")'); if (!guideBtn) guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("OK")'); if (!guideBtn) guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Skip")'); if (!guideBtn) guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("I know")'); if (!guideBtn) guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Got it")'); if (!guideBtn) guideBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Close")'); if (guideBtn) { await driver.tapElement(guideBtn); await sleep(2000); steps.push('关闭引导弹框'); // Might need to dismiss multiple steps of guide const afterGuide = await driver.getSource(); if (!afterGuide.includes('My Automations')) { let guideBtn2: string | null = null; guideBtn2 = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")'); if (!guideBtn2) guideBtn2 = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")'); if (!guideBtn2) guideBtn2 = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Done")'); if (guideBtn2) { await driver.tapElement(guideBtn2); await sleep(2000); steps.push('关闭第二步引导'); } } } } const pageSrc = await driver.getSource(); await logPageElements(pageSrc); return pageSrc.includes('My Automations') || pageSrc.includes('Recommended') || pageSrc.includes('automation'); } it('1.1 导航到Automation Tab并发现页面元素', { timeout: 120000 }, async () => { const start = Date.now(); const steps: string[] = []; try { const success = await navigateToAutomationTab(steps); expect(success).toBe(true); const src = await driver.getSource(); await logPageElements(src); steps.push('Automation页面元素已打印'); reporter.record('导航到Automation Tab', 'PASS', Date.now() - start, steps.join(' → ')); } catch (e: any) { reporter.record('导航到Automation Tab', 'FAIL', Date.now() - start, e.message); throw e; } }); it('1.2 点击Add按钮并发现创建流程页面', { timeout: 120000 }, async () => { const start = Date.now(); const steps: string[] = []; try { const onTab = await navigateToAutomationTab(steps); expect(onTab).toBe(true); let addEl: string | null = null; if (isAndroid()) { addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")'); if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")'); if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")'); if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Create")'); if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("add")'); } else { addEl = await driver.findElementRaw('predicate string', 'label == "Add" OR label == "+"'); } if (!addEl) { // Try resource ID or class-based search for FAB addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.ImageButton")'); if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().resourceId("com.theswitchbot.switchbot:id/addBto")'); if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().resourceId("com.theswitchbot.switchbot:id/fab")'); if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().resourceId("com.theswitchbot.switchbot:id/add")'); } // Print full source for debugging if still not found if (!addEl) { const debugSrc = await driver.getSource(); // Look for clickable elements const clickRe = /class="([^"]+)"[^>]*clickable="true"[^>]*(resource-id="([^"]*)")?/g; const clickable: string[] = []; let cm; while ((cm = clickRe.exec(debugSrc)) !== null) { clickable.push(`${cm[1]}${cm[3] ? ':'+cm[3] : ''}`); } console.log('Clickable elements:', clickable.join(' | ')); // Also look for ImageButton/ImageView with resource-id const idRe = /resource-id="([^"]{1,100})"/g; const ids: string[] = []; while ((cm = idRe.exec(debugSrc)) !== null) { if (cm[1] && !ids.includes(cm[1])) ids.push(cm[1]); } console.log('Resource IDs:', ids.join(' | ')); } expect(addEl).not.toBeNull(); await driver.tapElement(addEl!); await sleep(5000); steps.push('点击Add按钮'); const pageSrc = await driver.getSource(); await logPageElements(pageSrc); steps.push('Add后页面元素已打印'); reporter.record('点击Add发现创建页面', 'PASS', Date.now() - start, steps.join(' → ')); } catch (e: any) { const src = await driver.getSource(); await logPageElements(src); reporter.record('点击Add发现创建页面', 'FAIL', Date.now() - start, e.message); throw e; } }); it('1.3 探索条件选择页面', { timeout: 120000 }, async () => { const start = Date.now(); const steps: string[] = []; try { const onTab = await navigateToAutomationTab(steps); expect(onTab).toBe(true); // Click Add button let addEl: string | null = null; if (isAndroid()) { addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().resourceId("com.theswitchbot.switchbot:id/addBto")'); if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.ImageButton")'); } expect(addEl).not.toBeNull(); await driver.tapElement(addEl!); await sleep(5000); steps.push('点击Add按钮'); // Dismiss all guide popups (multiple "Got it" tooltips) for (let i = 0; i < 5; i++) { const gotIt = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")'); if (gotIt) { await driver.tapElement(gotIt); await sleep(2000); steps.push(`关闭引导弹框${i + 1}`); } else { break; } } // Now on the "Add condition" wizard page let src = await driver.getSource(); await logPageElements(src); // Click the condition card to enter condition type selection let condCard: string | null = null; condCard = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Add condition")'); if (!condCard) condCard = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add condition")'); if (condCard) { await driver.tapElement(condCard); await sleep(3000); // Dismiss any more tooltips for (let i = 0; i < 3; i++) { const gotIt = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")'); if (gotIt) { await driver.tapElement(gotIt); await sleep(1500); } else break; } steps.push('点击条件卡片'); src = await driver.getSource(); await logPageElements(src); } reporter.record('探索条件选择页面', 'PASS', Date.now() - start, steps.join(' → ')); } catch (e: any) { reporter.record('探索条件选择页面', 'FAIL', Date.now() - start, e.message); throw e; } }); });