Communitygithub.com

windows-desktop-e2e

使用 pywinauto 和 Windows UI Automation 对 Windows 原生桌面应用(WPF、WinForms、Win32/MFC、Qt)进行端到端测试。在需要为 Windows 原生桌面应用编写 E2E 测试时使用。

windows-desktop-e2e 是什么?

这是一个专门用于测试 Windows 原生桌面应用的端到端(E2E)测试技能,基于 pywinauto 和 Windows UI Automation (UIA) 技术栈。它支持多种桌面框架,包括 WPF、WinForms、Win32/MFC 以及 Qt 5.x/6.x,并为 Qt 提供了专门的指导。该技能的核心优势在于通过 UIA 树结构定位控件,并推荐优先使用 AutomationId 作为最稳定的定位策略。它提供了完整的项目脚手架,包括 Page Object Model 设计模式、基线页面类(base_page.py)、定位器策略(AutomationId > Name > ClassName + 索引 > XPath)、等待模式(wait_visible、wait_gone、wait_until)以及失败截图和逐步骤跟踪等调试机制。该技能还特别强调了可测试性设置:为每个交互控件提供稳定的 AutomationId 是提升测试可靠性的关键一步,并针对不同框架给出了具体实现示例。它支持 CI/CD 集成(如 GitHub Actions windows-latest),并提供了处理不稳定测试的最佳实践,包括隔离标记(skip/flaky)、超时调整和逐步骤跟踪日志。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/windows-desktop-e2e

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Windows 桌面端到端(E2E)测试

使用 pywinauto 结合 Windows UI Automation (UIA) 对 Windows 原生桌面应用程序进行端到端测试。涵盖 WPF、WinForms、Win32/MFC 和 Qt(5.x / 6.x)——其中 Qt 有专门的指导章节。

何时激活

  • 为 Windows 原生桌面应用程序编写或运行 E2E 测试
  • 从零开始搭建桌面 GUI 测试套件
  • 诊断不稳定或失败的桌面自动化测试
  • 为现有应用程序添加可测试性(AutomationId、可访问名称)
  • 将桌面 E2E 集成到 CI/CD 流水线(GitHub Actions windows-latest)

何时不使用

  • Web 应用程序 → 使用 e2e-testing 技能(Playwright)
  • Electron / CEF / WebView2 应用程序 → HTML 层需要浏览器自动化,而非 UIA
  • 移动应用 → 使用平台特定工具(UIAutomator、XCUITest)
  • 纯单元测试或集成测试(无需运行 GUI)

核心概念

所有 Windows 桌面自动化都依赖于 UI Automation (UIA),这是 Windows 内置的可访问性 API。每个支持的框架都暴露出一个 UIA 元素树,其中包含 Claude 可以读取和操作的属性:

你的测试 (Python)
    └── pywinauto (UIA 后端)
        └── Windows UI Automation API   ← 内置于 Windows,框架无关
            └── 应用程序的 UIA 提供程序  ← 每个框架自带提供程序
                └── 运行中的 .exe

各框架的 UIA 质量:

框架AutomationId可靠性备注
WPF5/5优秀x:Name 直接映射到 AutomationId
WinForms4/5良好AccessibleName = AutomationId
UWP / WinUI 35/5优秀微软完全支持
Qt 6.x5/5优秀默认启用辅助功能;类名变为 Qt6*
Qt 5.15+4/5良好辅助功能模块改进
Qt 5.7–5.143/5一般需要 QT_ACCESSIBILITY=1;手动设置 objectName
Win32 / MFC3/5一般控制 ID 可访问;文本匹配常见

设置与前提条件

# Python 3.8+,仅限 Windows
pip install pywinauto pytest pytest-html Pillow pytest-timeout
# 可选:屏幕录制
# 安装 ffmpeg 并将其添加到 PATH:https://ffmpeg.org/download.html

验证 UIA 是否可访问:

from pywinauto import Desktop
Desktop(backend="uia").windows()  # 列出所有顶级窗口

