AI 生成自动化脚本 + ADB / HDC 折叠状态控制 + 视觉 AI 分析一套代码跑完展开 / 折叠全流程,零死角检测界面异常

📖 导读:先看真实案例,再看怎么解决

场景一:新闻 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 分析”全链路,任何人拿着脚本在设备上跑一遍就能出报告。

本文你能得到什么



第一章 整体方案设计:AI + 折叠控制 + 视觉分析

折叠屏兼容性测试的核心问题不是”脚本怎么写”,而是如何让脚本自动控制折叠状态并自动判断结果。本文的方案设计围绕三个核心环节展开:

1.1 全流程架构

整个方案分为三个层次,每层解决一个问题:

微信公众号二维码图片引自微信公众号,扫码关注阅读原文

AI 生成脚本 → 折叠状态控制 → 执行截图 → AI 视觉分析,三个环节依次衔接最终输出报告

1.2 工具链说明

工具
用途
备注
Appium + UiAutomator2
Android 端 UI 自动化
控制 APP 操作与截图
Appium + HarmonyOS 驱动
HarmonyOS 端 UI 自动化
通过 DevEco 工具链连接
ADB
Android 折叠状态控制
wm size 切换分辨率模拟折叠
HDC
HarmonyOS 折叠状态控制
hdc shell bm setDisplayMode
通义千问 / GPT-4V
AI 视觉分析
分析截图,输出异常描述
Python requests
调用视觉模型 API
与 Appium 脚本集成在同一文件

1.3 折叠屏测试的特殊性

