Communitygithub.com

windows-desktop-e2e

使用 pywinauto 和 Windows UI Automation 對 Windows 原生桌面應用程式(WPF、WinForms、Win32/MFC、Qt)進行端對端測試。

windows-desktop-e2e 是什麼?

這個技能提供了使用 Python 的 pywinauto 函式庫(後端為 Windows UI Automation)對 Windows 原生桌面應用程式進行端對端 (E2E) 測試的完整指南與程式碼樣板。它支援 WPF、WinForms、Win32/MFC 及 Qt(5.x/6.x)等主流架構,並針對不同框架提供了特定的測試性設定建議。技能內容涵蓋了專案設定、頁面物件模型 (Page Object Model) 的實作、定位策略、等待模式、失敗截圖等工件管理,以及重試機制等處理不穩定測試的技巧。此技能特別適用於需要從零開始建立桌面 GUI 測試套件、診斷不穩定的自動化測試,或將桌面 E2E 測試整合至 CI/CD 管線(如 GitHub Actions)的開發者。其定位相當明確,僅專注於 Windows 原生桌面應用,不適用於網頁、Electron 或行動裝置應用程式。

相容平台✓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 Automation (UIA))對 Windows 原生桌面應用程式進行端對端測試。涵蓋 WPF、WinForms、Win32/MFC 和 Qt(5.x / 6.x)— 其中 Qt 有專門章節提供指引。

何時啟用

  • 為 Windows 原生桌面應用程式撰寫或執行 E2E 測試
  • 從頭開始建立桌面 GUI 測試套件
  • 診斷不穩定或失敗的桌面自動化測試
  • 為現有應用程式加入可測試性(AutomationId、易於存取的命名)
  • 將桌面 E2E 整合至 CI/CD 管線(GitHub Actions 的 windows-latest)

何時不使用

  • 網頁應用程式 → 使用 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極佳完整的 Microsoft 支援
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(免費,來自 Microsoft)— 這是您在撰寫任何測試之前檢查 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}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="主視窗"):
        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

對於新專案,建議優先使用第 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", "")           # 透過 env 設定 — 無預設路徑
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 + index  >  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 上的競態。Issue #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.

相關技能