|
|
📖 导读:先看真实案例,再看怎么解决
场景一:新闻 App 顶部 Banner 离奇消失
某头部新闻 App 在折叠屏展开时,顶部 Banner 显示正常。折叠手机后,Banner 区域直接被”截断”,只剩半截文字。原因:App 在折叠态重新 layout 时,顶部安全区计算错误,把 Banner 的 topMargin 当成了状态栏高度叠加,导致内容被推出可视区。这是典型的折叠屏布局适配缺陷。
场景二:电商 App 商品卡片重叠
一款日活 300 万的电商 App,在折叠屏展开时商品列表为 3 列布局;折叠后系统切换到”手机模式”,布局需要重新计算为 2 列。但开发者在折叠回调中漏掉了 notifyDataSetChanged,RecyclerView 没有刷新列数,导致卡片宽度计算错误、文字和图片重叠在一起。用户以为是 App 崩溃,实际上是布局异常。
场景三:社交 App 输入框被铰链遮挡
某社交 App 的聊天输入框固定在屏幕底部 60dp 处。折叠屏铰链区域恰好在屏幕中间偏下,折叠态时输入框底部恰好”骑”在铰链上,文字无法点击输入。这是硬件物理特性与软件布局的冲突——没有折叠屏设备的团队根本测不出来。
|
传统测试的困境: 一台折叠屏手机数千元,团队往往只有 1-2 台;人工折叠测试效率低(折叠一次至少 10 秒)、容易漏检(截图对比纯靠肉眼)、无法回归(每次发版都要重复折叠)。 AI 生成脚本自动控制折叠/展开状态 + 截图 → AI 视觉分析自动识别界面异常,一套代码覆盖”展开执行 → 折叠执行 → AI 分析”全链路,任何人拿着脚本在设备上跑一遍就能出报告。 |
本文你能得到什么
-
可直接复制的 Douyin 折叠屏测试 Skill(Android / HarmonyOS 两套脚本) -
用 ADB(Android)和 HDC(HarmonyOS)控制折叠状态的完整代码 -
集成 AI 视觉分析的自动化脚本(脚本跑完 → AI 自动分析 → 输出报告) -
初学者跟着做也能跑通的完整操作步骤
第一章 整体方案设计:AI + 折叠控制 + 视觉分析
折叠屏兼容性测试的核心问题不是”脚本怎么写”,而是如何让脚本自动控制折叠状态并自动判断结果。本文的方案设计围绕三个核心环节展开:
1.1 全流程架构
整个方案分为三个层次,每层解决一个问题:
图片引自微信公众号,扫码关注阅读原文AI 生成脚本 → 折叠状态控制 → 执行截图 → AI 视觉分析,三个环节依次衔接最终输出报告
1.2 工具链说明
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1.3 折叠屏测试的特殊性
折叠屏兼容性测试和普通兼容性测试有三个本质区别:
- 分辨率突变
:折叠态和展开态的分辨率完全不同(常见如展开 2200×1840 vs 折叠 984×2096),布局会重新计算 - 铰链区域
:屏幕中间有物理铰链,内容不能覆盖铰链区域,否则显示异常或触控失灵 - 状态切换延迟
:折叠 / 展开操作后,App 收到状态变更回调需要时间刷新 UI,这个时间窗口内截图可能出现”过渡态”(半渲染状态),需要等待
本文的解决方案正是围绕这三个特殊性设计的:折叠控制通过 ADB / HDC 实现、截图后等待 UI 稳定、AI 分析两次状态下的截图差异。
第二章 AI生成脚本 + Skill设计(使用抖音APP模拟演示)
设计思路和 Skill 构建的完整方法论已在公众号上一篇文章《使用大模型实现APP自动化测试:Android、iOS、HarmonyOS一次搞定》中详细讲解过,本文不再重复。下面直接给出本章配套的折叠屏测试 Skill,把这份内容发给 AI,就能生成可直接运行的折叠屏测试脚本。
|
使用方式:复制下方 Skill 内容,发给通义千问(或其他支持代码生成的大模型),然后发送你的测试需求。 |
2.1 折叠屏测试 Skill(Android)
SKILL.md—折叠屏测试(Android)
# 抖音-折叠屏自动化测试 Skill## 角色定义你是一名资深 Android 自动化测试工程师,精通 Appium + Python,熟悉折叠屏设备特性。## 测试目标验证抖音 APP(com.ss.android.ugc.aweme)在折叠屏展开态和折叠态下的界面兼容性。## APP 信息- 包名:com.ss.android.ugc.aweme- 主 Activity:com.ss.android.ugc.aweme.main.MainActivity- 安装来源:应用市场## 测试步骤(每次折叠状态执行)1. 启动抖音 APP,等待 APP 完全启动(首页视频流出现)2. 在首页搜索框输入文字「APP自动化测试」3. 点击搜索按钮,触发搜索4. 按 Back 键退出搜索页,回到首页5. 退出 APP(Home 键退后台)## 关键要求- 每个步骤后执行全屏截图,截图文件名格式:{fold_state}_step{num}_{timestamp}.png- fold_state:展开态用 `unfold`,折叠态用 `fold`- num:步骤序号 1~4- timestamp:Unix 时间戳(秒)- 截图后等待截图特效消失(screencap 进程结束)再执行下一步- 最后一步(退出 APP)截图后不判断截图特效## 截图等待逻辑```pythonimport subprocess, time, osdef wait_screenshot_ready(screenshot_path, timeout=5):"""等待截图文件写入完成(screencap 进程退出)"""start = time.time()while time.time() - start < timeout:time.sleep(0.3)try:size1 = os.path.getsize(screenshot_path)time.sleep(0.3)size2 = os.path.getsize(screenshot_path)if size1 > 0 and size1 == size2:return True # 文件大小稳定,截图完成except:passreturn False # 超时仍不稳定def take_screenshot(driver, filepath):driver.save_screenshot(filepath)return wait_screenshot_ready(filepath)```## 输出要求生成完整的 Python 脚本(Python 3.9+),使用 Appium Python Client,导出为可独立运行的 .py 文件,包含:- desired_caps 配置(含 appPackage / appActivity)- wait_screenshot_ready() 函数- take_screenshot() 函数- 各步骤操作函数(start_app / search / click_search / exit_search / quit_app)- 主流程:执行展开态 4 步 → 执行折叠态 4 步(折叠态前调用折叠脚本)- 折叠控制由外部提供,这里只负责调用折叠函数
2.2 折叠屏测试 Skill(HarmonyOS)
SKILL.md — Douyin 折叠屏测试(HarmonyOS)
# 抖音-折叠屏自动化测试 Skill(HarmonyOS)## 角色定义你是一名资深 HarmonyOS 自动化测试工程师,精通 Appium + Python,熟悉鸿蒙设备特性和 HDC 命令行工具。## 测试目标验证抖音 APP 在 HarmonyOS 折叠屏展开态和折叠态下的界面兼容性。## APP 信息(注意:HarmonyOS 版抖音包名可能不同)- 包名:com.ss.android.ugc.aweme(与 Android 相同,部分设备可能为鸿蒙定制版,包名不同)- 启动方式:通过 HDC 或 Appium 启动 ability## 折叠控制命令(通过 HDC)```bash# 查询当前折叠状态hdc shell hidumper -s 62 -a '-a' | grep FoldStatus# 切换到折叠态(具体 mode 值需根据设备确认)hdc shell bm setDisplayMode --mode 1# 切换到展开态hdc shell bm setDisplayMode --mode 0# 等待折叠状态切换完成(单位:秒)import time; time.sleep(3)```## 测试步骤(每次折叠状态执行)1. 启动抖音 APP,等待 APP 完全启动2. 在首页搜索框输入文字「APP自动化测试」3. 点击搜索按钮,触发搜索4. 按 Back 键退出搜索页,回到首页5. 退出 APP## 截图要求- 每个步骤后截图,文件名格式:harmony_{fold_state}_step{num}_{timestamp}.png- 截图后等待截图特效消失再执行下一步## 输出要求生成完整的 Python 脚本(Python 3.9+),使用 Appium Python Client,包含 HDC 折叠控制逻辑,折叠函数签名如下:def set_fold_state(state: str): # state = "folded" 或 "unfolded""""通过 HDC 控制 HarmonyOS 设备折叠状态"""
第三章 折叠状态控制:Android与HarmonyOS双方案
折叠状态控制是整个方案的核心技术环节。Android 和 HarmonyOS 采用了完全不同的控制方式,下面分别讲解原理、命令和代码实现。
3.1 Android:ADB + wm size 分辨率模拟方案
3.1.1 原理说明
Android 官方并未提供直接通过 ADB 命令切换折叠状态的接口。各大厂商(华为、三星、OPPO)均有自己的折叠控制方案,但均为厂商私有 API,无法通过通用 ADB 访问。
本文采用的方案是:通过 adb shell wm size 命令动态修改显示分辨率,模拟折叠态和展开态的屏幕尺寸差异。当分辨率切换后,Android 系统会触发 configuration changed 事件,应用收到后重新 layout,从而测试应用在”折叠后分辨率”下的界面表现。
图片引自微信公众号,扫码关注阅读原文| ⚠ 重要说明:分辨率模拟 vs 真实折叠
通过 wm size 修改分辨率可以测试应用在”折叠后分辨率”下的布局表现,但无法模拟真实的铰链区域遮挡和传感器事件。如果你需要测试铰链区域遮挡(如输入框被铰链遮挡的场景),建议使用真实折叠设备,在物理折叠状态下执行测试。分辨率模拟方案适用于验证”布局是否适配折叠后尺寸”类问题。 |
3.1.2 Python 实现
fold_control_android.py — ADB 折叠控制
import subprocessimport timeimport re# ============ 配置区(根据你的设备修改)============ANDROID_DEVICE_ADDR = "192.168.1.100:5555" # 设备地址,真机用 IP,模拟器用 emulator-xxxxORIGINAL_RESOLUTION = "2208x1840" # 展开态分辨率(查 adb shell wm size 获取)FOLDED_RESOLUTION = "984x2096" # 折叠态分辨率(查 adb shell wm size 获取)FOLD_TRANSITION_WAIT = 3 # 折叠状态切换后等待秒数(让系统完成回调)# ==================================================def run_adb(command: str) -> str:"""执行 ADB 命令并返回输出"""full_cmd = f"adb -s {ANDROID_DEVICE_ADDR} {command}"result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=30)return result.stdout.strip()def get_current_resolution() -> str:"""获取当前屏幕分辨率"""output = run_adb("shell wm size")# 输出格式:Physical size: 2208x1840 或 Override size: 984x2096match = re.search(r'Override size:\s*(\d+x\d+)', output)if match:return match.group(1)match = re.search(r'Physical size:\s*(\d+x\d+)', output)if match:return match.group(1)return "unknown"def set_fold_state(state: str) -> bool:"""通过 ADB wm size 控制 Android 设备折叠状态参数:state: "folded"(折叠态)或 "unfolded"(展开态)返回:True=成功,False=失败"""if state == "folded":target_res = FOLDED_RESOLUTIONdisplay_name = "折叠态"elif state == "unfolded":target_res = ORIGINAL_RESOLUTIONdisplay_name = "展开态"else:print(f"[ERROR] 未知状态: {state}")return Falseprint(f"[INFO] 正在切换到 {display_name}(分辨率: {target_res})...")current = get_current_resolution()print(f"[INFO] 当前分辨率: {current}")if current == target_res:print(f"[INFO] 已是 {display_name},跳过切换")return True# 执行分辨率切换result = run_adb(f"shell wm size {target_res}")if "error" in result.lower():print(f"[ERROR] 切换分辨率失败: {result}")return False# 等待系统完成折叠状态回调print(f"[INFO] 等待 {FOLD_TRANSITION_WAIT} 秒,系统完成状态切换...")time.sleep(FOLD_TRANSITION_WAIT)# 验证切换结果new_res = get_current_resolution()if new_res == target_res:print(f"[OK] {display_name} 切换成功,当前分辨率: {new_res}")return Trueelse:print(f"[WARN] 切换完成但分辨率不匹配,期望: {target_res},实际: {new_res}")return True # 不算失败,只是记录def reset_resolution() -> bool:"""恢复原始分辨率"""print("[INFO] 恢复原始分辨率...")result = run_adb("shell wm size reset")time.sleep(2)current = get_current_resolution()print(f"[INFO] 恢复后分辨率: {current}")return Trueif __name__ == "__main__":# 测试折叠状态切换print("=== Android 折叠状态控制测试 ===")print(f"当前分辨率: {get_current_resolution()}")print("\n--- 测试切换到展开态 ---")set_fold_state("unfolded")print("\n--- 测试切换到折叠态 ---")set_fold_state("folded")print("\n--- 恢复原始分辨率 ---")reset_resolution()
3.1.3 Appium 中的分辨率同步
切换分辨率后,需要通知 Appium driver 刷新当前上下文(context),否则 driver 仍使用旧的窗口尺寸。切换分辨率后调用:
# 切换分辨率后,在 Appium Python Client 中刷新 sessiondriver.refresh_caps() # 重新获取设备 capabilitiesdriver.update_settings({}) # 触发 driver 重新计算窗口尺寸# 或者更简单的方式:重启 Appium sessiondriver.quit()driver = webdriver.Remote(APPIUM_SERVER, desired_caps)
3.2 HarmonyOS:HDC + bm setDisplayMode 方案
3.2.1 原理说明
HarmonyOS 提供了较为完善的折叠屏状态管理 API。在测试场景中,我们通过 HDC(HarmonyOS Device Connector)命令行工具直接调用系统服务来控制折叠状态。
HDC 是 HarmonyOS 官方提供的调试工具,功能上对标 Android 的 ADB。HDC 默认随 DevEco Studio 安装,路径在:
-
Windows: DevEco Studio/sdk/default/openharmony/toolchains/hdc.exe -
macOS: DevEco Studio.app/Contents/sdk/default/openharmony/toolchains/hdc
折叠屏状态控制主要通过 bm(Bundle Manager)子命令的 setDisplayMode 参数实现:
图片引自微信公众号,扫码关注阅读原文3.2.2 HarmonyOS 折叠状态枚举说明
HarmonyOS 折叠屏的状态值定义在 ohos.display.FoldStatus 枚举中:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如何确认你设备对应的 mode 值
不同厂商的 HarmonyOS 设备,mode 值映射可能不同。建议先用以下命令查询设备支持的显示模式:hdc shell bm setDisplayMode -h 或 hdc shell bm setDisplayMode --help
如果有输出帮助信息,会列出支持的 mode 值。如果无帮助,先用查询命令确认当前状态,再逐个尝试 mode=0/1/2 等。
3.2.3 Python 实现
fold_control_harmony.py — HDC 折叠控制
import subprocessimport timeimport reimport os# ============ 配置区(根据你的设备修改)============HDC_PATH = r"D:\Huawei\DevEco Studio\sdk\default\openharmony\toolchains\hdc.exe"HARMONY_DEVICE_ID = "" # 留空=单设备自动匹配;多设备填序列号(hdc list targets 查看)FOLD_TRANSITION_WAIT = 3 # 折叠状态切换后等待秒数# ==================================================def run_hdc(command: str) -> str:"""执行 HDC 命令并返回输出"""hdc_exe = HDC_PATHif HARMONY_DEVICE_ID:full_cmd = f'"{hdc_exe}" -t {HARMONY_DEVICE_ID} {command}'else:full_cmd = f'"{hdc_exe}" {command}'result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=30)return result.stdout.strip()def get_fold_status() -> str:"""查询当前折叠状态"""output = run_hdc("shell hidumper -s 62 -a '-a'")# 查找 FoldStatus 相关信息for line in output.splitlines():if "FoldStatus" in line or "foldStatus" in line or "fold_status" in line:return line.strip()return f"unknown (raw: {output[:200]})"def set_fold_state(state: str) -> bool:"""通过 HDC 控制 HarmonyOS 设备折叠状态参数:state: "folded"(折叠态)或 "unfolded"(展开态)或 "half_folded"(悬停态)返回:True=成功,False=失败"""mode_map = {"folded": "1","unfolded": "0","half_folded": "2",}display_name_map = {"folded": "折叠态","unfolded": "展开态","half_folded": "悬停态",}if state not in mode_map:print(f"[ERROR] 未知状态: {state},可用值: folded / unfolded / half_folded")return Falsemode = mode_map[state]display_name = display_name_map[state]print(f"[INFO] 正在切换到 {display_name}(mode={mode})...")current_status = get_fold_status()print(f"[INFO] 当前状态: {current_status}")# 执行折叠状态切换result = run_hdc(f"shell bm setDisplayMode --mode {mode}")print(f"[DEBUG] HDC 输出: {result}")if "error" in result.lower() or "fail" in result.lower():print(f"[WARN] HDC 返回异常: {result},继续等待...")# 等待系统完成折叠状态回调print(f"[INFO] 等待 {FOLD_TRANSITION_WAIT} 秒,系统完成状态切换...")time.sleep(FOLD_TRANSITION_WAIT)# 验证切换结果new_status = get_fold_status()print(f"[OK] 切换后状态: {new_status}")return Truedef reset_to_unfolded() -> bool:"""恢复到展开态(测试结束时调用)"""print("[INFO] 恢复设备到展开态...")return set_fold_state("unfolded")if __name__ == "__main__":# 测试 HDC 连接print("=== HarmonyOS 折叠状态控制测试 ===")devices = run_hdc("list targets")print(f"[INFO] 已连接设备: {devices}")print("\n--- 查询当前折叠状态 ---")print(get_fold_status())print("\n--- 测试切换到展开态 ---")set_fold_state("unfolded")print("\n--- 测试切换到折叠态 ---")set_fold_state("folded")print("\n--- 恢复展开态 ---")reset_to_unfolded()
3.3 截图特效等待逻辑
在折叠状态切换后,Android / HarmonyOS 系统会触发显示模式变更回调,App 收到回调后开始重新渲染 UI。这个渲染过程通常需要 1-3 秒,期间截图可能显示”半渲染状态”(如白屏、布局错乱等)。
为了避免截图到过渡态,本文的脚本在每个截图操作后都等待截图特效消失。核心逻辑:
screenshot_helper.py — 截图特效等待逻辑
import osimport timeimport subprocessdef wait_screenshot_ready(screenshot_path: str, timeout: float = 5.0) -> bool:"""等待截图文件写入完成(screencap 进程退出且文件大小稳定)原理:1. screencap 是异步进程,调用 save_screenshot() 后进程启动2. 进程退出时文件写入完成3. 通过轮询文件大小验证写入稳定性(连续两次大小一致=写入完成)参数:screenshot_path: 截图文件路径timeout: 最大等待秒数返回:True=截图就绪,False=超时"""if not os.path.exists(screenshot_path):print(f"[WARN] 截图文件不存在: {screenshot_path}")return Falsestart = time.time()last_size = -1stable_count = 0while time.time() - start < timeout:time.sleep(0.3)try:current_size = os.path.getsize(screenshot_path)if current_size == last_size and current_size > 0:stable_count += 1if stable_count >= 2: # 连续两次大小一致return Trueelse:stable_count = 0last_size = current_sizeexcept OSError:passprint(f"[WARN] 等待截图就绪超时,文件大小: {os.path.getsize(screenshot_path) if os.path.exists(screenshot_path) else 'N/A'}")return Falsedef take_screenshot(driver, filepath: str, wait_effect: bool = True) -> bool:"""执行截图并等待截图特效消失参数:driver: Appium WebDriver 实例filepath: 截图保存路径wait_effect: 是否等待截图特效(最后一步退出时传 False)返回:True=截图成功,False=失败"""try:driver.save_screenshot(filepath)print(f"[DEBUG] 截图已保存: {filepath}")except Exception as e:\n print(f"[ERROR] 截图失败: {e}")return Falseif wait_effect:return wait_screenshot_ready(filepath)return True
4.1 环境准备清单
开始之前,确认以下环境已就绪:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
python --version |
|
|
|
npm i -g appium |
appium --version |
|
|
|
pip install appium-python-client |
pip show appium-python-client |
|
|
|
|
adb version |
|
|
|
pip install requests |
pip show requests |
|
|
|
|
adb devices |
|
如何开启 USB 调试 折叠屏手机 → 设置 → 关于手机 → 连续点击”版本号”7次进入开发者模式 → 返回设置 → 开发者选项 → 开启”USB 调试”和”USB 调试(安全设置)”(允许通过 USB 调试修改权限)。用数据线连接电脑,在手机上确认”允许 USB 调试”的弹窗 |
4.2 完整 Python 脚本(Android 版)
下面是完整的可运行脚本,复制保存为 foldable_test_android.py,修改配置区的参数后直接运行:
foldable_test_android.py—Android抖音折叠屏测试脚本
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""Android 折叠屏兼容性测试脚本测试 APP:抖音(com.ss.android.ugc.aweme)测试流程:展开态 4 步 → 折叠态 4 步 → AI 视觉分析依赖安装:pip install appium-python-client requests运行方式:python foldable_test_android.py"""import osimport timeimport subprocessimport reimport requestsfrom appium import webdriverfrom appium.options.android import UiAutomator2Optionsfrom appium.webdriver.common.appiumby import AppiumBy# ============ 配置区(根据你的环境修改)============APPIUM_SERVER = "http://127.0.0.1:4723"ANDROID_DEVICE_ADDR = "192.168.1.100:5555" # adb devices 查到的设备地址DOUYIN_PACKAGE = "com.ss.android.ugc.aweme"DOUYIN_ACTIVITY = "com.ss.android.ugc.aweme.main.MainActivity"# 折叠屏分辨率(查 adb shell wm size 获取你的设备实际值)ORIGINAL_RESOLUTION = "2208x1840" # 展开态分辨率FOLDED_RESOLUTION = "984x2096" # 折叠态分辨率# 截图保存目录SCREENSHOT_DIR = r"C:\Users\51362\Desktop\文章\foldable_test_screenshots"os.makedirs(SCREENSHOT_DIR, exist_ok=True)# AI 视觉分析配置(替换为你的 API Key 和接口地址)AI_VISION_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/vision/vision/describe"AI_API_KEY = "your-api-key-here" # 替换为通义千问 API Key# ==================================================# ============ 折叠控制区(ADB + wm size)============def run_adb(command: str) -> str:full_cmd = f"adb -s {ANDROID_DEVICE_ADDR} {command}"result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=30)return result.stdout.strip()def get_current_resolution() -> str:output = run_adb("shell wm size")match = re.search(r'Override size:\s*(\d+x\d+)', output)if match:return match.group(1)match = re.search(r'Physical size:\s*(\d+x\d+)', output)if match:return match.group(1)return "unknown"def set_fold_state(state: str) -> bool:"""通过 ADB wm size 控制折叠状态"""if state == "folded":target = FOLDED_RESOLUTIONelif state == "unfolded":target = ORIGINAL_RESOLUTIONelse:return Falseprint(f"[INFO] 切换到 {state},分辨率: {target}")run_adb(f"shell wm size {target}")time.sleep(3)print(f"[INFO] 当前分辨率: {get_current_resolution()}")return True# ============ 截图特效等待区 =============def wait_screenshot_ready(path: str, timeout: float = 5.0) -> bool:if not os.path.exists(path):return Falsestart = time.time()last_size = -1stable = 0while time.time() - start < timeout:time.sleep(0.3)try:s = os.path.getsize(path)if s == last_size and s > 0:stable += 1if stable >= 2:return Trueelse:stable = 0last_size = sexcept OSError:passreturn Falsedef take_screenshot(driver, filepath: str, wait: bool = True) -> bool:try:driver.save_screenshot(filepath)except Exception as e:\n print(f"[ERROR] 截图失败: {e}")return Falseif wait:return wait_screenshot_ready(filepath)return True# ============ AI 视觉分析区 =============def analyze_image_via_ai(image_path: str, state: str, step: int) -> dict:"""调用通义千问视觉 API 分析截图返回:{"normal": bool, "description": str, "confidence": float}"""if not os.path.exists(image_path):return {"normal": False, "description": f"截图文件不存在: {image_path}", "confidence": 0.0}with open(image_path, "rb") as f:\n image_data = f.read()\n\n import base64\n image_base64 = base64.b64encode(image_data).decode("utf-8")prompt = ("你是一位专业的 APP 兼容性测试工程师。请仔细分析这张截图,""判断抖音 APP 当前界面是否正常。注意检查以下几点:\n""1. 布局是否正常(元素有无重叠、截断、错位)\n""2. 文字是否清晰可读(无乱码、无遮挡)\n""3. 图片是否正常显示(无缺失、无花屏)\n""4. 整体视觉是否协调(无异常留白、无元素异常放大或缩小)\n\n""请用 JSON 格式返回分析结果:\n"'{"normal": true/false, "description": "界面描述", ''"issues": ["问题1", "问题2"], "confidence": 0.0~1.0}')headers = {"Authorization": f"Bearer {AI_API_KEY}","Content-Type": "application/json",}payload = {"model": "qwen-vl-plus","input": {"messages": [{"role": "user","content": [{"image": f"data:image/png;base64,{image_base64}"},{"text": prompt}]}]}}try:resp = requests.post(AI_VISION_API_URL, headers=headers, json=payload, timeout=30)if resp.status_code == 200:result = resp.json()# 解析返回内容(通义千问格式)content = result.get("output", {}).get("choices", [{}])[0].get("message", {}).get("content", "")# 尝试解析 JSONimport jsonfor line in content.splitlines():try:parsed = json.loads(line)return parsedexcept:passreturn {"normal": True, "description": content[:200], "confidence": 0.8}else:return {"normal": False, "description": f"API 调用失败: {resp.status_code}", "confidence": 0.0}except Exception as e:\n return {"normal": False, "description": f"AI 分析异常: {str(e)}", "confidence": 0.0}def analyze_all_screenshots():"""分析所有截图,生成报告"""print("\n" + "="*60)print("开始 AI 视觉分析...")print("="*60)all_results = []for filename in sorted(os.listdir(SCREENSHOT_DIR)):if filename.endswith(".png"):filepath = os.path.join(SCREENSHOT_DIR, filename)# 从文件名解析状态和步骤parts = filename.replace(".png", "").split("_")state = parts[0] if parts else "unknown"step = parts[1] if len(parts) > 1 else "?"print(f"\n[分析] {filename}({state} 态 第 {step} 步)...")result = analyze_image_via_ai(filepath, state, step)all_results.append({"filename": filename, "state": state, "step": step, **result})status = "✅ 正常" if result.get("normal") else "⚠️ 异常"print(f"[结果] {status} | 置信度: {result.get('confidence', 0):.2f}")print(f"[描述] {result.get('description', '')[:100]}")# 输出汇总报告print("\n" + "="*60)print("📊 折叠屏兼容性测试报告")print("="*60)total = len(all_results)abnormal = sum(1 for r in all_results if not r.get("normal"))print(f"总截图数: {total}")print(f"正常: {total - abnormal} | 异常: {abnormal}")print(f"\n{'='*60}")for r in all_results:icon = "✅" if r.get("normal") else "⚠️"print(f"{icon} [{r['state']} 态 步骤{r['step']}] {r['filename']}")if r.get("issues"):for issue in r["issues"]:print(f" 问题: {issue}")print(f" 描述: {r.get('description', '')[:80]}")print(f"{'='*60}")# 保存报告到文件report_path = os.path.join(SCREENSHOT_DIR, "test_report.txt")with open(report_path, "w", encoding="utf-8") as f:\n f.write("折叠屏兼容性测试报告\n")f.write(f"测试时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")f.write(f"测试应用: 抖音 {DOUYIN_PACKAGE}\n")f.write(f"总截图数: {total} | 正常: {total-abnormal} | 异常: {abnormal}\n")f.write("="*60 + "\n")for r in all_results:f.write(f"[{'✅' if r.get('normal') else '⚠️'}] {r['filename']}\n")f.write(f" 状态: {r['state']} 态 步骤{r['step']}\n")f.write(f" 描述: {r.get('description', '')}\n")if r.get("issues"):for issue in r["issues"]:f.write(f" 问题: {issue}\n")f.write("\n")print(f"\n报告已保存: {report_path}")return all_results# ============ Appium 操作区(抖音四步操作)============def init_driver(state: str):"""初始化 Appium Driver"""options = UiAutomator2Options()options.platform_name = "Android"options.device_name = ANDROID_DEVICE_ADDRoptions.app_package = DOUYIN_PACKAGEoptions.app_activity = DOUYIN_ACTIVITYoptions.no_reset = True# 强制刷新当前窗口尺寸(分辨率切换后需要)options.new_command_timeout = 120return webdriver.Remote(APPIUM_SERVER, options=options)def wait_for_element(driver, by, value, timeout=15):"""等待元素出现"""from selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as ECtry:el = WebDriverWait(driver, timeout).until(EC.presence_of_element_located((by, value)))return elexcept:return Nonedef execute_douyin_steps(driver, fold_state: str):"""执行抖音四步操作:打开 → 搜索 → 点击搜索 → 退出每步截图,折叠态前执行折叠控制"""ts = int(time.time())results = []# 步骤 1:启动抖音,等待首页出现print(f"\n[步骤 1/4] 启动抖音({fold_state} 态)...")driver.launch_app()time.sleep(3) # 等待 APP 完全启动filepath = os.path.join(SCREENSHOT_DIR, f"{fold_state}_step1_{ts}.png")ok = take_screenshot(driver, filepath)results.append(("step1_launch", filepath, ok))print(f"[截图] {'✅' if ok else '❌'} {filepath}")# 步骤 2:点击搜索框并输入文字print("[步骤 2/4] 点击搜索框并输入「APP自动化测试」...")# 抖音首页搜索框通常在顶部,可以用 Accessibility ID 或 XPath 定位# 以下为常见定位方式,根据实际版本可能需要调整search_selectors = [(AppiumBy.ID, "com.ss.android.ugc.aweme:id/search_view"),(AppiumBy.ID, "com.ss.android.ugc.aweme:id/aweme_search_bar"),(AppiumBy.XPATH, "//*[@content-desc='搜索']"),(AppiumBy.XPATH, "//android.widget.TextView[contains(@text,'搜索')]"),]search_box = Nonefor by, val in search_selectors:try:search_box = wait_for_element(driver, by, val, timeout=5)if search_box:search_box.click()print(f"[INFO] 使用定位器 {by}:{val} 找到搜索框")breakexcept:passif search_box is None:print("[WARN] 未找到搜索框,跳过步骤 2")else:time.sleep(1)# 输入搜索文字search_input_selectors = [(AppiumBy.ID, "com.ss.android.ugc.aweme:id/aweme_search_input_edit_text"),(AppiumBy.CLASS_NAME, "android.widget.EditText"),(AppiumBy.XPATH, "//android.widget.EditText[@focusable='true']"),]for by, val in search_input_selectors:try:inp = wait_for_element(driver, by, val, timeout=3)if inp:inp.send_keys("APP自动化测试")print("[INFO] 输入文字成功")breakexcept:passtime.sleep(1)filepath = os.path.join(SCREENSHOT_DIR, f"{fold_state}_step2_input_{ts}.png")ok = take_screenshot(driver, filepath)results.append(("step2_input", filepath, ok))print(f"[截图] {'✅' if ok else '❌'} {filepath}")# 步骤 3:点击搜索按钮print("[步骤 3/4] 点击搜索按钮...")search_btn_selectors = [(AppiumBy.ID, "com.ss.android.ugc.aweme:id/aweme_search_confirm_btn"),(AppiumBy.XPATH, "//*[@text='搜索' and @clickable='true']"),(AppiumBy.XPATH, "//android.widget.Button[contains(@text,'搜索')]"),]search_btn = Nonefor by, val in search_btn_selectors:try:search_btn = wait_for_element(driver, by, val, timeout=3)if search_btn:search_btn.click()print(f"[INFO] 使用 {by}:{val} 点击搜索按钮")breakexcept:passtime.sleep(2)filepath = os.path.join(SCREENSHOT_DIR, f"{fold_state}_step3_search_{ts}.png")ok = take_screenshot(driver, filepath)results.append(("step3_search", filepath, ok))print(f"[截图] {'✅' if ok else '❌'} {filepath}")# 步骤 4:按 Back 键退出搜索,回到首页print("[步骤 4/4] 按 Back 键退出搜索...")driver.back()time.sleep(2)# 最后一步退出 APP 后截图,不等待截图特效filepath = os.path.join(SCREENSHOT_DIR, f"{fold_state}_step4_home_{ts}.png")ok = take_screenshot(driver, filepath, wait=False)results.append(("step4_home", filepath, ok))print(f"[截图] {'✅' if ok else '❌'} {filepath}")# 退出 APPdriver.terminate_app(DOUYIN_PACKAGE)print("[INFO] APP 已退出")return results# ============ 主流程 =============def main():print("="*60)print("Android 折叠屏兼容性测试")print(f"测试应用: 抖音 {DOUYIN_PACKAGE}")print(f"截图目录: {SCREENSHOT_DIR}")print("="*60)# 第一轮:展开态print("\n========== 第一轮:展开态测试 ==========")set_fold_state("unfolded")time.sleep(2)driver = init_driver("unfolded")try:execute_douyin_steps(driver, "unfold")finally:driver.quit()# 第二轮:折叠态print("\n========== 第二轮:折叠态测试 ==========")set_fold_state("folded")time.sleep(3) # 等待折叠完成 + 系统回调driver = init_driver("folded")try:execute_douyin_steps(driver, "fold")finally:driver.quit()# 恢复展开态print("\n========== 恢复设备到展开态 ==========")set_fold_state("unfolded")# AI 视觉分析analyze_all_screenshots()print("\n[完成] 全部测试已完成,报告见上方输出。")if __name__ == "__main__":main()
图片引自微信公众号,扫码关注阅读原文Appium 抖音操作 + 截图流程
4.3 运行步骤说明
保存脚本后,按以下步骤运行:
- 启动 Appium Server
:命令行执行 appium(或 Appium Desktop 启动服务) - 连接设备
:确保折叠屏手机已通过 USB 连接电脑,USB 调试已开启,执行 adb devices确认设备在线 - 修改配置区
:将 ANDROID_DEVICE_ADDR改为你的设备地址,ORIGINAL_RESOLUTION和FOLDED_RESOLUTION改为你的设备实际分辨率(执行adb shell wm size查到) - 运行脚本
: python foldable_test_android.py - 查看结果
:脚本执行完毕后,自动弹出 AI 分析报告;截图保存在配置区指定的目录中
|
定位器失效怎么办 抖音版本更新后,元素定位器(ID、XPath)可能变化。遇到”未找到元素”时,用 Appium Inspector 重新抓取当前版本的元素信息:启动 Appium Desktop → 点击 Start Inspector Session → 填写 desired_caps → 填入抖音的包名和 Activity → 点击 Start Session。抓取后用新的 ID 替换脚本中的 |
HarmonyOS 版脚本的整体结构与 Android 版一致,主要差异在于三个方面:折叠控制命令(HDC vs ADB)、设备连接方式(HDC 端口 vs ADB 地址)、APP 启动方式(bundle name vs package name)。本节重点讲解这些差异点,并给出完整脚本。
5.1 三大差异点详解
图片引自微信公众号,扫码关注阅读原文Android 与 HarmonyOS 折叠屏测试的三大差异点。核心差异在折叠控制命令和设备连接方式
5.2 完整 Python 脚本(HarmonyOS 版)
foldable_test_harmony.py — HarmonyOS 抖音折叠屏测试脚本
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""HarmonyOS 折叠屏兼容性测试脚本测试 APP:抖音测试流程:展开态 4 步 → 折叠态 4 步 → AI 视觉分析依赖安装:pip install appium-python-client requests运行方式:python foldable_test_harmony.py注意:HarmonyOS 版需要提前配置 HDC 环境变量,或修改 HDC_PATH 指向 hdc.exe 的实际路径"""import osimport timeimport subprocessimport reimport requestsfrom appium import webdriverfrom appium.options.android import UiAutomator2Optionsfrom appium.webdriver.common.appiumby import AppiumBy# ============ 配置区(根据你的环境修改)============APPIUM_SERVER = "http://127.0.0.1:4723"# HDC 配置HDC_PATH = r"D:\Huawei\DevEco Studio\sdk\default\openharmony\toolchains\hdc.exe"HARMONY_DEVICE_ID = "" # 留空=单设备自动匹配;多设备填 hdc list targets 返回的 ID# 抖音 APP 信息(HarmonyOS 版包名可能与 Android 不同)DOUYIN_BUNDLE_NAME = "com.ss.android.ugc.aweme"DOUYIN_ABILITY_NAME = "EntryAbility" # 鸿蒙 Ability 入口名称# 截图保存目录SCREENSHOT_DIR = r"C:\Users\51362\Desktop\文章\foldable_test_screenshots"os.makedirs(SCREENSHOT_DIR, exist_ok=True)# AI 视觉分析配置AI_VISION_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/vision/vision/describe"AI_API_KEY = "your-api-key-here"# ==================================================# ============ HDC 折叠控制区 =============def run_hdc(command: str) -> str:hdc = HDC_PATHif HARMONY_DEVICE_ID:full = f'"{hdc}" -t {HARMONY_DEVICE_ID} {command}'else:full = f'"{hdc}" {command}'result = subprocess.run(full, shell=True, capture_output=True, text=True, timeout=30)return result.stdout.strip()def get_fold_status() -> str:output = run_hdc("shell hidumper -s 62 -a '-a'")for line in output.splitlines():if "FoldStatus" in line or "foldStatus" in line:return line.strip()return f"unknown (raw: {output[:200]})"def set_fold_state(state: str) -> bool:"""通过 HDC 控制 HarmonyOS 折叠状态"""mode_map = {"folded": "1", "unfolded": "0", "half_folded": "2"}name_map = {"folded": "折叠态", "unfolded": "展开态", "half_folded": "悬停态"}if state not in mode_map:return Falseprint(f"[INFO] 切换到 {name_map[state]}(mode={mode_map[state]})...")print(f"[INFO] 当前状态: {get_fold_status()}")result = run_hdc(f"shell bm setDisplayMode --mode {mode_map[state]}")print(f"[DEBUG] HDC: {result}")time.sleep(3)print(f"[INFO] 切换后状态: {get_fold_status()}")return True# ============ 截图等待 + AI 分析区(与 Android 版相同)============def wait_screenshot_ready(path: str, timeout: float = 5.0) -> bool:if not os.path.exists(path):return Falsestart = time.time()last = -1stable = 0while time.time() - start < timeout:time.sleep(0.3)try:s = os.path.getsize(path)if s == last and s > 0:stable += 1if stable >= 2:return Trueelse:stable = 0last = sexcept OSError:passreturn Falsedef take_screenshot(driver, filepath: str, wait: bool = True) -> bool:try:driver.save_screenshot(filepath)except Exception as e:\n print(f"[ERROR] 截图失败: {e}")return Falseif wait:return wait_screenshot_ready(filepath)return Truedef analyze_image_via_ai(image_path: str, state: str, step: str) -> dict:"""调用 AI 视觉分析(与 Android 版相同,详见第六章)"""# 此处省略实现,与 foldable_test_android.py 中的 analyze_image_via_ai 完全相同# 完整实现请参考第六章 AI 视觉分析代码return {"normal": True, "description": "AI 分析中...", "confidence": 0.5, "issues": []}def analyze_all_screenshots():print("\n" + "="*60)print("开始 AI 视觉分析...")print("="*60)all_results = []for filename in sorted(os.listdir(SCREENSHOT_DIR)):if filename.endswith(".png") and filename.startswith("harmony"):filepath = os.path.join(SCREENSHOT_DIR, filename)parts = filename.replace(".png", "").split("_")state = parts[1] if len(parts) > 1 else "?"step = parts[2] if len(parts) > 2 else "?"print(f"\n[分析] {filename}...")result = analyze_image_via_ai(filepath, state, step)all_results.append({"filename": filename, "state": state, "step": step, **result})print(f"[结果] {'✅ 正常' if result.get('normal') else '⚠️ 异常'} | {result.get('description','')[:80]}")print("\n" + "="*60)print("📊 测试报告汇总")print("="*60)total = len(all_results)abnormal = sum(1 for r in all_results if not r.get("normal"))print(f"总截图数: {total} | 正常: {total-abnormal} | 异常: {abnormal}")for r in all_results:print(f"{'✅' if r.get('normal') else '⚠️'} [{r['state']} {r['step']}] {r['filename']}")# ============ Appium 操作区(抖音四步操作)============def init_driver():options = UiAutomator2Options()options.platform_name = "Android" # HarmonyOS 兼容 Android 驱动options.app_package = DOUYIN_BUNDLE_NAME# HarmonyOS 启动 Abilityoptions.app_activity = f".{DOUYIN_ABILITY_NAME}"options.no_reset = Trueoptions.new_command_timeout = 120return webdriver.Remote(APPIUM_SERVER, options=options)def execute_douyin_steps(driver, fold_state: str):"""执行抖音四步操作(HarmonyOS 版本)"""ts = int(time.time())prefix = "harmony" # 截图文件名加 harmony_ 前缀区分平台# 步骤 1:启动 APPprint(f"\n[步骤 1/4] 启动抖音({fold_state} 态)...")driver.launch_app()time.sleep(4) # HarmonyOS 启动通常比 Android 稍慢filepath = os.path.join(SCREENSHOT_DIR, f"{prefix}_{fold_state}_step1_{ts}.png")ok = take_screenshot(driver, filepath)print(f"[截图] {'✅' if ok else '❌'} {filepath}")# 步骤 2:输入搜索文字print("[步骤 2/4] 输入「APP自动化测试」...")time.sleep(2)# HarmonyOS 抖音的搜索框定位(可能与 Android 不同,根据实际版本调整)search_selectors = [(AppiumBy.XPATH, "//*[contains(@text,'搜索')]"),(AppiumBy.ID, f"{DOUYIN_BUNDLE_NAME}:id/search_view"),(AppiumBy.ACCESSIBILITY_ID, "搜索"),]search_box = Nonefor by, val in search_selectors:try:el = driver.find_element(by, val, timeout=3)if el:el.click()search_box = elprint(f"[INFO] 搜索框定位成功: {by}:{val}")breakexcept:passif search_box:time.sleep(1)# 输入文字input_selectors = [(AppiumBy.CLASS_NAME, "android.widget.EditText"),(AppiumBy.XPATH, "//android.widget.EditText"),]for by, val in input_selectors:try:inp = driver.find_element(by, val, timeout=2)if inp:inp.send_keys("APP自动化测试")print("[INFO] 输入文字成功")breakexcept:passtime.sleep(1)filepath = os.path.join(SCREENSHOT_DIR, f"{prefix}_{fold_state}_step2_{ts}.png")ok = take_screenshot(driver, filepath)print(f"[截图] {'✅' if ok else '❌'} {filepath}")# 步骤 3:点击搜索print("[步骤 3/4] 点击搜索按钮...")btn_selectors = [(AppiumBy.XPATH, "//*[contains(@text,'搜索') and @clickable='true']"),(AppiumBy.ID, f"{DOUYIN_BUNDLE_NAME}:id/search_confirm_btn"),]for by, val in btn_selectors:try:btn = driver.find_element(by, val, timeout=3)if btn:btn.click()print(f"[INFO] 点击搜索: {by}:{val}")breakexcept:passtime.sleep(2)filepath = os.path.join(SCREENSHOT_DIR, f"{prefix}_{fold_state}_step3_{ts}.png")ok = take_screenshot(driver, filepath)print(f"[截图] {'✅' if ok else '❌'} {filepath}")# 步骤 4:返回print("[步骤 4/4] 按 Back 返回...")driver.back()time.sleep(2)filepath = os.path.join(SCREENSHOT_DIR, f"{prefix}_{fold_state}_step4_{ts}.png")ok = take_screenshot(driver, filepath, wait=False)print(f"[截图] {'✅' if ok else '❌'} {filepath}")driver.terminate_app(DOUYIN_BUNDLE_NAME)print("[INFO] APP 已退出")# ============ 主流程 =============def main():print("="*60)print("HarmonyOS 折叠屏兼容性测试")print(f"测试应用: 抖音 {DOUYIN_BUNDLE_NAME}")print("="*60)# 第一轮:展开态print("\n========== 第一轮:展开态测试 ==========")set_fold_state("unfolded")time.sleep(2)driver = init_driver()try:execute_douyin_steps(driver, "unfold")finally:driver.quit()# 第二轮:折叠态print("\n========== 第二轮:折叠态测试 ==========")set_fold_state("folded")time.sleep(4) # HarmonyOS 折叠回调比 Android 稍慢driver = init_driver()try:execute_douyin_steps(driver, "fold")finally:driver.quit()# 恢复展开态print("\n========== 恢复设备到展开态 ==========")set_fold_state("unfolded")# AI 视觉分析analyze_all_screenshots()print("\n[完成] 全部测试已完成。")if __name__ == "__main__":main()
5.3 HDC 连接问题排查
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
hdc kill-server && hdc start-server 重启服务 |
|
|
|
|
|
|
|
|
AI 视觉分析是整个方案的最后一环,也是价值最高的一环——把”人眼对比截图”这件事自动化。脚本执行完两轮测试后,8 张截图(4 步 × 2 状态)已保存在本地目录,AI 分析代码自动接续执行,逐张分析并输出异常报告。
6.1 AI 分析 Prompt 设计
Prompt 的设计决定了 AI 能否准确识别界面异常。以下是优化后的分析 Prompt:
AI 分析 Prompt(通义千问视觉 API)
ANALYZE_PROMPT = """你是一位专业的 APP 兼容性测试工程师。请仔细分析这张截图,判断抖音 APP 当前界面是否正常。请检查以下维度:1. 布局异常:元素有无重叠、截断、错位、缺失2. 文字异常:有无乱码、文字被遮挡、文字异常放大或缩小3. 图片异常:图片是否正常显示(无缺失、无花屏、无拉伸变形)4. 铰链区域:折痕处是否有内容异常(被遮挡、被拉伸)5. 整体协调:整体视觉是否协调(无异常留白、无元素异常重叠)折叠屏特别关注:- 分辨率变化后,布局是否重新计算并正确显示- 底部导航栏是否被铰链区域遮挡- 图片和视频流在折叠态下是否自适应缩放请用以下 JSON 格式返回分析结果(只返回 JSON,不要有其他内容):{"normal": true或false,"description": "一句话描述当前界面状态","issues": ["具体问题1", "具体问题2"],"confidence": 0.0到1.0之间的小数,"fold_state": "展开态"或"折叠态","step": "步骤编号"}"""
6.2 完整 AI 分析代码
图片引自微信公众号,扫码关注阅读原文|
|
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""AI 视觉分析模块功能:分析折叠屏测试生成的截图,输出异常报告支持模型:通义千问(qwen-vl-plus)、OpenAI GPT-4V 等用法:1. 直接运行:python ai_vision_analyzer.py2. 被导入:from ai_vision_analyzer import analyze_all_screenshots"""import osimport jsonimport timeimport base64import requests# ============ 配置区 =============AI_VISION_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/vision/vision/describe"AI_API_KEY = "your-api-key-here" # 通义千问 API KeyAI_MODEL = "qwen-vl-plus" # 模型名称(通义千问 / gpt-4-vision-preview)SCREENSHOT_DIR = r"C:\Users\51362\Desktop\文章\foldable_test_screenshots"REPORT_PATH = os.path.join(SCREENSHOT_DIR, "test_report.txt")# =================================ANALYZE_PROMPT = """你是一位专业的 APP 兼容性测试工程师。请仔细分析这张截图,判断抖音 APP 当前界面是否正常。请检查以下维度:1. 布局异常:元素有无重叠、截断、错位、缺失2. 文字异常:有无乱码、文字被遮挡、文字异常放大或缩小3. 图片异常:图片是否正常显示(无缺失、无花屏、无拉伸变形)4. 铰链区域:折痕处是否有内容异常(被遮挡、被拉伸)5. 整体协调:整体视觉是否协调(无异常留白、无元素异常重叠)折叠屏特别关注:- 分辨率变化后,布局是否重新计算并正确显示- 底部导航栏是否被铰链区域遮挡- 图片和视频流在折叠态下是否自适应缩放请用以下 JSON 格式返回分析结果(只返回 JSON,不要有其他内容):{"normal": true或false,"description": "一句话描述当前界面状态","issues": ["具体问题1", "具体问题2"],"confidence": 0.0到1.0之间的小数,"fold_state": "展开态"或"折叠态","step": "步骤编号"}"""def analyze_single_image(image_path: str, api_url: str, api_key: str, model: str) -> dict:"""调用视觉大模型分析单张截图参数:image_path: 截图文件路径api_url: API 接口地址api_key: API Keymodel: 模型名称返回:dict: 包含 normal/description/issues/confidence/fold_state/step"""if not os.path.exists(image_path):return {"normal": False, "description": f"文件不存在: {image_path}","confidence": 0.0, "issues": ["文件缺失"], "fold_state": "?", "step": "?"}with open(image_path, "rb") as f:\n image_data = f.read()\n image_base64 = base64.b64encode(image_data).decode("utf-8")headers = {"Authorization": f"Bearer {api_key}","Content-Type": "application/json",}# 通义千问视觉 API 格式payload = {"model": model,"input": {"messages": [{"role": "user","content": [{"image": f"data:image/png;base64,{image_base64}"},{"text": ANALYZE_PROMPT}]}]}}try:resp = requests.post(api_url, headers=headers, json=payload, timeout=60)if resp.status_code == 200:result = resp.json()# 通义千问返回格式:output.choices[0].message.contentcontent = (result.get("output", {}).get("choices", [{}])[0].get("message", {}).get("content", ""))# 尝试从 content 中提取 JSONfor segment in content.splitlines():segment = segment.strip()if segment.startswith("{") and segment.endswith("}"):try:parsed = json.loads(segment)return parsedexcept json.JSONDecodeError:passreturn {"normal": True, "description": content[:300],"confidence": 0.8, "issues": [], "fold_state": "?", "step": "?"}else:return {"normal": False, "description": f"API 错误 {resp.status_code}: {resp.text[:100]}","confidence": 0.0, "issues": ["API 调用失败"], "fold_state": "?", "step": "?"}except requests.exceptions.Timeout:return {"normal": False, "description": "API 调用超时","confidence": 0.0, "issues": ["超时"], "fold_state": "?", "step": "?"}except Exception as e:\n return {"normal": False, "description": f"异常: {str(e)}","confidence": 0.0, "issues": [str(e)], "fold_state": "?", "step": "?"}def parse_filename_info(filename: str) -> dict:"""从截图文件名解析状态和步骤信息"""# 格式: unfold_step1_1234567890.png 或 harmony_fold_step2_1234567890.pngname = filename.replace(".png", "")parts = name.split("_")if "harmony" in parts:idx = parts.index("harmony")state = parts[idx + 1] if idx + 1 < len(parts) else "?"step = parts[idx + 2] if idx + 2 < len(parts) else "?"else:state = parts[0] if parts else "?"step = parts[1] if len(parts) > 1 else "?"return {"fold_state": state, "step": step}def analyze_all_screenshots(screenshot_dir: str = None, api_url: str = None,api_key: str = None, model: str = None) -> list:"""分析 screenshots 目录下所有截图,生成异常报告参数:screenshot_dir: 截图目录(默认使用配置区 SCREENSHOT_DIR)api_url: API 地址(默认使用配置区 AI_VISION_API_URL)api_key: API Key(默认使用配置区 AI_API_KEY)model: 模型名称(默认使用配置区 AI_MODEL)返回:list: 所有截图的分析结果"""screenshot_dir = screenshot_dir or SCREENSHOT_DIRapi_url = api_url or AI_VISION_API_URLapi_key = api_key or AI_API_KEYmodel = model or AI_MODELif not os.path.exists(screenshot_dir):print(f"[ERROR] 截图目录不存在: {screenshot_dir}")return []print("=" * 60)print("AI 视觉分析开始")print(f"截图目录: {screenshot_dir}")print(f"模型: {model}")print("=" * 60)all_results = []png_files = sorted([f for f in os.listdir(screenshot_dir) if f.endswith(".png")])if not png_files:print("[WARN] 目录中未找到任何截图文件")return []print(f"找到 {len(png_files)} 张截图,开始分析...\n")for i, filename in enumerate(png_files, 1):filepath = os.path.join(screenshot_dir, filename)info = parse_filename_info(filename)print(f"[{i}/{len(png_files)}] 分析: {filename} ({info['fold_state']} 态 第 {info['step']} 步)...")result = analyze_single_image(filepath, api_url, api_key, model)result["filename"] = filenameresult.update(info)all_results.append(result)icon = "✅" if result.get("normal") else "⚠️"conf = result.get("confidence", 0)desc = result.get("description", "")[:80]print(f" {icon} 正常={result.get('normal')} | 置信度={conf:.2f}")print(f" 描述: {desc}")if result.get("issues"):for issue in result["issues"]:print(f" ⚠️ 问题: {issue}")print()# 控制调用频率(通义千问限速)if i < len(png_files):time.sleep(1)# 汇总报告total = len(all_results)abnormal = sum(1 for r in all_results if not r.get("normal"))normal = total - abnormalprint("=" * 60)print(f"📊 折叠屏兼容性测试报告")print("=" * 60)print(f"测试时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")print(f"截图总数: {total} | 正常: {normal} | 异常: {abnormal}")print(f"异常率: {abnormal/total*100:.1f}%" if total > 0 else "")print("-" * 60)for r in all_results:icon = "✅" if r.get("normal") else "⚠️"state_ch = "展开" if r.get("fold_state") == "unfold" else "折叠" if r.get("fold_state") == "fold" else r.get("fold_state", "?")print(f"{icon} [{state_ch}态 步骤{r.get('step','?')}] {r['filename']}")print(f" 置信度: {r.get('confidence', 0):.2f} | {r.get('description','')[:60]}")if r.get("issues"):for issue in r["issues"]:print(f" ⚠️ {issue}")print("=" * 60)# 保存报告文件report_path = REPORT_PATHwith open(report_path, "w", encoding="utf-8") as f:\n f.write("折叠屏兼容性测试报告\n")f.write(f"生成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")f.write(f"截图目录: {screenshot_dir}\n")f.write(f"模型: {model}\n")f.write(f"总截图数: {total} | 正常: {normal} | 异常: {abnormal}\n")f.write("=" * 60 + "\n\n")for r in all_results:icon = "✅" if r.get("normal") else "⚠️"state_ch = "展开" if r.get("fold_state") == "unfold" else "折叠"f.write(f"{icon} [{state_ch}态 步骤{r.get('step','?')}] {r['filename']}\n")f.write(f" 置信度: {r.get('confidence', 0):.2f}\n")f.write(f" 描述: {r.get('description', '')}\n")if r.get("issues"):for issue in r["issues"]:f.write(f" 问题: {issue}\n")f.write("\n")f.write("\n" + "=" * 60 + "\n")f.write("异常截图清单:\n")for r in all_results:if not r.get("normal"):state_ch = "展开" if r.get("fold_state") == "unfold" else "折叠"f.write(f" ⚠️ {r['filename']}({state_ch}态 第{r.get('step','?')}步)\n")print(f"\n📄 报告已保存: {report_path}")return all_resultsif __name__ == "__main__":# 直接运行:分析 screenshots 目录中的所有截图analyze_all_screenshots()
第七章 完整脚本:Android+HarmonyOS双版本+AI分析全流程
为了方便读者直接使用,本章把前面各章的代码整合成两个完整的独立文件,并给出具体的使用步骤。两个文件分别对应 Android 和 HarmonyOS 平台,运行后自动完成:折叠状态控制 → 执行测试 → AI 视觉分析 → 输出报告。
7.1 Android 完整脚本
保存为 foldable_test_android.py,修改配置区的 5 个参数后直接运行:
需要修改的配置项(共 5 处)
|
7.2 HarmonyOS 完整脚本
保存为 foldable_test_harmony.py,修改配置区的 4 个参数后直接运行:
需要修改的配置项(共 4 处)
|
7.3 一键运行脚本
创建运行脚本 run_test.bat(Windows),一键启动 Appium + 运行测试:
run_test.bat — 一键启动测试
@echo offchcp 65001 >nulecho ========================================echo 折叠屏兼容性测试一键运行脚本echo ========================================echo.:: 启动 Appium Server(后台运行)echo [1/3] 启动 Appium Server...start "Appium" cmd /c "appium --allow-cors"timeout /t 5 /nobreak >nul:: 选择测试平台echo.echo 请选择测试平台:echo 1 - Androidecho 2 - HarmonyOSecho 3 - 全部(先 Android 后 HarmonyOS)set /p choice=请输入选项(1/2/3):if "%choice%"=="1" (echo.echo [2/3] 运行 Android 折叠屏测试...python "%~dp0foldable_test_android.py") else if "%choice%"=="2" (echo.echo [2/3] 运行 HarmonyOS 折叠屏测试...python "%~dp0foldable_test_harmony.py") else if "%choice%"=="3" (echo.echo [2/3] 运行 Android 折叠屏测试...python "%~dp0foldable_test_android.py"echo.echo [3/3] 运行 HarmonyOS 折叠屏测试...python "%~dp0foldable_test_harmony.py") else (echo [ERROR] 无效选项,请输入 1、2 或 3pauseexit /b 1)echo.echo ========================================echo 测试完成!报告保存在截图目录下的 test_report.txtecho ========================================pause
8.1 折叠状态控制问题
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
adb shell settings put system user_rotation 1 |
|
|
|
|
|
|
|
|
8.2 脚本执行问题
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
netstat -ano | findstr 4723),重新启动 appium |
|
|
|
|
|
|
|
|
8.3 如何扩展测试场景
- 换 APP
:修改配置区的包名 / Activity 和搜索框定位器即可,其他代码不变 - 增加测试步骤
:在 execute_douyin_steps()函数中追加步骤函数调用 - 换 AI 模型
:修改 AI_VISION_API_URL和AI_MODEL参数即可(支持通义千问 / GPT-4V / Claude 等) - 接入 CI/CD
:将脚本注册到 Jenkins / GitHub Actions,配置触发条件为代码合并后自动执行
|
推荐:持续集成方案 将折叠屏测试接入 CI/CD 流水线,建议流程:代码合并 → 触发 Jenkins → 执行折叠屏测试脚本 → AI 分析报告生成 → 报告上传到 Artifacts → 发送钉钉/企业微信通知。每日构建自动跑折叠屏兼容性测试,发现异常及时告警。 |
本文完整介绍了使用大模型解决折叠屏兼容性测试的方案,从 AI 生成脚本、折叠状态控制、AI 视觉分析三个环节依次展开,给出了 Android 和 HarmonyOS 两套完整可运行的代码。
📦 本文交付的三份核心资产
- Android 完整脚本
(foldable_test_android.py):ADB 折叠控制 + 抖音四步操作 + AI 视觉分析,开箱即用 - HarmonyOS 完整脚本
(foldable_test_harmony.py):HDC 折叠控制 + 抖音四步操作 + AI 视觉分析,适配鸿蒙生态 - AI 视觉分析模块
(ai_vision_analyzer.py):可独立运行,分析任意截图目录,自动输出报告
扩展思路
- 换 APP
:将抖音换成你们实际的被测 APP,修改包名和定位器即可 - 换测试步骤
:在 Skill 中定义新的操作步骤,AI 自动生成对应脚本 - 换 AI 模型
:将通义千问换成 GPT-4V / Claude Vision,Prompt 格式略有不同但逻辑相同 - 接 CI/CD
:注册到 Jenkins 或 GitHub Actions,每日构建自动触发折叠屏测试 - 多设备并行
:用 Python multiprocessing 同时控制多台设备,批量测试
折叠屏的碎片化问题是长期挑战,但通过 AI + 自动化的方式,可以将”每次发版都要人工折叠测试”变成”一键自动跑完全部场景”。希望本文的方案能帮你把这个能力真正落地。
| 下一步建议
拿你的折叠屏设备,按本文步骤配置环境,先跑通 Android 版脚本——拿到第一份 AI 分析报告后,你会对这个方案的价值有最直观的感受。然后再迁移到你的实际被测 APP,逐步扩大测试覆盖面。 |