安装 Accessibility Insights for Windows(免费,来自微软)——这是你在编写任何测试之前检查 UIA 元素树的 DevTools 等效工具。

可测试性设置(按框架)

你能做的最有影响力的一件事是在编写测试之前为每个交互控件提供一个稳定的 AutomationId。

WPF

<!-- XAML:x:Name 自动变为 AutomationId -->
<TextBox x:Name="usernameInput" />
<PasswordBox x:Name="passwordInput" />
<Button x:Name="btnLogin" Content="Login" />
<TextBlock x:Name="lblError" />

WinForms

// 在设计器或代码中设置
usernameInput.AccessibleName = "usernameInput";
passwordInput.AccessibleName = "passwordInput";
btnLogin.AccessibleName = "btnLogin";
lblError.AccessibleName = "lblError";

Win32 / MFC

// .rc 文件中的控件资源 ID 会作为 AutomationId 字符串公开
// IDC_EDIT_USERNAME -> AutomationId "1001"
// 优先使用 SetWindowText 设置 Name;添加 IAccessible 以获得更丰富的支持

Qt —— 见下方专门章节


页面对象模型

tests/
├── conftest.py          # 应用程序启动夹具,失败截图
├── pytest.ini
├── config.py
├── pages/
│   ├── __init__.py      # 导入所需
│   ├── base_page.py     # 定位器、等待、截图辅助方法
│   ├── login_page.py
│   └── main_page.py
├── tests/
│   ├── __init__.py
│   ├── test_login.py
│   └── test_main_flow.py
└── artifacts/           # 截图、视频、日志

base_page.py

import os, time
from pywinauto import Desktop
from config import ACTION_TIMEOUT, ARTIFACT_DIR

class BasePage:
    def __init__(self, window):
        self.window = window

    # --- 定位器(优先级顺序)---

    def by_id(self, auto_id, **kw):
        """AutomationId —— 最稳定。作为首选使用。"""
        return self.window.child_window(auto_id=auto_id, **kw)

    def by_name(self, name, **kw):
        """可见文本 / 可访问名称。"""
        return self.window.child_window(title=name, **kw)

    def by_class(self, cls, index=0, **kw):
        """控件类 + 索引 —— 脆弱,尽可能避免。"""
        return self.window.child_window(class_name=cls, found_index=index, **kw)

    # --- 等待 ---

    def wait_visible(self, spec, timeout=ACTION_TIMEOUT):
        spec.wait("visible", timeout=timeout)
        return spec

    def wait_gone(self, spec, timeout=ACTION_TIMEOUT):
        spec.wait_not("visible", timeout=timeout)
        return spec

    def wait_window(self, title, timeout=ACTION_TIMEOUT):
        """等待一个新顶级窗口(对话框、子窗口)。"""
        dlg = Desktop(backend="uia").window(title=title)
        dlg.wait("visible", timeout=timeout)
        return dlg

    def wait_until(self, fn, timeout=ACTION_TIMEOUT, interval=0.3):
        """轮询任意条件 —— 在 UIA 事件不可靠时使用。"""
        deadline = time.time() + timeout
        while time.time() < deadline:
            try:
                if fn():
                    return True
            except Exception:
                pass
            time.sleep(interval)
        raise TimeoutError(f"条件在 {timeout}s 内未满足")

    # --- 操作 ---

    def click(self, spec):
        self.wait_visible(spec)
        spec.click_input()

    def type_text(self, spec, text):
        self.wait_visible(spec)
        ctrl = spec.wrapper_object()
        try:
            ctrl.set_edit_text(text)
        except Exception as e:
            # Qt 5.x 回退:UIA Value Pattern 可能不完整
            import sys, pywinauto.keyboard as kb
            print(f"[windows-desktop-e2e] set_edit_text 失败 ({e}),使用键盘回退", file=sys.stderr)
            ctrl.click_input()
            kb.send_keys("^a")
            kb.send_keys(text, with_spaces=True)

    def get_text(self, spec):
        ctrl = spec.wrapper_object()
        for attr in ("window_text", "get_value"):
            try:
                v = getattr(ctrl, attr)()
                if v:
                    return v
            except Exception:
                pass
        return ""

    # --- 工件 ---

    def screenshot(self, name):
        os.makedirs(ARTIFACT_DIR, exist_ok=True)
        path = os.path.join(ARTIFACT_DIR, f"{name}.png")
        self.window.capture_as_image().save(path)
        return path

