AI_UIAutomation/tests/aihub/aihub_local_storage.test.ts

1022 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
});
});