通用 WPF GUI 自动化测试框架 — pip install 即用,零外部服务依赖
Project description
WPF GUI TestKit
通用 WPF GUI 自动化测试框架 — pip install 即用,零外部服务依赖。
基于 Python + pywinauto (UIA backend) + pytest,不需要 WinAppDriver、Appium 或其他外部服务。
可选扩展:支持阿里百炼 Qwen2.5-VL 多模态大模型,通过截图分析替代脆弱的 UIA 控件枚举。
特点
- 🎯 五级点击兜底 —
click() → click_input() → invoke() → set_focus + ENTER → Vision 坐标定位,覆盖所有 WPF 控件模板和分层窗口 - 🛡️ 崩溃守护 — 自动检测被测应用意外退出,记录截图和日志,支持 Vision 自动分析崩溃对话框内容
- 👁️ Vision 语义断言 — 通过多模态大模型从截图直接验证 UI 状态(弹窗是否打开、播放器是否正在播放、录音状态、错误提示检测等)
- 📸 失败自动截图 + AI 分析 — 测试失败时自动保存截图并发送给 Vision 分析,结果写入同目录的
_vision.txt - 🧹 进程隔离 — 每条用例独立启动/清理,不留残留进程或配置
- 🔌 零服务依赖 — 不需要 WinAppDriver、Appium、Selenium Grid 或其他外部服务
- 🔄 多模型供应商支持 — Provider Adapter 模式,可切换阿里百炼/MiniMax/智谱/OpenAI 等任意多模态模型
安装
# 核心框架(零额外依赖)
pip install wpf-gui-testkit
# 包含 Vision 多模态大模型支持
pip install wpf-gui-testkit[vision]
快速开始
1. 创建 Page Object main_page.py
from wpf_testkit.core.base_page import BasePage
class LoginPage(BasePage):
"""用户登录窗口 Page Object(示例)。"""
@property
def window(self):
if self._window is None:
self._window = self.app.window(auto_id="MainWindow")
return self._window
def enter_username(self, text: str):
self.set_text("TxtUsername", text)
def enter_password(self, text: str):
self.set_text("TxtPassword", text)
def click_login(self):
self.click_element("BtnLogin")
def get_status_text(self) -> str:
return self.get_text("TxtStatus")
2. 创建测试文件 test_login.py
import pytest
from main_page import LoginPage
class TestLogin:
def test_window_launch(self, main_window):
"""验证主窗口启动后可见。"""
assert main_window.exists()
assert main_window.is_visible()
def test_login_form(self, app_launch, main_window):
"""验证表单交互:输入凭据 → 点击登录 → 检查状态。"""
page = LoginPage(app_launch)
page.enter_username("admin")
page.enter_password("123456")
page.click_login()
page.wait_element_visible("TxtStatus", timeout=5)
assert "成功" in page.get_status_text()
3. 配置环境变量
# 被测应用路径(必填)
set WPF_TEST_APP_PATH=C:\path\to\YourWpfApp.exe
# 被测应用进程名(用于崩溃检测和进程清理)
set WPF_TEST_APP_PROCESS_NAME=YourWpfApp.exe
# 主窗口 AutomationProperties.AutomationId
set WPF_TEST_MAIN_WINDOW_ID=MainWindow
# %APPDATA% 下的应用数据目录名(可选,用于测试间清理残留配置)
set WPF_TEST_APP_DATA_DIR=YourWpfApp
4. 运行测试
pytest test_login.py -v
项目结构
wpf-gui-testkit/
├── wpf_testkit/
│ ├── __init__.py # 版本号
│ ├── exceptions.py # 自定义异常(ElementNotFound, CommandInvoke, CrashDetected)
│ ├── vision.py # VisionAnalyzer + Provider Adapter(多模态大模型视觉分析)
│ ├── core/
│ │ ├── base_page.py # Page Object 基类(五级点击兜底、等待、断言、截图、Vision 语义断言)
│ │ └── conftest.py # pytest fixtures(app_launch, main_window, crash_daemon 等)
│ └── utils/
│ ├── crash_daemon.py # 崩溃守护线程(每 2 秒检测进程存活 + Vision 崩溃分析)
│ ├── screenshot.py # 截图管理器(全屏/ROI/失败截图 + Vision 自动分析 + 自动清理)
│ ├── visual_diff.py # 视觉回归引擎(像素级 diff + Vision 语义差异过滤误报)
│ ├── uia_helpers.py # UIA 辅助工具(控件树转储、窗口查找、Vision 控件描述增强)
│ ├── win32_dialogs.py # Win32 系统对话框自动化(Open/Save FileDialog)
│ └── dpi_utils.py # DPI 缩放适配
├── examples/
│ ├── wpf-calculator/ # 示例:.NET 9 WPF 计算器(14 测试用例)
│ │ ├── WpfCalculator/ # 被测应用源码(C#)
│ │ └── tests/ # GUI 测试(pytest)
│ ├── wpf-contacts/ # 示例:.NET 9 WPF 通讯录(13 测试用例,覆盖多种控件)
│ │ ├── WpfContacts/ # 被测应用源码(C#)
│ │ └── tests/ # GUI 测试(pytest)
│ └── wpf-controls/ # 示例:.NET 9 WPF 控件展示(14 测试用例)
│ ├── WpfControls/ # 被测应用源码(C#)
│ └── tests/ # GUI 测试(pytest)
├── tests/
│ └── unit/ # 框架自测(纯逻辑,mock pywinauto,可在 WSL/CI 运行)
│ ├── test_wpf_testkit.py # 68 测试用例:exceptions/screenshot/crash_daemon/base_page/uia_helpers/vision
│ └── test_visual_diff.py # 12 测试用例:像素 diff + Vision 语义过滤
├── pyproject.toml
├── README.md
└── LICENSE
环境变量
| 变量 | 默认值 | 说明 |
|---|---|---|
WPF_TEST_APP_PATH |
(必填) | 被测应用 .exe 的完整路径 |
WPF_TEST_APP_PROCESS_NAME |
app.exe |
被测应用进程名(用于崩溃检测和进程清理,含子进程递归杀) |
WPF_TEST_APP_DATA_DIR |
(空) | %APPDATA% 下的应用数据目录名(测试间清理用) |
WPF_TEST_MAIN_WINDOW_ID |
MainWindow |
主窗口 AutomationProperties.AutomationId |
WPF_TEST_GUIDE_WINDOW_TITLE |
(空) | 首次启动引导页窗口标题(自动关闭用) |
ALIYUN_VISION_API_KEY |
(空) | Vision 多模态 API 密钥([vision] 扩展使用) |
VISION_API_URL |
https://dashscope.aliyuncs.com/... |
Vision API 端点(可切换其他供应商) |
VISION_MODEL |
qwen2.5-vl-72b-instruct |
多模态模型名称 |
API 参考
BasePage(app) — Page Object 基类
等待
| 方法 | 说明 |
|---|---|
wait_visible(timeout=15) |
等待窗口可见 |
wait_enabled(timeout=10) |
等待窗口可用 |
wait_element_visible(auto_id, timeout=10) |
等待指定控件可见 |
点击操作
| 方法 | 说明 |
|---|---|
click_element(auto_id, timeout=10, debug=False) |
点击控件,自动五级降级 |
click_input_element(auto_id, timeout=10) |
用 click_input 方式点击 |
click_by_vision(description, timeout=10) |
Vision 驱动:通过描述定位无 AutomationId 的控件并点击 |
combo_select_by_text(auto_id, text, timeout=10) |
从 ComboBox 中按键选择项 |
输入与读取
| 方法 | 说明 |
|---|---|
set_text(auto_id, text) |
文本框输入(Ctrl+A 清空后输入) |
get_text(auto_id) |
获取控件文本 |
safe_text(ctrl) |
安全获取控件文本,兼容 GBK 编码 |
判断与断言
| 方法 | 说明 |
|---|---|
is_element_visible(auto_id) |
判断控件是否可见 |
get_element_rectangle(auto_id) |
获取控件矩形区域 |
invoke_command(command_name, command_mapping=None) |
通过 UIA InvokePattern 触发 WPF Command |
assert_element_exists(auto_id) |
断言控件存在 |
assert_element_text_contains(auto_id, expected) |
断言文本包含 |
Vision 语义断言(需 [vision] 扩展)
| 方法 | 说明 |
|---|---|
vision_available |
属性:Vision 是否可用 |
vision_healthy_check() |
启动时检测 Vision API 可用性 |
vision_assert_dialog_open(dlg_name, timeout=10) |
断言弹窗已打开(重试直到超时) |
vision_assert_playing(timeout=10) |
断言播放器正在播放 |
vision_assert_recording(timeout=10) |
断言正在录音 |
vision_assert_no_error() |
断言截图中没有错误提示 |
vision_assert_dialog_closed(title, timeout=5) |
断言弹窗已关闭 |
vision_capture_and_analyze(prompt, name, save_dir) |
截图 + Vision 分析 + 保存(截图+分析文本) |
dump_controls_with_vision(fallback_on_empty=True) |
UIA 控件树 + Vision 描述(UIA 为 0 时自动启用) |
VisionAnalyzer — 多模态视觉分析器
位于 wpf_testkit/vision.py,通过 Provider Adapter 模式支持任意多模态模型供应商。
from wpf_testkit.vision import VisionAnalyzer, OpenAIVisionProvider
# 方式一:默认(阿里百炼 Qwen2.5-VL,需配置环境变量)
va = VisionAnalyzer()
# 方式二:自定义 Provider(可切换任意 OpenAI 兼容 API)
provider = OpenAIVisionProvider(
api_url="https://api.minimaxi.com/v1/chat/completions",
api_key="mm-xxx",
model="minimax-vl-01",
)
va = VisionAnalyzer(provider=provider)
if va.available:
result = va.analyze(img, "描述这张截图")
| 方法 | 说明 |
|---|---|
analyze(img, prompt, max_tokens=1024, detail="high") |
通用截图分析 |
healthy_check() |
检测 API 可用性(失败时自动禁用) |
find_control(img, description) |
通过文字描述定位控件坐标 |
find_close_button(img) |
定位关闭按钮坐标 |
compare_semantic(candidate_img, baseline_img) |
语义比较两张截图(过滤非功能性差异) |
VisualDiff(diff_output_dir="screenshots/diffs") — 视觉回归引擎
| 方法 | 说明 |
|---|---|
compare(candidate_path, baseline_path) → DiffResult |
比较截图(像素超标时自动 Vision 语义分析) |
update_baseline(candidate_path, baseline_path) |
将当前截图更新为新基准 |
DiffResult — 视觉回归结果
| 属性 | 说明 |
|---|---|
diff_pct |
差异像素百分比 (0.0 ~ 1.0) |
diff_count |
差异像素数 |
max_diff |
单像素最大差异 (0~255) |
diff_image_path |
差异高亮图路径(红色标记差异区域) |
semantic_diff |
Vision 语义分析结果(像素超标时自动触发) |
semantic_acceptable |
Vision 判定为非实质性差异(如时间/滚动变化) |
passed |
baseline 存在且尺寸匹配 |
within_threshold(threshold) |
差异是否在阈值内(默认 5%,含语义过滤) |
summary() |
人类可读摘要(含 Vision 语义提示) |
ScreenshotManager(save_dir="screenshots") — 截图管理器
| 方法 | 说明 |
|---|---|
capture(window, name) |
截取窗口/桌面截图 |
capture_roi(window, name, region) |
截取 ROI 区域 (x, y, w, h) |
capture_failure(window, test_name) |
失败截图 + 自动 Vision 分析 |
cleanup_old(keep_days=7) |
清理超过 N 天的截图 |
CrashDaemon(process_name, main_window_id, screenshot_dir) — 崩溃守护
| 方法 | 说明 |
|---|---|
start() |
启动守护线程(每 2 秒检测) |
stop() |
停止守护线程 |
has_crashed |
是否检测到崩溃 |
crash_log |
崩溃日志文件路径 |
get_summary() |
崩溃摘要(含 Vision 分析结果) |
How to use Vision 扩展(多模态大模型)
配置
# 阿里百炼 API 密钥(必填)
set ALIYUN_VISION_API_KEY=sk-xxx
# 可选:切换其他供应商
set VISION_API_URL=https://api.other-provider.com/v1/chat/completions
set VISION_MODEL=other-vision-model
Vision 第五级点击兜底
当 UIA 的四级降级全部失败时(典型:分层窗口 AllowsTransparency=True),自动启用 Vision 坐标定位:
page.click_element("BtnSettings") # 自动五级降级,无需额外代码
Vision 语义断言
适合 UIA 无法获取状态的场景(如播放器按钮图标变化):
# 验证弹窗弹出(自动重试直到超时)
page.vision_assert_dialog_open("设置", timeout=10)
# 验证播放器正在播放
page.vision_assert_playing()
# 验证录音状态
page.vision_assert_recording()
# 一次性截图 + 分析 + 保存
desc = page.vision_capture_and_analyze(
"描述这个对话框", name="about_dialog"
)
失败自动 Vision 分析
测试失败时,截图管理器自动将截图发给 Vision 分析,结果写入 screenshots/failures/:
screenshots/failures/test_login_failed_20260505_143022.png # 原始截图
screenshots/failures/test_login_failed_20260505_143022_vision.txt # Vision 分析
崩溃自动 Vision 分析
被测应用崩溃时,崩溃守护自动截图并分析错误对话框内容:
screenshots/crash_screen_20260505_143500.png # 崩溃瞬间截图
screenshots/crash_screen_20260505_143500_vision.txt # Vision 分析
视觉回归语义过滤
像素差异超标时自动 Vision 语义分析,过滤时钟/天气/滚动位置等非功能性误报:
result = vd.compare("current.png", "baseline.png")
# 像素超标但 Vision 判定为"非实质性差异"(如时间变化),still passes
assert result.within_threshold(0.05), result.summary()
Skills Playbook 自动选择(v3.1)
根据场景意图自动匹配合适的分析提示词模板,无需手写 prompt:
from wpf_testkit.vision import get_analyzer
va = get_analyzer()
# 自动匹配 playbook(推荐)
result = va.analyze_with_intent(screenshot, "确认弹窗已经关闭")
# → SceneMatcher 匹配到 dialog-verify playbook
# → 自动注入专业 prompt 进行分析
# 注册自定义 playbook(适用于业务专用场景)
va.register_custom_playbook(
name="jianting-main",
description="简听收音机主界面:电台列表、播放控制、音量、收藏",
prompt="分析简听收音机主界面截图。1)电台列表是否已加载?"
"2)当前选中的电台名?3)播放按钮状态?4)音量滑块位置?",
)
预置 6 个 playbook:dialog-verify、playback-status、control-existence、layout-integrity、error-state、mini-mode。
多模型角色分离(v3.1)
Cheap Brain(轻量模型低分辨率)做粗筛 + Premium Brain(高精度模型)做精检,auto 模式自动降级,可节省约 83% 成本:
# auto 模式(默认):先 cheap 看置信度,不确信再 premium
result = va.analyze_with_intent(screenshot, "播放按钮是否存在")
# 强制 cheap(高频检查)
result = va.analyze_with_intent(screenshot, "弹窗是否关闭", brain="cheap")
# 强制 premium(关键验证)
result = va.analyze_with_intent(screenshot, "界面上所有文字是否正确", brain="premium")
auto 模式下 90% 的常见检查由 cheap 模型完成,仅 10% 需要 premium 精检。
如何让 WPF 应用可测试
wpf-gui-testkit 依赖 UI Automation (UIA) 框架来识别控件。WPF 项目默认支持 UIA,但有几个关键点需要配合。
AutomationId 命名规范
每个可交互控件必须设置 AutomationProperties.AutomationId,否则 UIA 无法精准定位:
<!-- ✅ 正确:有明确的 AutomationId -->
<Button AutomationProperties.AutomationId="BtnLogin" Content="登录" />
<TextBox AutomationProperties.AutomationId="TxtUsername" />
<ComboBox AutomationProperties.AutomationId="ComboCity" />
<Slider AutomationProperties.AutomationId="SliderVolume" />
<CheckBox AutomationProperties.AutomationId="ChkRemember" />
<!-- ❌ 错误:UIA 只能靠文本/索引模糊查找,测试脆弱 -->
<Button Content="登录" />
<TextBox />
命名惯例:
| 控件类型 | 前缀 | 示例 |
|---|---|---|
| Button | Btn |
BtnLogin, BtnSave, BtnCancel |
| TextBox | Txt |
TxtUsername, TxtPassword |
| ComboBox | Combo |
ComboCategory, ComboLanguage |
| Slider | Slider |
SliderVolume, SliderBrightness |
| CheckBox | Chk |
ChkRemember, ChkAgree |
| RadioButton | Radio |
RadioMale, RadioFemale |
| TextBlock | Txt(作为标签) |
TxtStatus, TxtTitle |
| ListBox/ListView | List |
ListStations, ListResults |
| 顶层窗口 | Window |
MainWindow, SettingsWindow |
子窗口定位
如果被测应用有弹出窗口(设置、关于、对话框),这些子窗口没有主窗口的 auto_id,需要通过标题定位:
# 点击打开设置
main_page.click_element("BtnSettings")
time.sleep(0.5)
# 按标题找到子窗口
settings = main_page.app.window(title="设置")
assert settings.exists(), "设置窗口未弹出"
# 操作子窗口内的控件
checkbox = settings.child_window(title="启用通知", control_type="CheckBox")
checkbox.click_input()
# 关闭子窗口
settings.close()
窗口样式与 UIA 兼容性
| 窗口属性 | 影响 | 解决方案 |
|---|---|---|
WindowStyle=None |
UIA 按标题查找可能失败 | 优先用 auto_id 查找窗口 |
AllowsTransparency=True |
UIA InvokePattern 无法触发 Button 的 Command 绑定 |
Vision 第五级点击兜底自动生效 |
Topmost=True |
覆盖层拦截 UIA 点击 | 测试前先关闭覆盖窗口 |
Avoid Pitfalls
-
AllowsTransparency + Command 绑定失效 当
WindowStyle=None+AllowsTransparency=True时,WPF 分层窗口的路由事件系统与 UIAInvokePattern交互存在 BUG。click()、click_input()、invoke()、set_focus+ENTER均无法触发按钮的Command绑定。此时 Vision 第五级兜底自动启用,无需修改被测应用代码。 -
引导页/弹出层覆盖主界面 如果应用首次启动有引导页(
Topmost=True),它会覆盖主界面使点击穿透。在app_launchfixture 中关闭它:try: guide = app.window(auto_id="GuideView") if guide.exists(): guide.close() time.sleep(0.5) except: pass
-
中文编码导致窗口查找失败 从 WSL/CI 运行 Windows Python 时,stdout 编码默认为 GBK。中文窗口标题会乱码,需要用 UTF-8 模式:
set PYTHONIOENCODING=utf-8 pytest -v
-
ComboBox 没有 select() 方法 WPF ComboBox 不支持
select(),用键盘操作替代:combo = main_window.child_window(auto_id="ComboCity") combo.set_focus() combo.type_keys("%{DOWN}") # Alt+↓ 展开列表 combo.type_keys("{DOWN}") # 选择下一项 combo.type_keys("{ENTER}")
-
Visibility=Collapsed 控件不可查找
Visibility=Collapsed的控件 UIA 不暴露 auto_id。需要在同一父级下直接查找子控件,或先让控件可见。
视觉回归测试(L3)
wpf-gui-testkit 内置了基于 PIL 的截图比对引擎 + Vision 语义差异过滤。
基本用法
from wpf_testkit.utils.visual_diff import VisualDiff
def test_visual_regression(app_launch, main_window, screenshot_manager):
# 1. 截图
shot = screenshot_manager.capture(main_window, "main_window")
# 2. 与 baseline 对比
vd = VisualDiff()
result = vd.compare(shot, "screenshots/baseline/main_window.png")
# 3. 首次运行自动创建 baseline(不会失败)
if result.baseline_missing:
vd.update_baseline(shot, "screenshots/baseline/main_window.png")
return
# 4. 断言差异在阈值内(像素超标时自动 Vision 语义过滤)
assert result.within_threshold(0.05), result.summary()
生成差异高亮图
compare() 方法自动在 screenshots/diffs/ 目录生成差异高亮图(红色半透明标记差异区域),方便人工审查。
使用场景
| 时机 | 操作 | 说明 |
|---|---|---|
| 首次运行 | 自动创建 baseline | 测试通过,baseline 已保存 |
| 正常运行 | 对比 baseline | 差异 > 5% 自动失败(Vision 语义过滤误报) |
| UI 改版后 | pytest --update-baseline |
强制更新所有 baseline |
已知限制
- WPF
AllowsTransparency=True的窗口 — UIAInvokePattern可能无法触发 WPF Command 绑定。Vision 第五级点击兜底自动处理此场景 - 窗口
WindowStyle=None— 需自定义关闭按钮和拖拽事件,UIA 查找窗口时用auto_id而非title - 中文编码 — 在命令行运行需
set PYTHONIOENCODING=utf-8 - 视觉回归 +
pytest-xdist— baseline 目录不适用于并发写入。不要对--update-baseline或视觉回归测试使用-n auto - Vision 依赖网络 — 多模态大模型需要稳定的互联网连接到 API 端点,网络问题会自动降级到传统 UIA 模式
示例
WPF 计算器
examples/wpf-calculator/ 提供了基准测试示例:
- 被测应用:.NET 9 WPF 计算器(14 个 P0/P1/P2 分级测试用例)
- Page Object:
tests/pages/wpf_calculator_page.py - 测试用例:
tests/test_calculator.py
覆盖控件类型:Button(数字/运算符)、TextBlock(显示屏)
cd examples/wpf-calculator
set WPF_TEST_APP_PATH=C:\path\to\WpfCalculator.exe
pytest -v
WPF 通讯录
examples/wpf-contacts/ 提供了高级控件测试示例:
- 被测应用:.NET 9 WPF 通讯录管理器(13 个 P0/P1/P2 分级测试用例)
- Page Object:
tests/pages/wpf_contacts_page.py - 测试用例:
tests/test_contacts.py
覆盖控件类型:TextBox(搜索框+表单)、ListView+GridView(列表)、ComboBox、对话框窗口、StatusBar
cd examples/wpf-contacts
set WPF_TEST_APP_PATH=C:\path\to\WpfContacts.exe
pytest -v
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wpf_gui_testkit-0.6.2.tar.gz.
File metadata
- Download URL: wpf_gui_testkit-0.6.2.tar.gz
- Upload date:
- Size: 44.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0779a2d0ae5eb2ffb401a157b8cf16b5666c501536ad8a85e1077c5edbf1a251
|
|
| MD5 |
0f8b868eb0f4b01c7013761a0c39cda0
|
|
| BLAKE2b-256 |
c62734e69244cbd9971018b18ee4b8ddb0dc8b8a4b557c98c70aa5ca53a17e4d
|
File details
Details for the file wpf_gui_testkit-0.6.2-py3-none-any.whl.
File metadata
- Download URL: wpf_gui_testkit-0.6.2-py3-none-any.whl
- Upload date:
- Size: 42.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75e0352f5c17d5ea6849e1b6318ddc727082f5a59f762441707e335ffa4858f2
|
|
| MD5 |
7f9eea3409fa4cef3db7e8ae52b239a0
|
|
| BLAKE2b-256 |
554f18fe4004eed381ea0ff7957bbfa31ce76a384d01f6fff75c5aa4ff01b5c4
|