login_page.py

from pages.base_page import BasePage

class LoginPage(BasePage):
    @property
    def username(self): return self.by_id("usernameInput")

    @property
    def password(self): return self.by_id("passwordInput")

    @property
    def btn_login(self): return self.by_id("btnLogin")

    @property
    def error_label(self): return self.by_id("lblError")

    def login(self, user, pwd):
        self.type_text(self.username, user)
        self.type_text(self.password, pwd)
        self.click(self.btn_login)

    def login_ok(self, user, pwd, main_title="Main Window"):
        self.login(user, pwd)
        return self.wait_window(main_title)

    def login_fail(self, user, pwd):
        self.login(user, pwd)
        self.wait_visible(self.error_label)
        return self.get_text(self.error_label)

conftest.py

对于新项目,优先使用 Tier 1 沙箱夹具(见下文)——它可以零成本添加文件系统隔离。此基本夹具仅用于最小/传统设置。

import os, pytest
os.environ["QT_ACCESSIBILITY"] = "1"  # 为 Qt 5.x UIA 支持所需

from pywinauto import Application
from config import APP_PATH, MAIN_WINDOW_TITLE, LAUNCH_TIMEOUT, ARTIFACT_DIR

@pytest.fixture
def app(request):
    if not APP_PATH:
        pytest.exit("未设置 APP_PATH 环境变量", returncode=1)
    proc = Application(backend="uia").start(APP_PATH, timeout=LAUNCH_TIMEOUT)
    win  = proc.window(title=MAIN_WINDOW_TITLE)
    win.wait("visible", timeout=LAUNCH_TIMEOUT)
    yield win
    # 失败时截图
    if getattr(getattr(request.node, "rep_call", None), "failed", False):
        os.makedirs(ARTIFACT_DIR, exist_ok=True)
        try:
            win.capture_as_image().save(
                os.path.join(ARTIFACT_DIR, f"FAIL_{request.node.name}.png")
            )
        except Exception:
            pass
    # 优雅退出,失败时强制终止
    # proc 是一个 pywinauto Application —— 使用 wait_for_process_exit(),而不是 wait_for_process()
    try:
        win.close()
        proc.wait_for_process_exit(timeout=5)
    except Exception:
        proc.kill()

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    setattr(item, f"rep_{outcome.get_result().when}", outcome.get_result())

config.py

import os
APP_PATH          = os.environ.get("APP_PATH", "")           # 通过环境变量设置 —— 无默认路径
MAIN_WINDOW_TITLE = os.environ.get("APP_TITLE", "")
LAUNCH_TIMEOUT    = int(os.environ.get("LAUNCH_TIMEOUT", "15"))
ACTION_TIMEOUT    = int(os.environ.get("ACTION_TIMEOUT", "10"))
ARTIFACT_DIR      = os.path.join(os.path.dirname(__file__), "artifacts")

pytest.ini

[pytest]
testpaths = tests
markers =
    smoke: 关键路径的快速冒烟测试
    flaky: 已知不稳定的测试
addopts = -v --tb=short --html=artifacts/report.html --self-contained-html

定位器策略

AutomationId  >  Name (文本)  >  ClassName + 索引  >  XPath
  (稳定)           (可读)            (脆弱)            (最后手段)

使用 Accessibility Insights 检查 → 属性 面板 → 首先查找 AutomationId。

# 运行时检查 —— 粘贴到 REPL 中探索树
win.print_control_identifiers()
# 或缩小范围:
win.child_window(auto_id="groupBox1").print_control_identifiers()

等待模式

# 等待控件出现
page.wait_visible(page.by_id("statusLabel"))

# 等待控件消失(例如加载动画)
page.wait_gone(page.by_id("spinnerOverlay"))

