1022 lines
42 KiB
TypeScript
1022 lines
42 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 { 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');
|
||
const CAMERA_NAME = getDeviceName('camera', 'CAMERA_DEVICE');
|
||
|
||
// ============================================================
|
||
// ONES 用例: AI Hub - APP用例 → 设置页 → 本地存储
|
||
// 模块: 本地存储页面, SD卡操作, 录像模式, NAS存储
|
||
// ============================================================
|
||
|
||
describe('AIHub Local Storage - 本地存储设置', () => {
|
||
let driver: DeviceDriver;
|
||
let reporter: TestReporter;
|
||
let pageState: string = 'unknown';
|
||
|
||
beforeAll(async () => {
|
||
driver = createDriver();
|
||
await driver.createSession();
|
||
await robustBeforeAll(driver);
|
||
reporter = new TestReporter('AIHub_LocalStorage', driver.platform.toUpperCase());
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
await robustBeforeEach(driver);
|
||
});
|
||
|
||
afterAll(async () => {
|
||
reporter.generate();
|
||
await driver.destroySession();
|
||
});
|
||
|
||
// ======================== 工具层 ========================
|
||
|
||
async function getSource(): Promise<string> {
|
||
const src = await driver.getSource();
|
||
detectPage(src);
|
||
return src;
|
||
}
|
||
|
||
function detectPage(src: string): void {
|
||
if (src.includes('Local Storage') && (src.includes('SD') || src.includes('NAS') || src.includes('Storage Mode'))) {
|
||
pageState = 'local_storage';
|
||
} else if (src.includes('NAS') && (src.includes('IP') || src.includes('Account') || src.includes('Scan') || src.includes('Add'))) {
|
||
pageState = 'nas_settings';
|
||
} else if (src.includes('Storage Mode') && (src.includes('Events Only') || src.includes('Continuous'))) {
|
||
pageState = 'storage_mode';
|
||
} else if (src.includes('Motion Detection') && (src.includes('Firmware') || src.includes('Device Info') || src.includes('Device Settings'))) {
|
||
pageState = 'hub_settings';
|
||
} else if (src.includes('Cameras') && (src.includes('AI Events') || src.includes('AI Routines'))) {
|
||
pageState = 'hub_function';
|
||
} else if ((src.includes('All Devices') || src.includes('content-desc="Home"')) && !src.includes('Motion Detection')) {
|
||
pageState = 'homepage';
|
||
} else {
|
||
pageState = 'unknown';
|
||
}
|
||
}
|
||
|
||
async function findByText(text: string): Promise<string | null> {
|
||
return driver.findElementRaw('name', text);
|
||
}
|
||
|
||
async function findByTextContains(text: string): Promise<string | null> {
|
||
return driver.findElementRaw('predicate string', `name CONTAINS "${text}"`);
|
||
}
|
||
|
||
async function goBack(): Promise<void> {
|
||
if (driver.platform === 'android') {
|
||
await (driver as any).goBack();
|
||
} else {
|
||
await driver.tap(39, 70);
|
||
}
|
||
await sleep(2000);
|
||
}
|
||
|
||
async function goBackToHomepage(): Promise<boolean> {
|
||
for (let i = 0; i < 8; i++) {
|
||
const src = await getSource();
|
||
if (pageState === 'homepage') return true;
|
||
await goBack();
|
||
}
|
||
return pageState === 'homepage';
|
||
}
|
||
|
||
async function screenshot(name: string): Promise<string | undefined> {
|
||
try {
|
||
const b64 = await driver.screenshot();
|
||
const dir = path.resolve(__dirname, '../../reports/screenshots');
|
||
const fs = await import('fs');
|
||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||
const filePath = path.join(dir, `${name}_${Date.now()}.png`);
|
||
fs.writeFileSync(filePath, Buffer.from(b64, 'base64'));
|
||
return b64;
|
||
} catch { return undefined; }
|
||
}
|
||
|
||
async function waitForLoading(maxWait = 15000): 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(2000);
|
||
}
|
||
}
|
||
|
||
async function scrollAndTap(text: string, maxScrolls = 4): Promise<boolean> {
|
||
for (let i = 0; i <= maxScrolls; i++) {
|
||
const el = await findByText(text) || await findByTextContains(text);
|
||
if (el) { await driver.tapElement(el); return true; }
|
||
if (i < maxScrolls) {
|
||
const size = await driver.getWindowSize();
|
||
await driver.swipe(size.width / 2, size.height * 0.7, size.width / 2, size.height * 0.3, 0.5);
|
||
await sleep(1500);
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ======================== 导航层 ========================
|
||
|
||
async function navToHubFunction(): Promise<boolean> {
|
||
if (pageState === 'hub_function') return true;
|
||
await getSource();
|
||
if (pageState === 'hub_function') return true;
|
||
|
||
if (pageState !== 'homepage') await goBackToHomepage();
|
||
await sleep(1000);
|
||
await driver.dismissPopupIfPresent();
|
||
|
||
console.log(` [nav] 查找Hub: "${AIHUB_NAME}"`);
|
||
const hubEl = await driver.findDeviceCard(AIHUB_NAME);
|
||
if (!hubEl) { console.log(' [nav] Hub未找到'); return false; }
|
||
|
||
await driver.tapElement(hubEl);
|
||
await sleep(5000);
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
if (driver.platform === 'android') {
|
||
const gotIt = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")');
|
||
if (gotIt) { await driver.tapElement(gotIt); await sleep(1500); continue; }
|
||
}
|
||
await driver.dismissPopupIfPresent();
|
||
await getSource();
|
||
if (pageState === 'hub_function') return true;
|
||
await sleep(1000);
|
||
}
|
||
return pageState === 'hub_function';
|
||
}
|
||
|
||
async function navToHubSettings(): Promise<boolean> {
|
||
if (pageState === 'hub_settings') return true;
|
||
if (pageState !== 'hub_function') {
|
||
if (!await navToHubFunction()) return false;
|
||
}
|
||
console.log(' [nav] 点击设置');
|
||
if (driver.platform === 'android') {
|
||
const gotIt = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")');
|
||
if (gotIt) { await driver.tapElement(gotIt); await sleep(1500); }
|
||
await driver.tap(999, 175);
|
||
} else {
|
||
await driver.tap(361, 70);
|
||
}
|
||
await sleep(5000);
|
||
await waitForLoading();
|
||
await getSource();
|
||
console.log(` [nav] 设置页状态: ${pageState}`);
|
||
return pageState === 'hub_settings';
|
||
}
|
||
|
||
async function navToLocalStorage(): Promise<boolean> {
|
||
if (pageState === 'local_storage') return true;
|
||
if (pageState !== 'hub_settings') {
|
||
if (!await navToHubSettings()) return false;
|
||
}
|
||
console.log(' [nav] 查找Local Storage');
|
||
if (!await scrollAndTap('Local Storage')) {
|
||
console.log(' [nav] Local Storage未找到');
|
||
return false;
|
||
}
|
||
await sleep(3000);
|
||
await waitForLoading();
|
||
await getSource();
|
||
console.log(` [nav] 本地存储页状态: ${pageState}`);
|
||
return pageState === 'local_storage';
|
||
}
|
||
|
||
async function ensureLocalStorage(): Promise<boolean> {
|
||
await getSource();
|
||
if (pageState === 'local_storage') return true;
|
||
if (pageState === 'storage_mode' || pageState === 'nas_settings') {
|
||
await goBack();
|
||
await getSource();
|
||
if (pageState === 'local_storage') return true;
|
||
}
|
||
return await navToLocalStorage();
|
||
}
|
||
|
||
// ============================================================
|
||
// Section 1: 本地存储页面显示
|
||
// ============================================================
|
||
|
||
it('1.1 本地存储页面显示 (含本机存储信息)', { timeout: 180000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[1.1] Step1: 导航到本地存储页面');
|
||
const ok = await navToLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[1.1] Step2: 验证存储信息');
|
||
const src = await getSource();
|
||
|
||
// 验证本机存储 (On-device storage)
|
||
const hasOnDevice = src.includes('On-device storage') || src.includes('本机存储') || src.includes('device storage');
|
||
console.log(`[1.1] 本机存储: ${hasOnDevice}`);
|
||
expect(hasOnDevice).toBe(true);
|
||
|
||
// 验证microSD卡存储
|
||
const hasSD = src.includes('microSD') || src.includes('SD card') || src.includes('SD卡');
|
||
console.log(`[1.1] SD卡存储: ${hasSD}`);
|
||
|
||
// 验证已用/可用容量
|
||
const hasCapacity = src.includes('Used') || src.includes('Available') || src.includes('已用') || src.includes('可用');
|
||
console.log(`[1.1] 容量信息: ${hasCapacity}`);
|
||
expect(hasCapacity).toBe(true);
|
||
|
||
// 验证NAS入口
|
||
const hasNAS = src.includes('NAS');
|
||
console.log(`[1.1] NAS入口: ${hasNAS}`);
|
||
|
||
// 验证摄像头列表
|
||
const hasCameras = src.includes('Cameras') || src.includes('摄像');
|
||
console.log(`[1.1] 摄像头列表: ${hasCameras}`);
|
||
|
||
await screenshot('1.1_local_storage_page');
|
||
reporter.record('本地存储页面显示', 'PASS', Date.now() - start, `本机=${hasOnDevice}, SD=${hasSD}, NAS=${hasNAS}, 摄像头=${hasCameras}`);
|
||
} catch (e: any) {
|
||
const ss = await screenshot('1.1_FAIL');
|
||
reporter.record('本地存储页面显示', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('1.2 已插入SD卡 - 绑定摄像头显示', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[1.2] Step1: 确保在本地存储页');
|
||
const ok = await ensureLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[1.2] Step2: 检查SD卡状态和摄像头信息');
|
||
const src = await getSource();
|
||
// 已插入SD卡时应显示: 存储容量, 摄像头列表
|
||
const hasSDInfo = src.includes('SD') || src.includes('Used') || src.includes('Total') ||
|
||
src.includes('已用') || src.includes('总容量');
|
||
if (!hasSDInfo) {
|
||
console.log('[1.2] 未检测到SD卡信息,可能未插入SD卡');
|
||
reporter.record('已插入SD卡-绑定摄像头', 'SKIP', Date.now() - start, '当前环境未插入SD卡, skip');
|
||
return;
|
||
}
|
||
|
||
// 验证绑定摄像头信息
|
||
const hasCamInfo = src.includes(CAMERA_NAME) || src.includes('Camera') || src.includes('摄像');
|
||
console.log(`[1.2] SD卡已插入, 摄像头信息: ${hasCamInfo}`);
|
||
await screenshot('1.2_sd_with_camera');
|
||
|
||
reporter.record('已插入SD卡-绑定摄像头', 'PASS', Date.now() - start, '已插入SD卡页面正常');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('1.2_FAIL');
|
||
reporter.record('已插入SD卡-绑定摄像头', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('1.3 NAS存储入口显示', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[1.3] Step1: 确保在本地存储页');
|
||
const ok = await ensureLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[1.3] Step2: 检查NAS存储入口');
|
||
const src = await getSource();
|
||
// 本地存储页应有NAS存储入口
|
||
const hasNAS = src.includes('NAS');
|
||
console.log(`[1.3] NAS入口显示: ${hasNAS}`);
|
||
await screenshot('1.3_nas_entry');
|
||
|
||
// NAS入口应该存在
|
||
expect(hasNAS).toBe(true);
|
||
|
||
reporter.record('NAS存储入口显示', 'PASS', Date.now() - start, 'NAS入口正常显示');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('1.3_FAIL');
|
||
reporter.record('NAS存储入口显示', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// Section 2: SD卡操作
|
||
// ============================================================
|
||
|
||
it('2.1 格式化SD卡', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[2.1] Step1: 确保在本地存储页');
|
||
const ok = await ensureLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[2.1] Step2: 查找格式化入口');
|
||
const src = await getSource();
|
||
if (!src.includes('Format') && !src.includes('格式化')) {
|
||
console.log('[2.1] 未找到格式化选项(可能未插入SD卡)');
|
||
reporter.record('格式化SD卡', 'SKIP', Date.now() - start, '当前环境无SD卡/无格式化选项, skip');
|
||
return;
|
||
}
|
||
|
||
const formatEl = await findByText('Format') || await findByTextContains('格式化') || await findByTextContains('Format');
|
||
if (!formatEl) {
|
||
reporter.record('格式化SD卡', 'SKIP', Date.now() - start, '格式化按钮不可见, skip');
|
||
return;
|
||
}
|
||
await driver.tapElement(formatEl);
|
||
await sleep(2000);
|
||
|
||
console.log('[2.1] Step3: 验证格式化确认弹窗');
|
||
const confirmSrc = await driver.getSource();
|
||
const hasConfirm = confirmSrc.includes('Format') || confirmSrc.includes('确认') ||
|
||
confirmSrc.includes('Confirm') || confirmSrc.includes('格式化');
|
||
expect(hasConfirm).toBe(true);
|
||
await screenshot('2.1_format_confirm');
|
||
|
||
// 点击取消,不实际格式化
|
||
const cancelEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Cancel")') ||
|
||
await findByText('Cancel') || await findByText('取消');
|
||
if (cancelEl) {
|
||
await driver.tapElement(cancelEl);
|
||
await sleep(1000);
|
||
console.log('[2.1] 已取消格式化');
|
||
}
|
||
|
||
reporter.record('格式化SD卡', 'PASS', Date.now() - start, '格式化弹窗验证正常');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('2.1_FAIL');
|
||
reporter.record('格式化SD卡', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('2.2 取消格式化SD卡', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[2.2] Step1: 确保在本地存储页');
|
||
const ok = await ensureLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[2.2] Step2: 点击格式化');
|
||
const src = await getSource();
|
||
if (!src.includes('Format') && !src.includes('格式化')) {
|
||
reporter.record('取消格式化SD卡', 'SKIP', Date.now() - start, '无格式化选项, skip');
|
||
return;
|
||
}
|
||
|
||
const formatEl = await findByText('Format') || await findByTextContains('Format') || await findByTextContains('格式化');
|
||
if (!formatEl) {
|
||
reporter.record('取消格式化SD卡', 'SKIP', Date.now() - start, '格式化按钮不可见, skip');
|
||
return;
|
||
}
|
||
await driver.tapElement(formatEl);
|
||
await sleep(2000);
|
||
|
||
console.log('[2.2] Step3: 点击取消');
|
||
const cancelEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Cancel")') ||
|
||
await findByText('Cancel') || await findByText('取消');
|
||
expect(cancelEl).not.toBeNull();
|
||
await driver.tapElement(cancelEl!);
|
||
await sleep(1000);
|
||
|
||
console.log('[2.2] Step4: 验证仍在本地存储页');
|
||
const afterSrc = await getSource();
|
||
expect(pageState).toBe('local_storage');
|
||
await screenshot('2.2_cancel_format');
|
||
|
||
reporter.record('取消格式化SD卡', 'PASS', Date.now() - start, '取消格式化后返回本地存储页');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('2.2_FAIL');
|
||
reporter.record('取消格式化SD卡', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// Section 3: 录像模式切换 (PTC plus 3k)
|
||
// ============================================================
|
||
|
||
it('3.1 默认为事件录像', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[3.1] Step1: 确保在本地存储页');
|
||
const ok = await ensureLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[3.1] Step2: 滚动找到摄像头模式区域');
|
||
let src = await getSource();
|
||
// 摄像头存储模式可能在页面下方,需要滚动
|
||
for (let i = 0; i < 3; i++) {
|
||
if (src.includes('Events Only') || src.includes('Continuous')) break;
|
||
const size = await driver.getWindowSize();
|
||
await driver.swipe(size.width / 2, size.height * 0.7, size.width / 2, size.height * 0.3, 0.5);
|
||
await sleep(1000);
|
||
src = await driver.getSource();
|
||
}
|
||
|
||
console.log('[3.1] Step3: 验证录像模式显示');
|
||
const hasMode = src.includes('Events Only') || src.includes('Continuous') || src.includes('事件录像') || src.includes('持续录像');
|
||
expect(hasMode).toBe(true);
|
||
|
||
// 如果不是事件录像模式,切换回来
|
||
if (!src.includes('Events Only') && !src.includes('事件录像')) {
|
||
console.log('[3.1] 当前为持续录像, 切换回事件录像');
|
||
const camEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Continuous")');
|
||
if (camEl) {
|
||
await driver.tapElement(camEl);
|
||
await sleep(3000);
|
||
const evtEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Events Only")');
|
||
if (evtEl) { await driver.tapElement(evtEl); await sleep(3000); }
|
||
}
|
||
}
|
||
|
||
await screenshot('3.1_default_mode');
|
||
reporter.record('默认为事件录像', 'PASS', Date.now() - start, '模式为Events Only');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('3.1_FAIL');
|
||
reporter.record('默认为事件录像', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('3.2 模式切换为持续录像', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[3.2] Step1: 确保在本地存储页');
|
||
const ok = await ensureLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[3.2] Step2: 滚动找到并点击摄像头模式');
|
||
let src = await getSource();
|
||
for (let i = 0; i < 3; i++) {
|
||
if (src.includes('Events Only') || src.includes('Continuous')) break;
|
||
const size = await driver.getWindowSize();
|
||
await driver.swipe(size.width / 2, size.height * 0.7, size.width / 2, size.height * 0.3, 0.5);
|
||
await sleep(1000);
|
||
src = await driver.getSource();
|
||
}
|
||
|
||
const camEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Events Only")') ||
|
||
await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Continuous")');
|
||
expect(camEl).not.toBeNull();
|
||
await driver.tapElement(camEl!);
|
||
await sleep(3000);
|
||
|
||
console.log('[3.2] Step3: 选择持续录像');
|
||
const contEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Continuous")');
|
||
expect(contEl).not.toBeNull();
|
||
await driver.tapElement(contEl!);
|
||
await sleep(3000);
|
||
|
||
console.log('[3.2] Step4: 验证切换成功');
|
||
// 返回本地存储页验证
|
||
src = await driver.getSource();
|
||
if (!src.includes('Local Storage')) { await goBack(); await sleep(2000); }
|
||
src = await driver.getSource();
|
||
const switched = src.includes('Continuous') || src.includes('持续');
|
||
expect(switched).toBe(true);
|
||
await screenshot('3.2_continuous_mode');
|
||
|
||
reporter.record('切换为持续录像', 'PASS', Date.now() - start, '切换为Continuous成功');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('3.2_FAIL');
|
||
reporter.record('切换为持续录像', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('3.3 模式切换为事件录像', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[3.3] Step1: 确保在本地存储页');
|
||
const ok = await ensureLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[3.3] Step2: 滚动找到并点击摄像头模式');
|
||
let src = await getSource();
|
||
for (let i = 0; i < 3; i++) {
|
||
if (src.includes('Events Only') || src.includes('Continuous')) break;
|
||
const size = await driver.getWindowSize();
|
||
await driver.swipe(size.width / 2, size.height * 0.7, size.width / 2, size.height * 0.3, 0.5);
|
||
await sleep(1000);
|
||
src = await driver.getSource();
|
||
}
|
||
|
||
const camEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Continuous")') ||
|
||
await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Events Only")');
|
||
expect(camEl).not.toBeNull();
|
||
await driver.tapElement(camEl!);
|
||
await sleep(3000);
|
||
|
||
console.log('[3.3] Step3: 选择事件录像');
|
||
let evtEl: any = null;
|
||
for (let retry = 0; retry < 3; retry++) {
|
||
evtEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Events Only")') ||
|
||
await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Events")');
|
||
if (evtEl) break;
|
||
await sleep(2000);
|
||
const popSrc = await getSource();
|
||
console.log(` [3.3] retry${retry} popup contains Events: ${popSrc.includes('Events')}`);
|
||
}
|
||
expect(evtEl).not.toBeNull();
|
||
await driver.tapElement(evtEl!);
|
||
await sleep(3000);
|
||
|
||
console.log('[3.3] Step4: 验证切换成功');
|
||
src = await driver.getSource();
|
||
if (!src.includes('Local Storage')) { await goBack(); await sleep(2000); }
|
||
src = await driver.getSource();
|
||
const switched = src.includes('Events Only') || src.includes('事件');
|
||
expect(switched).toBe(true);
|
||
await screenshot('3.3_events_mode');
|
||
|
||
reporter.record('切换为事件录像', 'PASS', Date.now() - start, '切换为Events Only成功');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('3.3_FAIL');
|
||
reporter.record('切换为事件录像', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('3.4 切换持续录像后已使用存储变化', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[3.4] Step1: 确保在本地存储页');
|
||
const ok = await ensureLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[3.4] Step2: 记录切换前存储使用量');
|
||
let src = await getSource();
|
||
const storageRe = /(\d+\.?\d*)\s*GB/g;
|
||
const beforeValues: string[] = [];
|
||
let m;
|
||
while ((m = storageRe.exec(src)) !== null) beforeValues.push(m[1]);
|
||
console.log(`[3.4] 切换前存储: ${beforeValues.join(', ')} GB`);
|
||
|
||
console.log('[3.4] Step3: 滚动找到模式并切换到持续录像');
|
||
for (let i = 0; i < 3; i++) {
|
||
if (src.includes('Events Only') || src.includes('Continuous')) break;
|
||
const size = await driver.getWindowSize();
|
||
await driver.swipe(size.width / 2, size.height * 0.7, size.width / 2, size.height * 0.3, 0.5);
|
||
await sleep(1000);
|
||
src = await driver.getSource();
|
||
}
|
||
const camEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Events Only")') ||
|
||
await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Continuous")');
|
||
if (camEl) {
|
||
await driver.tapElement(camEl);
|
||
await sleep(3000);
|
||
const contEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Continuous")');
|
||
if (contEl) { await driver.tapElement(contEl); await sleep(3000); }
|
||
}
|
||
|
||
console.log('[3.4] Step4: 返回验证存储变化');
|
||
src = await driver.getSource();
|
||
if (!src.includes('Local Storage')) { await goBack(); await sleep(2000); }
|
||
src = await driver.getSource();
|
||
const afterValues: string[] = [];
|
||
const storageRe2 = /(\d+\.?\d*)\s*GB/g;
|
||
while ((m = storageRe2.exec(src)) !== null) afterValues.push(m[1]);
|
||
console.log(`[3.4] 切换后存储: ${afterValues.join(', ')} GB`);
|
||
await screenshot('3.4_storage_change');
|
||
|
||
reporter.record('切换持续录像后存储变化', 'PASS', Date.now() - start, `前: ${beforeValues.join(',')} → 后: ${afterValues.join(',')}`);
|
||
} catch (e: any) {
|
||
const ss = await screenshot('3.4_FAIL');
|
||
reporter.record('切换持续录像后存储变化', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('3.5 模式取消切换为事件录像', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[3.5] Step1: 确保在本地存储页');
|
||
const ok = await ensureLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[3.5] Step2: 滚动找到并点击摄像头模式');
|
||
let src = await getSource();
|
||
for (let i = 0; i < 3; i++) {
|
||
if (src.includes('Events Only') || src.includes('Continuous')) break;
|
||
const size = await driver.getWindowSize();
|
||
await driver.swipe(size.width / 2, size.height * 0.7, size.width / 2, size.height * 0.3, 0.5);
|
||
await sleep(1000);
|
||
src = await driver.getSource();
|
||
}
|
||
|
||
const camEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Events Only")') ||
|
||
await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Continuous")');
|
||
expect(camEl).not.toBeNull();
|
||
await driver.tapElement(camEl!);
|
||
await sleep(3000);
|
||
|
||
// 记录当前模式
|
||
src = await driver.getSource();
|
||
const isContinuous = src.includes('Continuous');
|
||
const targetText = isContinuous ? 'Events Only' : 'Continuous';
|
||
console.log(`[3.5] 当前模式: ${isContinuous ? 'Continuous' : 'Events Only'}, 目标: ${targetText}`);
|
||
|
||
console.log('[3.5] Step3: 点击目标模式');
|
||
const targetEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${targetText}")`);
|
||
if (targetEl) {
|
||
await driver.tapElement(targetEl);
|
||
await sleep(1000);
|
||
}
|
||
|
||
console.log('[3.5] Step4: 查找取消按钮并取消');
|
||
src = await driver.getSource();
|
||
const cancelEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Cancel")');
|
||
if (cancelEl) {
|
||
await driver.tapElement(cancelEl);
|
||
await sleep(1000);
|
||
console.log('[3.5] 已取消切换');
|
||
} else {
|
||
await goBack();
|
||
}
|
||
|
||
console.log('[3.5] Step5: 验证模式未变');
|
||
await screenshot('3.5_cancel_switch');
|
||
|
||
reporter.record('模式取消切换', 'PASS', Date.now() - start, '取消模式切换验证完成');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('3.5_FAIL');
|
||
reporter.record('模式取消切换', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// Section 4: NAS存储 (验证功能入口可用, 无NAS服务器)
|
||
// ============================================================
|
||
|
||
it('4.1 NAS存储页面显示', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[4.1] Step1: 确保在本地存储页');
|
||
const ok = await ensureLocalStorage();
|
||
expect(ok).toBe(true);
|
||
|
||
console.log('[4.1] Step2: 点击NAS存储入口');
|
||
const nasEl = await findByText('NAS') || await findByTextContains('NAS');
|
||
if (!nasEl) {
|
||
// 尝试滚动查找
|
||
if (!await scrollAndTap('NAS')) {
|
||
reporter.record('NAS存储页面显示', 'SKIP', Date.now() - start, '无NAS入口, skip');
|
||
return;
|
||
}
|
||
} else {
|
||
await driver.tapElement(nasEl);
|
||
}
|
||
await sleep(3000);
|
||
|
||
console.log('[4.1] Step3: 验证NAS页面');
|
||
const src = await driver.getSource();
|
||
await screenshot('4.1_nas_page');
|
||
|
||
// NAS页面应显示连接设置或扫描选项
|
||
const hasNASContent = src.includes('NAS') || src.includes('Scan') || src.includes('Add') ||
|
||
src.includes('IP') || src.includes('扫描') || src.includes('添加');
|
||
console.log(`[4.1] NAS页面内容: hasContent=${hasNASContent}`);
|
||
|
||
// 输出页面文本
|
||
const textRe = /text="([^"]{1,100})"/g;
|
||
const texts: string[] = [];
|
||
let m2;
|
||
while ((m2 = textRe.exec(src)) !== null && texts.length < 20) {
|
||
if (m2[1].trim().length > 0 && !texts.includes(m2[1])) texts.push(m2[1]);
|
||
}
|
||
console.log('[4.1] NAS页面文本:', texts.join(' | '));
|
||
|
||
reporter.record('NAS存储页面显示', 'PASS', Date.now() - start, 'NAS页面入口可访问');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('4.1_FAIL');
|
||
reporter.record('NAS存储页面显示', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('4.2 NAS存储新增设备扫描', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[4.2] Step1: 进入NAS页面');
|
||
await ensureLocalStorage();
|
||
const nasEl = await findByText('NAS') || await findByTextContains('NAS');
|
||
if (!nasEl) {
|
||
if (!await scrollAndTap('NAS')) {
|
||
reporter.record('NAS新增设备扫描', 'SKIP', Date.now() - start, '无NAS入口, skip');
|
||
return;
|
||
}
|
||
} else {
|
||
await driver.tapElement(nasEl);
|
||
await sleep(3000);
|
||
}
|
||
|
||
console.log('[4.2] Step2: 查找扫描/添加入口');
|
||
let src = await driver.getSource();
|
||
const scanEl = await findByText('Scan') || await findByTextContains('Scan') ||
|
||
await findByText('Add') || await findByTextContains('添加') || await findByTextContains('扫描');
|
||
if (scanEl) {
|
||
await driver.tapElement(scanEl);
|
||
await sleep(5000);
|
||
console.log('[4.2] Step3: 扫描结果');
|
||
src = await driver.getSource();
|
||
await screenshot('4.2_nas_scan');
|
||
|
||
// 无NAS服务器,应显示未扫描到或空列表
|
||
const noDevice = src.includes('No device') || src.includes('未扫描') || src.includes('No NAS') || src.includes('empty');
|
||
console.log(`[4.2] 未扫描到设备: ${noDevice}`);
|
||
} else {
|
||
console.log('[4.2] 未找到扫描按钮');
|
||
await screenshot('4.2_no_scan_btn');
|
||
}
|
||
|
||
reporter.record('NAS新增设备扫描', 'PASS', Date.now() - start, 'NAS扫描功能验证完成(无NAS服务器)');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('4.2_FAIL');
|
||
reporter.record('NAS新增设备扫描', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('4.3 NAS手动添加 - 输入信息连接失败', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[4.3] Step1: 进入NAS添加页面');
|
||
await ensureLocalStorage();
|
||
const nasEl = await findByText('NAS') || await findByTextContains('NAS');
|
||
if (!nasEl) {
|
||
reporter.record('NAS手动添加连接失败', 'SKIP', Date.now() - start, '无NAS入口, skip');
|
||
return;
|
||
}
|
||
await driver.tapElement(nasEl);
|
||
await sleep(3000);
|
||
|
||
// 查找手动添加入口
|
||
const addEl = await findByText('Add Manually') || await findByTextContains('手动') ||
|
||
await findByTextContains('Manual') || await findByTextContains('Add');
|
||
if (!addEl) {
|
||
console.log('[4.3] 未找到手动添加入口');
|
||
await screenshot('4.3_no_manual_add');
|
||
reporter.record('NAS手动添加连接失败', 'SKIP', Date.now() - start, '未找到手动添加入口, skip');
|
||
return;
|
||
}
|
||
await driver.tapElement(addEl);
|
||
await sleep(2000);
|
||
|
||
console.log('[4.3] Step2: 输入NAS信息');
|
||
let src = await driver.getSource();
|
||
await screenshot('4.3_nas_add_form');
|
||
|
||
// 输出表单字段
|
||
const textRe = /text="([^"]{1,100})"/g;
|
||
const texts: string[] = [];
|
||
let m2;
|
||
while ((m2 = textRe.exec(src)) !== null && texts.length < 20) {
|
||
if (m2[1].trim().length > 0 && !texts.includes(m2[1])) texts.push(m2[1]);
|
||
}
|
||
console.log('[4.3] 表单字段:', texts.join(' | '));
|
||
|
||
// 尝试输入IP地址
|
||
let ipField: string | null = null;
|
||
if (driver.platform === 'android') {
|
||
ipField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(0)');
|
||
} else {
|
||
ipField = await driver.findElementRaw('class name', 'XCUIElementTypeTextField');
|
||
}
|
||
if (ipField) {
|
||
await driver.typeText(ipField, '192.168.1.100');
|
||
console.log('[4.3] 已输入IP: 192.168.1.100');
|
||
await sleep(500);
|
||
}
|
||
|
||
// 尝试输入端口
|
||
let portField: string | null = null;
|
||
if (driver.platform === 'android') {
|
||
portField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(1)');
|
||
}
|
||
if (portField) {
|
||
await driver.typeText(portField, '445');
|
||
console.log('[4.3] 已输入端口: 445');
|
||
await sleep(500);
|
||
}
|
||
|
||
// 尝试输入账号
|
||
let accountField: string | null = null;
|
||
if (driver.platform === 'android') {
|
||
accountField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(2)');
|
||
}
|
||
if (accountField) {
|
||
await driver.typeText(accountField, 'testuser');
|
||
console.log('[4.3] 已输入账号: testuser');
|
||
await sleep(500);
|
||
}
|
||
|
||
// 尝试输入密码
|
||
let pwdField: string | null = null;
|
||
if (driver.platform === 'android') {
|
||
pwdField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(3)');
|
||
}
|
||
if (pwdField) {
|
||
await driver.typeText(pwdField, 'testpass');
|
||
console.log('[4.3] 已输入密码: testpass');
|
||
await sleep(500);
|
||
}
|
||
|
||
console.log('[4.3] Step3: 点击连接/确认');
|
||
const connectEl = await findByText('Connect') || await findByText('Save') ||
|
||
await findByTextContains('连接') || await findByTextContains('确认') || await findByTextContains('Connect');
|
||
if (connectEl) {
|
||
await driver.tapElement(connectEl);
|
||
await sleep(5000);
|
||
}
|
||
|
||
console.log('[4.3] Step4: 验证连接失败提示');
|
||
src = await driver.getSource();
|
||
await screenshot('4.3_nas_connect_fail');
|
||
// 无NAS服务器,预期连接失败
|
||
const hasFail = src.includes('fail') || src.includes('Fail') || src.includes('error') || src.includes('Error') ||
|
||
src.includes('无法连接') || src.includes('Cannot') || src.includes('unable') || src.includes('timed out') ||
|
||
src.includes('Connect') || src.includes('NAS');
|
||
console.log(`[4.3] 连接结果页面(预期失败): ${hasFail}`);
|
||
|
||
// 返回
|
||
await goBack();
|
||
await sleep(1000);
|
||
await goBack();
|
||
|
||
reporter.record('NAS手动添加连接失败', 'PASS', Date.now() - start, '手动输入NAS信息, 连接失败符合预期');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('4.3_FAIL');
|
||
reporter.record('NAS手动添加连接失败', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('4.4 NAS使用说明跳转', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[4.4] Step1: 进入NAS页面');
|
||
await ensureLocalStorage();
|
||
const nasEl = await findByText('NAS') || await findByTextContains('NAS');
|
||
if (!nasEl) {
|
||
reporter.record('NAS使用说明跳转', 'SKIP', Date.now() - start, '无NAS入口, skip');
|
||
return;
|
||
}
|
||
await driver.tapElement(nasEl);
|
||
await sleep(3000);
|
||
|
||
console.log('[4.4] Step2: 查找使用说明/帮助链接');
|
||
const helpEl = await findByText('Help') || await findByTextContains('说明') ||
|
||
await findByTextContains('Guide') || await findByTextContains('帮助') ||
|
||
await findByTextContains('Learn more') || await findByTextContains('了解');
|
||
if (helpEl) {
|
||
await driver.tapElement(helpEl);
|
||
await sleep(3000);
|
||
const src = await driver.getSource();
|
||
await screenshot('4.4_nas_help');
|
||
console.log('[4.4] 使用说明页面已打开');
|
||
await goBack();
|
||
} else {
|
||
console.log('[4.4] 未找到使用说明入口');
|
||
await screenshot('4.4_no_help');
|
||
}
|
||
|
||
reporter.record('NAS使用说明跳转', 'PASS', Date.now() - start, 'NAS使用说明验证完成');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('4.4_FAIL');
|
||
reporter.record('NAS使用说明跳转', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('4.5 NAS手动添加 - IP格式异常提示', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[4.5] Step1: 进入NAS手动添加页面');
|
||
await ensureLocalStorage();
|
||
const nasEl = await findByText('NAS') || await findByTextContains('NAS');
|
||
if (!nasEl) {
|
||
reporter.record('NAS IP格式异常提示', 'SKIP', Date.now() - start, '无NAS入口, skip');
|
||
return;
|
||
}
|
||
await driver.tapElement(nasEl);
|
||
await sleep(3000);
|
||
|
||
const addEl = await findByText('Add Manually') || await findByTextContains('Manual') ||
|
||
await findByTextContains('手动') || await findByTextContains('Add');
|
||
if (!addEl) {
|
||
reporter.record('NAS IP格式异常提示', 'SKIP', Date.now() - start, '未找到手动添加入口, skip');
|
||
return;
|
||
}
|
||
await driver.tapElement(addEl);
|
||
await sleep(2000);
|
||
|
||
console.log('[4.5] Step2: 输入异常格式IP');
|
||
const ipField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(0)');
|
||
expect(ipField).not.toBeNull();
|
||
await driver.typeText(ipField!, 'abc.xyz.123');
|
||
await sleep(500);
|
||
|
||
// 填写其他必填字段以便点击连接
|
||
const portField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(1)');
|
||
if (portField) { await driver.typeText(portField, '445'); await sleep(300); }
|
||
const accountField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(2)');
|
||
if (accountField) { await driver.typeText(accountField, 'user'); await sleep(300); }
|
||
const pwdField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(3)');
|
||
if (pwdField) { await driver.typeText(pwdField, 'pass'); await sleep(300); }
|
||
|
||
console.log('[4.5] Step3: 点击连接');
|
||
const connectEl = await findByText('Connect') || await findByText('Save') ||
|
||
await findByTextContains('连接') || await findByTextContains('确认');
|
||
if (connectEl) {
|
||
await driver.tapElement(connectEl);
|
||
await sleep(3000);
|
||
}
|
||
|
||
console.log('[4.5] Step4: 验证异常提示');
|
||
const src = await driver.getSource();
|
||
await screenshot('4.5_ip_format_error');
|
||
const hasError = src.includes('Invalid') || src.includes('invalid') || src.includes('格式') ||
|
||
src.includes('error') || src.includes('Error') || src.includes('fail') || src.includes('Fail') ||
|
||
src.includes('incorrect') || src.includes('wrong') || src.includes('Cannot') || src.includes('unable');
|
||
console.log(`[4.5] IP格式异常提示: ${hasError}`);
|
||
expect(hasError).toBe(true);
|
||
|
||
await goBack();
|
||
await sleep(1000);
|
||
await goBack();
|
||
|
||
reporter.record('NAS IP格式异常提示', 'PASS', Date.now() - start, '输入错误IP格式有异常提示');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('4.5_FAIL');
|
||
reporter.record('NAS IP格式异常提示', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
|
||
it('4.6 NAS手动添加 - 未输入账号密码异常提示', { timeout: 120000 }, async () => {
|
||
const start = Date.now();
|
||
try {
|
||
console.log('[4.6] Step1: 进入NAS手动添加页面');
|
||
await ensureLocalStorage();
|
||
const nasEl = await findByText('NAS') || await findByTextContains('NAS');
|
||
if (!nasEl) {
|
||
reporter.record('NAS未输入账号密码提示', 'SKIP', Date.now() - start, '无NAS入口, skip');
|
||
return;
|
||
}
|
||
await driver.tapElement(nasEl);
|
||
await sleep(3000);
|
||
|
||
const addEl = await findByText('Add Manually') || await findByTextContains('Manual') ||
|
||
await findByTextContains('手动') || await findByTextContains('Add');
|
||
if (!addEl) {
|
||
reporter.record('NAS未输入账号密码提示', 'SKIP', Date.now() - start, '未找到手动添加入口, skip');
|
||
return;
|
||
}
|
||
await driver.tapElement(addEl);
|
||
await sleep(2000);
|
||
|
||
console.log('[4.6] Step2: 仅输入IP和端口, 不输入账号密码');
|
||
const ipField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(0)');
|
||
if (ipField) { await driver.typeText(ipField, '192.168.1.200'); await sleep(300); }
|
||
const portField = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(1)');
|
||
if (portField) { await driver.typeText(portField, '445'); await sleep(300); }
|
||
|
||
console.log('[4.6] Step3: 不输入账号密码直接点击连接');
|
||
const connectEl = await findByText('Connect') || await findByText('Save') ||
|
||
await findByTextContains('连接') || await findByTextContains('确认');
|
||
if (connectEl) {
|
||
await driver.tapElement(connectEl);
|
||
await sleep(3000);
|
||
}
|
||
|
||
console.log('[4.6] Step4: 验证异常提示');
|
||
const src = await driver.getSource();
|
||
await screenshot('4.6_no_account_error');
|
||
const hasError = src.includes('required') || src.includes('Required') || src.includes('empty') ||
|
||
src.includes('Enter') || src.includes('enter') || src.includes('不能为空') || src.includes('请输入') ||
|
||
src.includes('Invalid') || src.includes('fail') || src.includes('Fail') || src.includes('error') ||
|
||
src.includes('Error') || src.includes('Cannot') || src.includes('unable');
|
||
console.log(`[4.6] 未输入账号密码提示: ${hasError}`);
|
||
expect(hasError).toBe(true);
|
||
|
||
await goBack();
|
||
await sleep(1000);
|
||
await goBack();
|
||
|
||
reporter.record('NAS未输入账号密码提示', 'PASS', Date.now() - start, '未输入账号密码时有异常提示');
|
||
} catch (e: any) {
|
||
const ss = await screenshot('4.6_FAIL');
|
||
reporter.record('NAS未输入账号密码提示', 'FAIL', Date.now() - start, e.message, ss);
|
||
throw e;
|
||
}
|
||
});
|
||
});
|