折叠屏兼容性测试和普通兼容性测试有三个本质区别:

  1. 分辨率突变
    :折叠态和展开态的分辨率完全不同(常见如展开 2200×1840 vs 折叠 984×2096),布局会重新计算
  2. 铰链区域
    :屏幕中间有物理铰链,内容不能覆盖铰链区域,否则显示异常或触控失灵
  3. 状态切换延迟
    :折叠 / 展开操作后,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:            pass    return 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 设备折叠状态"""
把上面的 Skill 内容复制给 AI,AI 就能为你生成符合要求的脚本。生成后需要根据自己的设备参数(设备地址、端口、包名等)修改配置区即可运行。


第三章 折叠状态控制: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,从而测试应用在”折叠后分辨率”下的界面表现。

微信公众号二维码图片引自微信公众号,扫码关注阅读原文
Android折叠态模拟:通过adb shell wm size 动态修改分辨率,触发应用重新布局实现折叠
⚠ 重要说明:分辨率模拟 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: 984x2096    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 控制 Android 设备折叠状态
    参数:        state: "folded"(折叠态)或 "unfolded"(展开态)    返回:        True=成功,False=失败    """    if state == "folded":        target_res = FOLDED_RESOLUTION        display_name = "折叠态"    elif state == "unfolded":        target_res = ORIGINAL_RESOLUTION        display_name = "展开态"    else:        print(f"[ERROR] 未知状态: {state}")        return False    print(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 True    else:        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 安装,路径在:

折叠屏状态控制主要通过 bm(Bundle Manager)子命令的 setDisplayMode 参数实现:

微信公众号二维码图片引自微信公众号,扫码关注阅读原文
HarmonyOS 折叠状态控制方案:通过 HDC 命令调用系统 Bundle Manager 的 setDisplayMode 接口,mode=0 为展开态,mode=1 为折叠态

3.2.2 HarmonyOS 折叠状态枚举说明

HarmonyOS 折叠屏的状态值定义在 ohos.display.FoldStatus 枚举中:

FoldStatus 值
含义
对应 HDC mode
FOLD_STATUS_EXPANDED = 0
完全展开态
–mode 0
FOLD_STATUS_FOLDED = 1
完全折叠态
–mode 1
FOLD_STATUS_HALF_FOLDED = 2
半折叠(悬停态)
–mode 2
FOLD_STATUS_INNER_FOLD_EXPANDED = 3
内折展开态(折叠屏内折机型)
–mode 3
FOLD_STATUS_INNER_FOLD_FOLDED = 4
内折折叠态
–mode 4

如何确认你设备对应的 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_PATH    if 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 False    mode = 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 False    start = time.time()    last_size = -1    stable_count = 0    while 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 += 1                if stable_count >= 2:  # 连续两次大小一致                    return True            else:                stable_count = 0                last_size = current_size        except OSError:            pass    print(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 False    if wait_effect:        return wait_screenshot_ready(filepath)    return True



第四章 实战演示:抖音自动化+折叠/展开(Android 脚本)
本章给出完整的 Android 版抖音折叠屏测试脚本。以抖音 APP 为例,演示在折叠屏展开态和折叠态下执行”打开 → 搜索 → 点击 → 退出”四步操作,并在每步后截图。脚本最后集成 AI 视觉分析调用。

4.1 环境准备清单

开始之前,确认以下环境已就绪:

依赖项
版本要求
安装方式
验证命令
Python
3.9+
python.org 下载
python --version
Appium Server
2.x
npm i -g appium appium --version
Appium Python Client
最新
pip install appium-python-client pip show appium-python-client
Android SDK (adb)
含 platform-tools
Android Studio 内置
adb version
requests(AI 分析用)
最新
pip install requests pip show requests
Android 真机/折叠屏
开启 USB 调试
设备设置 → 开发者选项
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_RESOLUTION    elif state == "unfolded":        target = ORIGINAL_RESOLUTION    else:        return False    print(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 False    start = time.time()    last_size = -1    stable = 0    while time.time() - start < timeout:        time.sleep(0.3)        try:            s = os.path.getsize(path)            if s == last_size and s > 0:                stable += 1                if stable >= 2:                    return True            else:                stable = 0                last_size = s        except OSError:            pass    return 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 False    if 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", "")            # 尝试解析 JSON            import json            for line in content.splitlines():                try:                    parsed = json.loads(line)                    return parsed                except:                    pass            return {"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_ADDR    options.app_package = DOUYIN_PACKAGE    options.app_activity = DOUYIN_ACTIVITY    options.no_reset = True    # 强制刷新当前窗口尺寸(分辨率切换后需要)    options.new_command_timeout = 120    return webdriver.Remote(APPIUM_SERVER, options=options)def wait_for_element(driver, by, value, timeout=15):    """等待元素出现"""    from selenium.webdriver.support.ui import WebDriverWait    from selenium.webdriver.support import expected_conditions as EC    try:        el = WebDriverWait(driver, timeout).until(            EC.presence_of_element_located((by, value))        )        return el    except:        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 = None    for 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} 找到搜索框")                break        except:            pass    if 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] 输入文字成功")                    break            except:                pass        time.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 = None    for 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} 点击搜索按钮")                break        except:            pass    time.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}")    # 退出 APP    driver.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 运行步骤说明

保存脚本后,按以下步骤运行:

  1. 启动 Appium Server
    :命令行执行 appium(或 Appium Desktop 启动服务)
  2. 连接设备
    :确保折叠屏手机已通过 USB 连接电脑,USB 调试已开启,执行 adb devices 确认设备在线
  3. 修改配置区
    :将 ANDROID_DEVICE_ADDR 改为你的设备地址,ORIGINAL_RESOLUTION 和 FOLDED_RESOLUTION 改为你的设备实际分辨率(执行 adb shell wm size 查到)
  4. 运行脚本
    :python foldable_test_android.py
  5. 查看结果
    :脚本执行完毕后,自动弹出 AI 分析报告;截图保存在配置区指定的目录中

定位器失效怎么办

抖音版本更新后,元素定位器(ID、XPath)可能变化。遇到”未找到元素”时,用 Appium Inspector 重新抓取当前版本的元素信息:启动 Appium Desktop → 点击 Start Inspector Session → 填写 desired_caps → 填入抖音的包名和 Activity → 点击 Start Session。抓取后用新的 ID 替换脚本中的 search_selectors 列表即可。




第五章 HarmonyOS版本:HDC + Appium适配要点

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_PATH    if 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 False    print(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 False    start = time.time()    last = -1    stable = 0    while time.time() - start < timeout:        time.sleep(0.3)        try:            s = os.path.getsize(path)            if s == last and s > 0:                stable += 1                if stable >= 2:                    return True            else:                stable = 0                last = s        except OSError:            pass    return 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 False    if 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 启动 Ability    options.app_activity = f".{DOUYIN_ABILITY_NAME}"    options.no_reset = True    options.new_command_timeout = 120    return webdriver.Remote(APPIUM_SERVER, options=options)def execute_douyin_steps(driver, fold_state: str):    """执行抖音四步操作(HarmonyOS 版本)"""    ts = int(time.time())    prefix = "harmony"  # 截图文件名加 harmony_ 前缀区分平台    # 步骤 1:启动 APP    print(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 = None    for by, val in search_selectors:        try:            el = driver.find_element(by, val, timeout=3)            if el:                el.click()                search_box = el                print(f"[INFO] 搜索框定位成功: {by}:{val}")                break        except:            pass    if 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] 输入文字成功")                    break            except:                pass        time.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}")                break        except:            pass    time.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: cannot connect to device
设备未开启开发者模式或 USB 调试
设备设置 → 开发者选项 → 开启 USB 调试 + HDCD(鸿蒙调试)
hdc list targets 为空
HDC 服务未启动
执行 hdc kill-server && hdc start-server 重启服务
bm setDisplayMode 报错
设备不支持该命令或权限不足
确认设备为折叠屏;部分设备需要 root 权限
Appium 连接超时
设备未通过 USB 连接或端口被占用
确认 USB 连接正常;检查 4723 端口是否被占用(netstat -ano | findstr 4723)



第六章 AI 视觉分析:脚本执行完毕后自动接续分析截图

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 分析代码

微信公众号二维码图片引自微信公众号,扫码关注阅读原文
步骤1: 遍历 screenshots 目录,找到所有 png 文件
步骤2: 逐张读取图片,转 base64
步骤3: 调用视觉 API,传入图片 + Prompt
步骤4: 解析 JSON 结果,输出到控制台 + 保存报告文件
AI 视觉分析流程:从 8 张截图到异常报告,全自动执行,无需人工介入
ai_vision_analyzer.py — 独立 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 Key        model: 模型名称    返回:        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.content            content = (                result.get("output", {}).get("choices", [{}])[0]                .get("message", {}).get("content", "")            )            # 尝试从 content 中提取 JSON            for segment in content.splitlines():                segment = segment.strip()                if segment.startswith("{") and segment.endswith("}"):                    try:                        parsed = json.loads(segment)                        return parsed                    except json.JSONDecodeError:                        pass            return {"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.png    name = 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_DIR    api_url = api_url or AI_VISION_API_URL    api_key = api_key or AI_API_KEY    model = model or AI_MODEL    if 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"] = filename        result.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 - abnormal    print("=" * 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_PATH    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"截图目录: {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 处)

  1. ANDROID_DEVICE_ADDR
    :ADB 设备地址(执行 adb devices 查到)
  2. ORIGINAL_RESOLUTION
     / FOLDED_RESOLUTION:你的折叠屏实际分辨率(执行 adb shell wm size 查到)
  3. AI_API_KEY
    :通义千问 API Key(前往 dashscope.console.aliyun.com 申请)
  4. SCREENSHOT_DIR
    :截图保存目录(任意路径)
  5. 搜索框定位器:根据当前抖音版本用 Appium Inspector 重新抓取

7.2 HarmonyOS 完整脚本

保存为 foldable_test_harmony.py,修改配置区的 4 个参数后直接运行:

需要修改的配置项(共 4 处)

  1. HDC_PATH
    :hdc.exe 的实际路径(DevEco Studio 安装目录下)
  2. HARMONY_DEVICE_ID
    :多设备时填入(单设备留空自动匹配)
  3. DOUYIN_BUNDLE_NAME
    :鸿蒙版抖音包名(可能与 Android 不同)
  4. AI_API_KEY
    :通义千问 API Key

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 或 3    pause    exit /b 1)echo.echo ========================================echo  测试完成!报告保存在截图目录下的 test_report.txtecho ========================================pause



第八章 常见问题与维护指南

8.1 折叠状态控制问题

问题
原因
解决方案
adb shell wm size 报错 permission denied
普通应用无法修改系统分辨率
设备需要 root 权限;或使用厂商提供的折叠控制 API(如华为 HiCar SDK)
分辨率切换后应用未重新布局
应用未监听 configuration changed
手动旋转屏幕触发重新布局:adb shell settings put system user_rotation 1
hdc shell bm setDisplayMode 报错
设备型号不支持该命令
部分折叠屏设备折叠状态切换需通过系统设置手动操作,HDC 命令仅供参考
折叠状态切换后 Appium 连接断开
设备分辨率变化导致 Appium session 失效
切换后重新初始化 driver(脚本中已实现 driver.quit() 后重建)

8.2 脚本执行问题

问题
原因
解决方案
元素定位超时(ElementNotFound)
抖音版本更新,ID/XPath 变化
用 Appium Inspector 重新抓取当前版本的元素定位器
Appium Server 报错 Connection refused
Appium 未启动或端口被占用
确认 4723 端口空闲(netstat -ano | findstr 4723),重新启动 appium
截图文件大小为 0
截图写入失败(磁盘满或权限不足)
检查截图目录是否存在、是否有写入权限
AI 分析 API 返回 401 Unauthorized
API Key 错误或已过期
登录阿里云 dashscope 控制台,重新获取 API Key

8.3 如何扩展测试场景

  1. 换 APP
    :修改配置区的包名 / Activity 和搜索框定位器即可,其他代码不变
  2. 增加测试步骤
    :在 execute_douyin_steps() 函数中追加步骤函数调用
  3. 换 AI 模型
    :修改 AI_VISION_API_URL 和 AI_MODEL 参数即可(支持通义千问 / GPT-4V / Claude 等)
  4. 接入 CI/CD
    :将脚本注册到 Jenkins / GitHub Actions,配置触发条件为代码合并后自动执行

推荐:持续集成方案

将折叠屏测试接入 CI/CD 流水线,建议流程:代码合并 → 触发 Jenkins → 执行折叠屏测试脚本 → AI 分析报告生成 → 报告上传到 Artifacts → 发送钉钉/企业微信通知。每日构建自动跑折叠屏兼容性测试,发现异常及时告警。




总结 三份核心资产+扩展思路

本文完整介绍了使用大模型解决折叠屏兼容性测试的方案,从 AI 生成脚本、折叠状态控制、AI 视觉分析三个环节依次展开,给出了 Android 和 HarmonyOS 两套完整可运行的代码。

📦 本文交付的三份核心资产

  1. Android 完整脚本
    (foldable_test_android.py):ADB 折叠控制 + 抖音四步操作 + AI 视觉分析,开箱即用
  2. HarmonyOS 完整脚本
    (foldable_test_harmony.py):HDC 折叠控制 + 抖音四步操作 + AI 视觉分析,适配鸿蒙生态
  3. AI 视觉分析模块
    (ai_vision_analyzer.py):可独立运行,分析任意截图目录,自动输出报告

扩展思路

折叠屏的碎片化问题是长期挑战,但通过 AI + 自动化的方式,可以将”每次发版都要人工折叠测试”变成”一键自动跑完全部场景”。希望本文的方案能帮你把这个能力真正落地。

下一步建议

拿你的折叠屏设备,按本文步骤配置环境,先跑通 Android 版脚本——拿到第一份 AI 分析报告后,你会对这个方案的价值有最直观的感受。然后再迁移到你的实际被测 APP,逐步扩大测试覆盖面。