335 lines
14 KiB
TypeScript
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;
|
|
}
|
|
});
|
|
});
|