AI_UIAutomation/tests/automation/automation_general.test.ts

335 lines
14 KiB
TypeScript

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<void> {
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<boolean> {
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;
}
});
});