Communitygithub.com

windows-desktop-e2e

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

windows-desktop-e2e 是什么?

windows-desktop-e2e 是一个 AI Agent 技能,专为使用 pywinauto 和 Windows UI Automation (UIA) 对 Windows 原生桌面应用进行端到端测试而设计。它支持 WPF、WinForms、Win32/MFC 和 Qt (5.x/6.x) 框架,并根据各框架的 UIA 支持质量提供详细的指导。该技能还提供了测试架构的最佳实践,包括页面对象模型、定位器策略(优先使用 AutomationId)、等待模式和失败截图/视频录制等产物管理,以及处理不稳定测试的策略,如隔离标记和逐步骤追踪。集成 CI/CD 也通过环境变量和 pytest 配置得到了支持。

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

在你喜欢的 AI 中提问

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

文档

Windows 桌面端到端测试

使用 pywinauto 和 Windows UI 自动化 (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 自动化 (UIA),这是一个 Windows 内置的可访问性 API。每个支持的框架都暴露一个 UIA 元素树,其中包含 Claude 可以读取和操作的属性:

您的测试 (Python)
    └── pywinauto (UIA 后端)
        └── Windows UI 自动化 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良好改进了 Accessibility 模块
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="登录" />
<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          # 应用启动 fixture,失败截图
├── 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} 秒内未满足")

    # --- 操作 ---

    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="主窗口"):
        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 沙箱 fixture(见下文)——它以零额外成本添加了文件系统隔离。此基本 fixture 仅适用于最小/遗留设置。

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  >  名称(文本) >  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("确认删除")

# 自定义条件(例如文本更改)
page.wait_until(lambda: page.get_text(page.by_id("lblStatus")) == "就绪")

永远不要使用 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 上动画竞争。问题 #42")
def test_animated_transition(self, app): ...

# 跳过重…

…[为翻译而截断]

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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能