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 { 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 { return driver.findElementRaw('name', text); } async function findByTextContains(text: string): Promise { return driver.findElementRaw('predicate string', `name CONTAINS "${text}"`); } async function goBack(): Promise { if (driver.platform === 'android') { await (driver as any).goBack(); } else { await driver.tap(39, 70); } await sleep(2000); } async function goBackToHomepage(): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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; } }); });