1602 lines
62 KiB
TypeScript
1602 lines
62 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 { getDeviceName } from '../../config/device.config';
|
||
import { sleep } from '../../utils/common';
|
||
import { execSync } from 'child_process';
|
||
import { robustBeforeAll, robustBeforeEach } from './aihub-setup.helper';
|
||
import * as dotenv from 'dotenv';
|
||
import * as path from 'path';
|
||
|
||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||
|
||
const AIHUB_NAME = getDeviceName('aihub', 'AIHUB_NAME');
|
||
|
||
describe('【AI Hub 勿扰模式】- Do Not Disturb 功能覆盖', () => {
|
||
let driver: DeviceDriver;
|
||
let reporter: TestReporter;
|
||
|
||
const isAndroid = () => driver.platform === 'android';
|
||
const SETTINGS_ICON = () => isAndroid() ? { x: 999, y: 175 } : { x: 361, y: 70 };
|
||
|
||
beforeAll(async () => {
|
||
driver = createDriver();
|
||
await driver.createSession();
|
||
await robustBeforeAll(driver);
|
||
reporter = new TestReporter('AIHub_DND', driver.platform.toUpperCase());
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
await robustBeforeEach(driver);
|
||
});
|
||
|
||
afterAll(async () => {
|
||
reporter.generate();
|
||
await driver.destroySession();
|
||
});
|
||
|
||
// --- 辅助函数 ---
|
||
async function captureScreenshot(): Promise<string | undefined> {
|
||
try { return await driver.screenshot(); } catch { return undefined; }
|
||
}
|
||
|
||
async function waitForLoading(maxWait = 30000): Promise<void> {
|
||
const start = Date.now();
|
||
while (Date.now() - start < maxWait) {
|
||
const s = await driver.getSource();
|
||
if (!s.includes('Loading') && !s.includes('In progress')) return;
|
||
await sleep(3000);
|
||
}
|
||
}
|
||
|
||
async function logPageElements(): Promise<string> {
|
||
const source = await driver.getSource();
|
||
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(' | '));
|
||
} else {
|
||
const nameRe = /name="([^"]{1,80})"/g;
|
||
const names: string[] = [];
|
||
let m;
|
||
while ((m = nameRe.exec(source)) !== null) {
|
||
if (!names.includes(m[1])) names.push(m[1]);
|
||
}
|
||
console.log('Page elements:', names.join(' | '));
|
||
}
|
||
return source;
|
||
}
|
||
|
||
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('Cameras') || src.includes('AI Events')) 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 enterHubFunctionPage(): Promise<boolean> {
|
||
const src = await driver.getSource();
|
||
if (src.includes('Cameras') && src.includes('AI Events')) return true;
|
||
|
||
await ensureAppRunning();
|
||
await driver.goBackToHomepage();
|
||
await sleep(2000);
|
||
await driver.dismissPopupIfPresent();
|
||
|
||
if (isAndroid()) {
|
||
const card = await (driver as any).findDeviceCard(AIHUB_NAME);
|
||
if (!card) return false;
|
||
await driver.tapElement(card);
|
||
await sleep(5000);
|
||
await waitForLoading();
|
||
await driver.dismissPopupIfPresent();
|
||
const s = await driver.getSource();
|
||
return s.includes('Cameras') || s.includes('AI Events');
|
||
}
|
||
|
||
for (let scroll = 0; scroll <= 5; scroll++) {
|
||
let hubEl = await driver.findElementRaw('predicate string',
|
||
`name CONTAINS "${AIHUB_NAME}" AND type == "XCUIElementTypeCell"`);
|
||
if (!hubEl) {
|
||
hubEl = await driver.findElementRaw('predicate string', `label CONTAINS "${AIHUB_NAME}"`);
|
||
}
|
||
if (hubEl) {
|
||
await driver.tapElement(hubEl);
|
||
await sleep(5000);
|
||
await waitForLoading();
|
||
await driver.dismissPopupIfPresent();
|
||
const s = await driver.getSource();
|
||
if (s.includes('Cameras') || s.includes('AI Events')) return true;
|
||
}
|
||
if (scroll < 5) {
|
||
await driver.swipe(195, 650, 195, 300, 0.5);
|
||
await sleep(1500);
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function enterHubSettings(): Promise<boolean> {
|
||
const src = await driver.getSource();
|
||
if (src.includes('Motion Detection') || src.includes('Firmware') || src.includes('Do Not Disturb')) {
|
||
return true;
|
||
}
|
||
|
||
const inHub = await enterHubFunctionPage();
|
||
if (!inHub) return false;
|
||
|
||
await driver.tap(SETTINGS_ICON().x, SETTINGS_ICON().y);
|
||
await sleep(5000);
|
||
await waitForLoading();
|
||
|
||
const settingSrc = await driver.getSource();
|
||
return settingSrc.includes('Motion Detection') || settingSrc.includes('Firmware')
|
||
|| settingSrc.includes('Do Not Disturb') || settingSrc.includes('Wi-Fi');
|
||
}
|
||
|
||
async function enterDNDPage(): Promise<boolean> {
|
||
const steps: string[] = [];
|
||
|
||
// Check if already on DND page
|
||
const curSrc = await driver.getSource();
|
||
if (curSrc.includes('Do Not Disturb') && (curSrc.includes('Add') || curSrc.includes('Tap Add below'))) {
|
||
steps.push('已在DND页面');
|
||
console.log('DND nav:', steps.join(' → '));
|
||
return true;
|
||
}
|
||
|
||
// Check if stuck on DND edit page (Start time / End time / Save)
|
||
if (curSrc.includes('Start time') && curSrc.includes('End time') && curSrc.includes('Save')) {
|
||
// On the add/edit form - go back, handle unsaved changes dialog (点确认退出)
|
||
await driver.goBack();
|
||
await sleep(1500);
|
||
if (isAndroid()) {
|
||
let confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
if (!confirmEl) confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("OK")');
|
||
if (!confirmEl) confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Yes")');
|
||
if (confirmEl) {
|
||
await driver.tapElement(confirmEl);
|
||
await sleep(2000);
|
||
steps.push('关闭未保存弹窗(确认退出)');
|
||
}
|
||
}
|
||
// Now check if we're back to DND list
|
||
const afterSrc = await driver.getSource();
|
||
if (afterSrc.includes('Do Not Disturb') && (afterSrc.includes('Add') || afterSrc.includes('Tap Add below'))) {
|
||
steps.push('从编辑页返回DND列表');
|
||
console.log('DND nav:', steps.join(' → '));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Check if in delete mode (has Select All / Finish)
|
||
if (curSrc.includes('Select All') && curSrc.includes('Finish')) {
|
||
let finishEl: string | null = null;
|
||
if (isAndroid()) {
|
||
finishEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Finish")');
|
||
if (!finishEl) finishEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Finish")');
|
||
}
|
||
if (finishEl) {
|
||
await driver.tapElement(finishEl);
|
||
await sleep(2000);
|
||
steps.push('退出删除模式');
|
||
}
|
||
const afterSrc = await driver.getSource();
|
||
if (afterSrc.includes('Do Not Disturb') && afterSrc.includes('Add')) {
|
||
console.log('DND nav:', steps.join(' → '));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Navigate to Hub settings
|
||
const inSettings = await enterHubSettings();
|
||
if (!inSettings) {
|
||
steps.push('无法进入Hub设置页');
|
||
console.log('DND nav:', steps.join(' → '));
|
||
return false;
|
||
}
|
||
steps.push('已在Hub设置页');
|
||
|
||
// Find and tap "Do Not Disturb"
|
||
let dndEl: string | null = null;
|
||
if (isAndroid()) {
|
||
dndEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Do Not Disturb")');
|
||
if (!dndEl) dndEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Do Not Disturb")');
|
||
if (!dndEl) dndEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("勿扰")');
|
||
} else {
|
||
dndEl = await driver.findElementRaw('predicate string', 'label CONTAINS "Do Not Disturb"');
|
||
if (!dndEl) dndEl = await driver.findElementRaw('predicate string', 'label CONTAINS "勿扰"');
|
||
}
|
||
|
||
if (!dndEl) {
|
||
// Scroll down to find it
|
||
for (let i = 0; i < 3; i++) {
|
||
await driver.scrollDown(300);
|
||
await sleep(2000);
|
||
if (isAndroid()) {
|
||
dndEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Do Not Disturb")');
|
||
if (!dndEl) dndEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Do Not Disturb")');
|
||
if (!dndEl) dndEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("勿扰")');
|
||
} else {
|
||
dndEl = await driver.findElementRaw('predicate string', 'label CONTAINS "Do Not Disturb"');
|
||
}
|
||
if (dndEl) break;
|
||
}
|
||
}
|
||
|
||
if (!dndEl) {
|
||
steps.push('未找到Do Not Disturb入口');
|
||
console.log('DND nav:', steps.join(' → '));
|
||
await logPageElements();
|
||
return false;
|
||
}
|
||
|
||
await driver.tapElement(dndEl);
|
||
await sleep(3000);
|
||
await waitForLoading();
|
||
steps.push('点击Do Not Disturb');
|
||
|
||
// Dismiss any guide popup
|
||
await driver.dismissPopupIfPresent();
|
||
if (isAndroid()) {
|
||
const gotIt = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")');
|
||
if (gotIt) {
|
||
await driver.tapElement(gotIt);
|
||
await sleep(1500);
|
||
steps.push('关闭引导弹框');
|
||
}
|
||
}
|
||
|
||
console.log('DND nav:', steps.join(' → '));
|
||
return true;
|
||
}
|
||
|
||
async function exitEditPage(): Promise<void> {
|
||
// 退出编辑页: goBack → 未保存弹窗 → 点确认退出
|
||
// 规则: 尝试goBack,如果弹窗出现则点确认; 如果多次仍有弹窗,直接点确认
|
||
for (let attempt = 0; attempt < 3; attempt++) {
|
||
const curSrc = await driver.getSource();
|
||
// 已经不在编辑页了
|
||
if (!curSrc.includes('Start time') && !curSrc.includes('End time') && !curSrc.includes('Save')) {
|
||
return;
|
||
}
|
||
|
||
await driver.goBack();
|
||
await sleep(1500);
|
||
|
||
// 检查弹窗
|
||
if (isAndroid()) {
|
||
const confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
if (confirmEl) {
|
||
await driver.tapElement(confirmEl);
|
||
await sleep(2000);
|
||
return;
|
||
}
|
||
// 也可能按钮是OK/Yes/Discard
|
||
const okEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("OK")');
|
||
if (okEl) {
|
||
await driver.tapElement(okEl);
|
||
await sleep(2000);
|
||
return;
|
||
}
|
||
const yesEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Yes")');
|
||
if (yesEl) {
|
||
await driver.tapElement(yesEl);
|
||
await sleep(2000);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async function pressKeyboardSearch(): Promise<void> {
|
||
if (isAndroid()) {
|
||
execSync('adb shell input keyevent 66');
|
||
await sleep(2000);
|
||
}
|
||
}
|
||
|
||
// ==========================================================================
|
||
// 第1组: DND页面发现 & 基础显示验证
|
||
// ==========================================================================
|
||
|
||
it('1.1 进入勿扰模式页面并发现元素', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('成功进入DND页面');
|
||
|
||
const src = await logPageElements();
|
||
steps.push('页面元素已打印');
|
||
|
||
reporter.record('进入勿扰模式页面', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('进入勿扰模式页面', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('1.2 勿扰页面显示验证', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
const src = await driver.getSource();
|
||
// 验证页面包含基本元素 (根据首轮发现修正)
|
||
const hasDNDTitle = src.includes('Do Not Disturb') || src.includes('勿扰');
|
||
expect(hasDNDTitle).toBe(true);
|
||
steps.push('页面标题正确');
|
||
|
||
// 检查是否有添加按钮
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
// Add button may or may not exist depending on page state - just log
|
||
if (addEl) {
|
||
steps.push('Add按钮存在');
|
||
} else {
|
||
steps.push('未发现Add按钮(可能用其他方式添加)');
|
||
}
|
||
|
||
reporter.record('勿扰页面显示', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('勿扰页面显示', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('1.3 勿扰默认状态验证', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
const src = await driver.getSource();
|
||
// 默认应显示空列表或已有的勿扰时间段
|
||
const hasSchedule = src.includes(':') && (src.includes('AM') || src.includes('PM')
|
||
|| src.includes('Every day') || src.includes('Once') || src.includes('Mon'));
|
||
const isEmpty = src.includes('No schedule') || src.includes('No data')
|
||
|| src.includes('空') || src.includes('no items');
|
||
|
||
if (hasSchedule) {
|
||
steps.push('存在已设置的勿扰时间段');
|
||
} else if (isEmpty) {
|
||
steps.push('勿扰列表为空(默认状态)');
|
||
} else {
|
||
steps.push('页面状态已记录');
|
||
}
|
||
await logPageElements();
|
||
|
||
reporter.record('勿扰默认状态', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('勿扰默认状态', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ==========================================================================
|
||
// 第2组: 添加勿扰时间段
|
||
// ==========================================================================
|
||
|
||
it('2.1 添加勿扰时间段-默认保存', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
// 点击Add按钮
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().resourceId("com.theswitchbot.switchbot:id/addBto")');
|
||
} else {
|
||
addEl = await driver.findElementRaw('predicate string', 'label == "Add" OR label == "+"');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add按钮');
|
||
|
||
// 进入添加/编辑页面 - 打印页面元素
|
||
const editSrc = await logPageElements();
|
||
steps.push('添加页面元素已打印');
|
||
|
||
// 查找Save/确认按钮
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
if (!saveEl) saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
if (!saveEl) saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Save")');
|
||
} else {
|
||
saveEl = await driver.findElementRaw('predicate string', 'label == "Save"');
|
||
}
|
||
|
||
if (saveEl) {
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
steps.push('点击Save保存');
|
||
} else {
|
||
steps.push('未找到Save按钮(页面结构待确认)');
|
||
await logPageElements();
|
||
}
|
||
|
||
// 验证返回DND列表且有时间段
|
||
const afterSrc = await driver.getSource();
|
||
const hasScheduleAdded = afterSrc.includes(':') && (afterSrc.includes('AM') || afterSrc.includes('PM')
|
||
|| afterSrc.includes('Every day') || afterSrc.includes('Once'));
|
||
steps.push(hasScheduleAdded ? '时间段已添加' : '添加结果待确认');
|
||
|
||
reporter.record('添加勿扰-默认保存', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('添加勿扰-默认保存', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('2.2 重复设置-仅一次', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
// 点击Add
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add');
|
||
|
||
// 寻找 "Only once" / "仅一次" 选项
|
||
let onceEl: string | null = null;
|
||
if (isAndroid()) {
|
||
onceEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Only once")');
|
||
if (!onceEl) onceEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Once")');
|
||
if (!onceEl) onceEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("仅一次")');
|
||
}
|
||
|
||
if (onceEl) {
|
||
await driver.tapElement(onceEl);
|
||
await sleep(2000);
|
||
steps.push('选择仅一次');
|
||
} else {
|
||
steps.push('未找到Once选项');
|
||
await logPageElements();
|
||
}
|
||
|
||
// Save
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
if (!saveEl) saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
}
|
||
if (saveEl) {
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
steps.push('保存');
|
||
}
|
||
|
||
const afterSrc = await driver.getSource();
|
||
const hasOnce = afterSrc.includes('Only once') || afterSrc.includes('Once') || afterSrc.includes('仅一次');
|
||
steps.push(hasOnce ? '仅一次时间段已添加' : '保存结果待确认');
|
||
|
||
reporter.record('重复设置-仅一次', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('重复设置-仅一次', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('2.3 重复设置-工作日(周一到周五)', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
// 点击Add
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add');
|
||
|
||
// 寻找 "Repeat" / "重复" 选项
|
||
let repeatEl: string | null = null;
|
||
if (isAndroid()) {
|
||
repeatEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Repeat")');
|
||
if (!repeatEl) repeatEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("重复")');
|
||
if (!repeatEl) repeatEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Repeat")');
|
||
}
|
||
|
||
if (repeatEl) {
|
||
await driver.tapElement(repeatEl);
|
||
await sleep(2000);
|
||
steps.push('选择重复');
|
||
}
|
||
|
||
// Repeat模式默认全选,取消Sat和Sun即可得到工作日
|
||
// 先取消Sat和Sun
|
||
const weekendDays = ['Sat', 'Sun'];
|
||
for (const day of weekendDays) {
|
||
let dayEl: string | null = null;
|
||
if (isAndroid()) {
|
||
dayEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${day}")`);
|
||
}
|
||
if (dayEl) {
|
||
await driver.tapElement(dayEl);
|
||
await sleep(300);
|
||
}
|
||
}
|
||
steps.push('取消Sat+Sun(保留Mon-Fri)');
|
||
|
||
// 检查是否显示"工作日"/"Weekdays"
|
||
const midSrc = await driver.getSource();
|
||
const showsWeekday = midSrc.includes('Weekdays') || midSrc.includes('工作日')
|
||
|| midSrc.includes('Mon, Tue, Wed, Thu, Fri');
|
||
steps.push(showsWeekday ? '显示工作日/Weekdays' : '周期显示待确认');
|
||
|
||
// Save
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
if (!saveEl) saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
}
|
||
if (saveEl) {
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
steps.push('保存');
|
||
}
|
||
|
||
reporter.record('重复设置-工作日', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('重复设置-工作日', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('2.4 重复设置-周末(周六周日)', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add');
|
||
|
||
// 先切换到 Repeat 模式(天数选择器才可见)
|
||
let repeatEl: string | null = null;
|
||
if (isAndroid()) {
|
||
repeatEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Repeat")');
|
||
}
|
||
if (repeatEl) {
|
||
await driver.tapElement(repeatEl);
|
||
await sleep(1000);
|
||
steps.push('切换到Repeat模式');
|
||
}
|
||
|
||
// 先取消所有已选中的天 (Repeat模式默认全选)
|
||
const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'];
|
||
for (const day of allDays) {
|
||
if (day === 'Sat' || day === 'Sun') continue;
|
||
let dayEl: string | null = null;
|
||
if (isAndroid()) {
|
||
dayEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${day}")`);
|
||
}
|
||
if (dayEl) { await driver.tapElement(dayEl); await sleep(300); }
|
||
}
|
||
steps.push('取消Mon-Fri(仅保留Sat+Sun)');
|
||
|
||
const midSrc = await driver.getSource();
|
||
const showsWeekend = midSrc.includes('Weekend') || midSrc.includes('周末')
|
||
|| midSrc.includes('Sat, Sun');
|
||
steps.push(showsWeekend ? '显示周末/Weekend' : '周期显示待确认');
|
||
|
||
// Save
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
if (!saveEl) saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
}
|
||
if (saveEl) {
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
steps.push('保存');
|
||
}
|
||
|
||
reporter.record('重复设置-周末', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('重复设置-周末', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('2.5 重复设置-自定义日期(如周一三五)', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add');
|
||
|
||
// 先进入Repeat模式(天数选择器可见)
|
||
let repeatEl: string | null = null;
|
||
if (isAndroid()) {
|
||
repeatEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Repeat")');
|
||
}
|
||
if (repeatEl) {
|
||
await driver.tapElement(repeatEl);
|
||
await sleep(1000);
|
||
steps.push('切换到Repeat模式');
|
||
}
|
||
|
||
// Repeat默认全选,取消除Mon/Wed/Fri外的天
|
||
const toDeselect = ['Sun', 'Tue', 'Thur', 'Sat'];
|
||
for (const day of toDeselect) {
|
||
let dayEl: string | null = null;
|
||
if (isAndroid()) {
|
||
dayEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${day}")`);
|
||
}
|
||
if (dayEl) {
|
||
await driver.tapElement(dayEl);
|
||
await sleep(300);
|
||
}
|
||
}
|
||
steps.push('取消Sun/Tue/Thur/Sat(保留Mon/Wed/Fri)');
|
||
|
||
const midSrc = await driver.getSource();
|
||
// 应显示自定义周期(Mon/Wed/Fri已选中)
|
||
const showsCustom = midSrc.includes('Mon') && midSrc.includes('Wed') && midSrc.includes('Fri');
|
||
steps.push(showsCustom ? '自定义周期显示正确(Mon/Wed/Fri)' : '周期显示待确认');
|
||
|
||
// Save
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
if (!saveEl) saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
}
|
||
if (saveEl) {
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
steps.push('保存');
|
||
}
|
||
|
||
reporter.record('重复设置-自定义日期', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('重复设置-自定义日期', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('2.6 重复与仅一次互斥验证', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add');
|
||
|
||
// 先选"重复/Repeat"
|
||
let repeatEl: string | null = null;
|
||
if (isAndroid()) {
|
||
repeatEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Repeat")');
|
||
if (!repeatEl) repeatEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Every day")');
|
||
}
|
||
if (repeatEl) {
|
||
await driver.tapElement(repeatEl);
|
||
await sleep(1000);
|
||
steps.push('选择Repeat');
|
||
}
|
||
|
||
// 再选"仅一次/Only once" - 应互斥
|
||
let onceEl: string | null = null;
|
||
if (isAndroid()) {
|
||
onceEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Only once")');
|
||
if (!onceEl) onceEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Once")');
|
||
}
|
||
if (onceEl) {
|
||
await driver.tapElement(onceEl);
|
||
await sleep(1000);
|
||
steps.push('选择Once');
|
||
}
|
||
|
||
// 验证当前状态: 应为"仅一次"模式,日期选择器消失或重复未选中
|
||
const midSrc = await driver.getSource();
|
||
// Once selected后,weekday selectors应不可见或取消选中
|
||
const noWeekdays = !midSrc.includes('Mon') || !midSrc.includes('Sun');
|
||
steps.push(noWeekdays ? '互斥验证通过(日期选择器消失)' : '互斥验证(页面仍显示日期)');
|
||
|
||
// 保存退出(避免未保存弹窗)
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
}
|
||
if (saveEl) {
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
steps.push('保存退出');
|
||
}
|
||
|
||
reporter.record('重复与仅一次互斥', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('重复与仅一次互斥', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ==========================================================================
|
||
// 第3组: 修改勿扰时间段
|
||
// ==========================================================================
|
||
|
||
it('3.1 修改勿扰时间段', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
const src = await driver.getSource();
|
||
// 找到已存在的时间段卡片 (点击进入编辑)
|
||
// 通常时间段以时间格式显示 (如 "10:00 PM - 7:00 AM")
|
||
let timeCard: string | null = null;
|
||
if (isAndroid()) {
|
||
timeCard = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textMatches(".*\\\\d+:\\\\d+.*")');
|
||
if (!timeCard) timeCard = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains(":")');
|
||
}
|
||
|
||
if (!timeCard) {
|
||
// 如果列表为空,先添加一个
|
||
steps.push('列表为空,先添加一个时间段');
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
}
|
||
if (addEl) {
|
||
await driver.tapElement(addEl);
|
||
await sleep(3000);
|
||
// Save with defaults
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
}
|
||
if (saveEl) {
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
}
|
||
steps.push('添加默认时间段');
|
||
// Now find the card
|
||
timeCard = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains(":")');
|
||
}
|
||
}
|
||
|
||
if (timeCard) {
|
||
await driver.tapElement(timeCard);
|
||
await sleep(3000);
|
||
steps.push('点击时间段卡片进入编辑');
|
||
|
||
// 打印编辑页面元素
|
||
await logPageElements();
|
||
steps.push('编辑页面元素已打印');
|
||
|
||
// 保存退出编辑页
|
||
await exitEditPage();
|
||
} else {
|
||
steps.push('无法获取时间段卡片');
|
||
}
|
||
|
||
reporter.record('修改勿扰时间段', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('修改勿扰时间段', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('3.2 修改勿扰开关(使能关闭)', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
// 查找开关元素 (Switch/Toggle)
|
||
let switchEl: string | null = null;
|
||
if (isAndroid()) {
|
||
switchEl = await driver.findElementRaw('-android uiautomator',
|
||
'new UiSelector().className("android.widget.Switch").instance(0)');
|
||
if (!switchEl) switchEl = await driver.findElementRaw('-android uiautomator',
|
||
'new UiSelector().className("android.widget.ToggleButton").instance(0)');
|
||
}
|
||
|
||
if (switchEl) {
|
||
const beforeAttr = await driver.getElementAttribute(switchEl, 'checked');
|
||
steps.push(`开关当前状态: ${beforeAttr}`);
|
||
|
||
await driver.tapElement(switchEl);
|
||
await sleep(2000);
|
||
steps.push('点击开关');
|
||
|
||
const afterAttr = await driver.getElementAttribute(switchEl, 'checked');
|
||
steps.push(`开关切换后状态: ${afterAttr}`);
|
||
|
||
// 恢复原状
|
||
if (beforeAttr !== afterAttr) {
|
||
await driver.tapElement(switchEl);
|
||
await sleep(2000);
|
||
steps.push('恢复开关状态');
|
||
}
|
||
} else {
|
||
steps.push('未找到开关元素(可能页面结构不同)');
|
||
await logPageElements();
|
||
}
|
||
|
||
reporter.record('修改勿扰开关', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('修改勿扰开关', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ==========================================================================
|
||
// 第4组: 删除勿扰
|
||
// ==========================================================================
|
||
|
||
it('4.1 发现删除入口(右上角按钮)', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
// 右上角最右侧按钮为删除按钮 (Android ~x=999, y=175)
|
||
await driver.tap(999, 175);
|
||
await sleep(3000);
|
||
steps.push('点击右上角删除按钮');
|
||
|
||
// 打印进入的页面元素
|
||
const src = await logPageElements();
|
||
steps.push('删除模式页面元素已打印');
|
||
|
||
// 点击Finish退出删除模式
|
||
let finishEl: string | null = null;
|
||
if (isAndroid()) {
|
||
finishEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Finish")');
|
||
if (!finishEl) finishEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Finish")');
|
||
}
|
||
if (finishEl) {
|
||
await driver.tapElement(finishEl);
|
||
await sleep(2000);
|
||
steps.push('点击Finish退出删除模式');
|
||
} else {
|
||
await driver.goBack();
|
||
await sleep(2000);
|
||
}
|
||
|
||
reporter.record('发现删除入口', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('发现删除入口', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('4.2 删除单个勿扰时间段', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
// 记录删除前数量
|
||
const beforeSrc = await driver.getSource();
|
||
const beforeCards = (beforeSrc.match(/22:00-08:00/g) || []).length;
|
||
steps.push(`删除前卡片数: ${beforeCards}`);
|
||
|
||
if (beforeCards === 0) {
|
||
steps.push('无卡片可删除');
|
||
reporter.record('删除单个勿扰', 'PASS', Date.now() - start, steps.join(' → '));
|
||
return;
|
||
}
|
||
|
||
// 点击右上角删除按钮进入删除模式
|
||
await driver.tap(999, 175);
|
||
await sleep(2000);
|
||
steps.push('进入删除模式');
|
||
|
||
// 选择第一个卡片
|
||
let firstCard: string | null = null;
|
||
if (isAndroid()) {
|
||
firstCard = await driver.findElementRaw('-android uiautomator',
|
||
'new UiSelector().descriptionContains("22:00-08:00")');
|
||
}
|
||
if (firstCard) {
|
||
await driver.tapElement(firstCard);
|
||
await sleep(1000);
|
||
steps.push('选中第一个卡片');
|
||
}
|
||
|
||
// 点击Delete
|
||
let delEl: string | null = null;
|
||
if (isAndroid()) {
|
||
delEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
|
||
if (!delEl) delEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Delete")');
|
||
}
|
||
expect(delEl).not.toBeNull();
|
||
await driver.tapElement(delEl!);
|
||
await sleep(2000);
|
||
steps.push('点击Delete');
|
||
|
||
// 检查确认弹窗
|
||
const dialogSrc = await driver.getSource();
|
||
if (dialogSrc.includes('Cancel') || dialogSrc.includes('Confirm') || dialogSrc.includes('OK')) {
|
||
steps.push('确认弹窗出现');
|
||
let confirmEl: string | null = null;
|
||
if (isAndroid()) {
|
||
confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
if (!confirmEl) confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
|
||
if (!confirmEl) confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("OK")');
|
||
}
|
||
if (confirmEl) {
|
||
await driver.tapElement(confirmEl);
|
||
await sleep(3000);
|
||
steps.push('确认删除');
|
||
}
|
||
}
|
||
|
||
// 验证删除结果
|
||
const afterSrc = await driver.getSource();
|
||
const afterCards = (afterSrc.match(/22:00-08:00/g) || []).length;
|
||
steps.push(`删除后卡片数: ${afterCards}`);
|
||
expect(afterCards).toBeLessThan(beforeCards);
|
||
|
||
reporter.record('删除单个勿扰', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('删除单个勿扰', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('4.3 删除全部勿扰(全选后删除)', { timeout: 180000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
const preSrc = await driver.getSource();
|
||
if (preSrc.includes('Tap Add below') || (!preSrc.includes('22:00-08:00') && !preSrc.includes('Only once'))) {
|
||
steps.push('无时间段可删除');
|
||
reporter.record('删除全部勿扰', 'PASS', Date.now() - start, steps.join(' → '));
|
||
return;
|
||
}
|
||
|
||
// 进入删除模式
|
||
await driver.tap(999, 175);
|
||
await sleep(2000);
|
||
steps.push('进入删除模式');
|
||
|
||
// 全选
|
||
let selectAllEl: string | null = null;
|
||
if (isAndroid()) {
|
||
selectAllEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Select All")');
|
||
if (!selectAllEl) selectAllEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Select All")');
|
||
}
|
||
if (selectAllEl) {
|
||
await driver.tapElement(selectAllEl);
|
||
await sleep(1000);
|
||
steps.push('点击全选');
|
||
}
|
||
|
||
// Delete
|
||
let delEl: string | null = null;
|
||
if (isAndroid()) {
|
||
delEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
|
||
if (!delEl) delEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Delete")');
|
||
}
|
||
expect(delEl).not.toBeNull();
|
||
await driver.tapElement(delEl!);
|
||
await sleep(2000);
|
||
steps.push('点击Delete');
|
||
|
||
// Confirm dialog
|
||
let confirmEl: string | null = null;
|
||
if (isAndroid()) {
|
||
confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
if (!confirmEl) confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
|
||
if (!confirmEl) confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("OK")');
|
||
}
|
||
if (confirmEl) {
|
||
await driver.tapElement(confirmEl);
|
||
await sleep(3000);
|
||
steps.push('确认删除');
|
||
}
|
||
|
||
// Verify empty
|
||
const afterSrc = await driver.getSource();
|
||
const isEmpty = afterSrc.includes('Tap Add below') || afterSrc.includes('No schedule')
|
||
|| (!afterSrc.includes('22:00-08:00') && !afterSrc.includes('Only once'));
|
||
expect(isEmpty).toBe(true);
|
||
steps.push('列表已清空');
|
||
|
||
reporter.record('删除全部勿扰', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('删除全部勿扰', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ==========================================================================
|
||
// 第5组: 数量限制与异常
|
||
// ==========================================================================
|
||
|
||
it('5.1 勿扰数量限制(最多10个)', { timeout: 180000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
// 先清空已有的勿扰(如果有的话)
|
||
// 然后尝试连续添加,验证最大数量限制
|
||
// 这个用例比较耗时,先记录现有数量
|
||
const src = await driver.getSource();
|
||
// Count existing schedules by counting time patterns
|
||
const timeMatches = src.match(/\d{1,2}:\d{2}/g);
|
||
const currentCount = timeMatches ? Math.floor(timeMatches.length / 2) : 0;
|
||
steps.push(`当前勿扰数量约: ${currentCount}`);
|
||
|
||
// 尝试添加直到达到限制
|
||
let addCount = 0;
|
||
for (let i = 0; i < 11 - currentCount; i++) {
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
if (!addEl) {
|
||
steps.push(`第${i + 1}次无法找到Add按钮(可能已达上限)`);
|
||
break;
|
||
}
|
||
await driver.tapElement(addEl);
|
||
await sleep(3000);
|
||
|
||
// Check if limit warning appeared
|
||
const addSrc = await driver.getSource();
|
||
if (addSrc.includes('limit') || addSrc.includes('maximum') || addSrc.includes('最多')
|
||
|| addSrc.includes('Cannot add') || addSrc.includes('cannot')) {
|
||
steps.push(`达到数量上限(第${currentCount + i + 1}个)`);
|
||
await logPageElements();
|
||
// Dismiss
|
||
if (isAndroid()) {
|
||
const okEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("OK")');
|
||
if (okEl) await driver.tapElement(okEl);
|
||
else await driver.goBack();
|
||
}
|
||
await sleep(2000);
|
||
break;
|
||
}
|
||
|
||
// Save with defaults
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
if (!saveEl) saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
}
|
||
if (saveEl) {
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
addCount++;
|
||
} else {
|
||
await driver.goBack();
|
||
await sleep(2000);
|
||
break;
|
||
}
|
||
}
|
||
steps.push(`成功添加${addCount}个时间段`);
|
||
|
||
reporter.record('勿扰数量限制', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('勿扰数量限制', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('5.2 勿扰时间异常(结束=开始)', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add');
|
||
|
||
// 尝试将开始和结束时间设为相同
|
||
// 需要根据真实时间选择器操作 - 首轮先记录时间组件元素
|
||
await logPageElements();
|
||
steps.push('时间组件元素已打印(需基于真实UI调整)');
|
||
|
||
// 尝试保存 - 如果开始=结束可能有警告
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
}
|
||
if (saveEl) {
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
const afterSrc = await driver.getSource();
|
||
if (afterSrc.includes('error') || afterSrc.includes('invalid') || afterSrc.includes('same')) {
|
||
steps.push('检测到时间异常提示');
|
||
}
|
||
}
|
||
|
||
// 保存退出
|
||
await exitEditPage();
|
||
|
||
reporter.record('勿扰时间异常', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('勿扰时间异常', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ==========================================================================
|
||
// 第6组: 时间组件验证
|
||
// ==========================================================================
|
||
|
||
it('6.1 时间组件跟随手机时间制(12h/24h)', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add');
|
||
|
||
// Check if time shows AM/PM (12h) or 24h format
|
||
const src = await driver.getSource();
|
||
const has12h = src.includes('AM') || src.includes('PM');
|
||
steps.push(has12h ? '12小时制(AM/PM)' : '24小时制');
|
||
|
||
// 保存退出
|
||
await exitEditPage();
|
||
|
||
reporter.record('时间制验证', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('时间制验证', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('6.2 开始/结束时间组件设置', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add');
|
||
|
||
// 查找开始时间和结束时间组件
|
||
let startTimeEl: string | null = null;
|
||
let endTimeEl: string | null = null;
|
||
if (isAndroid()) {
|
||
startTimeEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Start time")');
|
||
endTimeEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("End time")');
|
||
if (!startTimeEl) startTimeEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Start time")');
|
||
if (!endTimeEl) endTimeEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("End time")');
|
||
}
|
||
|
||
if (startTimeEl) steps.push('找到Start time组件');
|
||
if (endTimeEl) steps.push('找到End time组件');
|
||
if (!startTimeEl && !endTimeEl) {
|
||
steps.push('未找到时间组件(打印页面)');
|
||
await logPageElements();
|
||
}
|
||
|
||
// 保存退出
|
||
await exitEditPage();
|
||
|
||
reporter.record('时间组件设置', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('时间组件设置', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ==========================================================================
|
||
// 第7组: 勿扰模式列表为空验证
|
||
// ==========================================================================
|
||
|
||
it('7.1 勿扰列表为空状态', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
// 首先删除所有已有的时间段
|
||
let editEl: string | null = null;
|
||
if (isAndroid()) {
|
||
editEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Edit")');
|
||
if (!editEl) editEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Edit")');
|
||
}
|
||
|
||
if (editEl) {
|
||
await driver.tapElement(editEl);
|
||
await sleep(2000);
|
||
|
||
let selectAllEl: string | null = null;
|
||
if (isAndroid()) {
|
||
selectAllEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Select All")');
|
||
}
|
||
if (selectAllEl) {
|
||
await driver.tapElement(selectAllEl);
|
||
await sleep(1000);
|
||
|
||
let delBtn: string | null = null;
|
||
if (isAndroid()) {
|
||
delBtn = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
|
||
}
|
||
if (delBtn) {
|
||
await driver.tapElement(delBtn);
|
||
await sleep(2000);
|
||
let confirmEl: string | null = null;
|
||
if (isAndroid()) {
|
||
confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||
if (!confirmEl) confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
|
||
if (!confirmEl) confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("OK")');
|
||
}
|
||
if (confirmEl) {
|
||
await driver.tapElement(confirmEl);
|
||
await sleep(3000);
|
||
steps.push('删除全部完成');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 验证空列表状态
|
||
const emptySrc = await driver.getSource();
|
||
const isEmpty = emptySrc.includes('No schedule') || emptySrc.includes('No data')
|
||
|| emptySrc.includes('空') || !emptySrc.match(/\d{1,2}:\d{2}\s*(AM|PM)/);
|
||
steps.push(isEmpty ? '列表为空(验证通过)' : '列表状态待确认');
|
||
await logPageElements();
|
||
|
||
reporter.record('勿扰列表为空', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('勿扰列表为空', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ==========================================================================
|
||
// 第8组: 勿扰模式功能验证
|
||
// ==========================================================================
|
||
|
||
it('8.1 勿扰模式图标显示', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
// 先确保有至少一个勿扰时间段
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
const src = await driver.getSource();
|
||
if (!src.match(/\d{1,2}:\d{2}/)) {
|
||
// Add one schedule
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
}
|
||
if (addEl) {
|
||
await driver.tapElement(addEl);
|
||
await sleep(3000);
|
||
let saveEl: string | null = null;
|
||
if (isAndroid()) {
|
||
saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")');
|
||
}
|
||
if (saveEl) {
|
||
await driver.tapElement(saveEl);
|
||
await sleep(3000);
|
||
}
|
||
steps.push('添加一个默认时间段');
|
||
}
|
||
}
|
||
|
||
// 返回到Hub设置页检查图标
|
||
if (isAndroid()) {
|
||
await driver.goBack();
|
||
await sleep(2000);
|
||
}
|
||
|
||
const settingSrc = await driver.getSource();
|
||
const hasDNDIndicator = settingSrc.includes('Do Not Disturb')
|
||
|| settingSrc.includes('DND') || settingSrc.includes('勿扰');
|
||
steps.push(hasDNDIndicator ? '设置页显示DND入口/图标' : 'DND图标状态待确认');
|
||
|
||
reporter.record('勿扰模式图标', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('勿扰模式图标', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('8.2 选择某星期后自动切换到重复模式', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add');
|
||
|
||
// 确保在"仅一次"模式
|
||
let onceEl: string | null = null;
|
||
if (isAndroid()) {
|
||
onceEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Only once")');
|
||
}
|
||
if (onceEl) {
|
||
await driver.tapElement(onceEl);
|
||
await sleep(1000);
|
||
steps.push('先选择Once模式');
|
||
}
|
||
|
||
// 点击某个星期(如Mon) - 应自动切换到重复模式
|
||
let monEl: string | null = null;
|
||
if (isAndroid()) {
|
||
monEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Mon")');
|
||
}
|
||
if (monEl) {
|
||
await driver.tapElement(monEl);
|
||
await sleep(1000);
|
||
steps.push('选择Mon');
|
||
|
||
// 验证已切换到Repeat模式
|
||
const midSrc = await driver.getSource();
|
||
const isRepeat = midSrc.includes('Repeat') || midSrc.includes('Mon');
|
||
steps.push(isRepeat ? '自动切换到重复模式' : '模式切换待确认');
|
||
} else {
|
||
steps.push('未找到Mon按钮');
|
||
await logPageElements();
|
||
}
|
||
|
||
// 保存退出
|
||
await exitEditPage();
|
||
|
||
reporter.record('星期选择自动切换重复', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('星期选择自动切换重复', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('8.3 取消某星期后重复周期更新', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
let addEl: string | null = null;
|
||
if (isAndroid()) {
|
||
addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Add")');
|
||
if (!addEl) addEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("+")');
|
||
}
|
||
expect(addEl).not.toBeNull();
|
||
await driver.tapElement(addEl!);
|
||
await sleep(3000);
|
||
steps.push('点击Add');
|
||
|
||
// 选择 Repeat (Every day)
|
||
let repeatEl: string | null = null;
|
||
if (isAndroid()) {
|
||
repeatEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Repeat")');
|
||
if (!repeatEl) repeatEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Every day")');
|
||
}
|
||
if (repeatEl) {
|
||
await driver.tapElement(repeatEl);
|
||
await sleep(1000);
|
||
steps.push('选择Repeat');
|
||
}
|
||
|
||
// 取消一个星期 (如取消Sun)
|
||
let sunEl: string | null = null;
|
||
if (isAndroid()) {
|
||
sunEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Sun")');
|
||
}
|
||
if (sunEl) {
|
||
await driver.tapElement(sunEl);
|
||
await sleep(1000);
|
||
steps.push('取消Sun');
|
||
|
||
// 验证显示更新(不再显示"Every day",应显示具体天数)
|
||
const midSrc = await driver.getSource();
|
||
const noEveryDay = !midSrc.includes('Every day');
|
||
steps.push(noEveryDay ? '周期显示已更新(非Every day)' : '显示仍为Every day');
|
||
}
|
||
|
||
// 保存退出
|
||
await exitEditPage();
|
||
|
||
reporter.record('取消星期周期更新', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('取消星期周期更新', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('8.4 新手引导弹窗', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
const steps: string[] = [];
|
||
try {
|
||
// 新手引导通常只在首次进入时出现
|
||
// 这里仅验证进入页面后是否有引导提示
|
||
const onPage = await enterDNDPage();
|
||
expect(onPage).toBe(true);
|
||
steps.push('进入DND页面');
|
||
|
||
const src = await driver.getSource();
|
||
const hasGuide = src.includes('Got it') || src.includes('guide') || src.includes('tutorial')
|
||
|| src.includes('了解') || src.includes('引导');
|
||
steps.push(hasGuide ? '检测到引导提示' : '无引导弹窗(非首次进入)');
|
||
|
||
reporter.record('新手引导', 'PASS', Date.now() - start, steps.join(' → '));
|
||
} catch (e: any) {
|
||
const ss = await captureScreenshot();
|
||
reporter.record('新手引导', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
});
|