# 等待对话框弹出
dlg = page.wait_window("Confirm Delete")

# 自定义条件(例如文本变化)
page.wait_until(lambda: page.get_text(page.by_id("lblStatus")) == "Ready")

切勿将 time.sleep() 作为主要同步手段 —— 使用 wait() 或 wait_until()。

工件管理

# 按需截图
page.screenshot("after_login")

# 全屏截图(当窗口被遮挡或最小化时)
import pyautogui
pyautogui.screenshot("artifacts/fullscreen.png")

# 使用 ffmpeg 进行屏幕录制(测试前开始,测试后停止)
import subprocess

def start_recording(name):
    return subprocess.Popen([
        "ffmpeg", "-f", "gdigrab", "-framerate", "10",
        "-i", "desktop", "-y", f"artifacts/videos/{name}.mp4"
    ], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

def stop_recording(proc):
    proc.stdin.write(b"q"); proc.stdin.flush(); proc.wait(timeout=10)

逐步骤跟踪(可选启用)

默认的失败截图对于诊断不稳定测试来说往往信息不足。下面的步骤级跟踪默认关闭——仅在复现不稳定案例时启用。

启用

E2E_TRACE=1 pytest tests/test_login.py -v
# 在 JSONL 日志中包含输入的文本(不要在键入凭证/PII 的测试上使用):
E2E_TRACE=1 E2E_TRACE_INCLUDE_TEXT=1 pytest ...

注入到 BasePage 中

import os, json, time
TRACE_ENABLED      = os.environ.get("E2E_TRACE") == "1"
TRACE_INCLUDE_TEXT = os.environ.get("E2E_TRACE_INCLUDE_TEXT") == "1"

class BasePage:
    _step = 0

    def _trace(self, action, spec=None, text=None):
        if not TRACE_ENABLED:
            return
        BasePage._step += 1
        idx = f"{BasePage._step:03d}"
        os.makedirs(ARTIFACT_DIR, exist_ok=True)
        try:
            self.window.capture_as_image().save(
                os.path.join(ARTIFACT_DIR, f"step_{idx}_{action}.png"))
        except Exception:
            pass  # 捕获失败不得中断测试
        rec = {
            "ts": time.time(), "step": BasePage._step, "action": action,
            "locator": getattr(spec, "criteria", None),
            "text": text if TRACE_INCLUDE_TEXT else ("<redacted>" if text else None),
        }
        with open(os.path.join(ARTIFACT_DIR, "trace.jsonl"), "a") as f:
            f.write(json.dumps(rec) + "\n")

    def click(self, spec):
        self.wait_visible(spec); self._trace("click_before", spec)
        spec.click_input();      self._trace("click_after",  spec)

    def type_text(self, spec, text):
        self.wait_visible(spec); self._trace("type_before", spec, text)
        # ... 现有的 set_edit_text / 键盘回退 ...
        self._trace("type_after", spec)

注意事项

  • PII / 凭证:type_text 内容默认以 <redacted> 显示。切勿在登录或支付流程上设置 E2E_TRACE_INCLUDE_TEXT=1。
  • 开销:每个操作约 50–200ms + 每个步骤一个 PNG 文件在磁盘上。不要在默认的 CI 矩阵上启用——仅在专用的不稳定复现任务上启用。
  • 工件膨胀:长流程可能产生数十 MB;相应调整 retention-days。
  • 并行/重试清理:这个简单示例会追加到 trace.jsonl 并使用类级计数器。在重试前清理工件目录,并为并行测试使用每个工作者的独立工件目录。
  • 覆盖缺口:在 BasePage 外部执行的操作(测试代码中的原始 pywinauto 调用)不会被跟踪。

不稳定测试处理

# 隔离 —— 相当于 Playwright 的 test.fixme()
@pytest.mark.skip(reason="不稳定:慢速 CI 上的动画竞争条件。Issue #42")
def test_animated_transition(self, app): ...

# 跳过 i

…

[已截断以进行翻译]

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

相关技能