Communitygithub.com

windows-desktop-e2e

為 Windows 原生桌面應用程式(WPF、WinForms、Win32/MFC、Qt)使用 pywinauto 和 Windows UI Automation 進行端到端測試。當您需要為 Windows 原生桌面應用程式編寫 E2E 測試時使用。

windows-desktop-e2e 是什麼?

這是一項針對 Windows 原生桌面應用程式的端到端測試技能,採用 pywinauto 作為核心函式庫,並以 Windows UI Automation (UIA) 作為底層自動化引擎。它支援 WPF、WinForms、Win32/MFC 和 Qt(5.x/6.x)等多種架構,並提供針對 Qt 的專屬章節。此技能詳細說明了如何在 CI/CD 流程(如 GitHub Actions 的 windows-latest 執行器)中整合桌面 E2E 測試,並涵蓋了從專案設定、測試性植入、Page Object Model 架構到定位策略、等待模式、失敗截圖與影片記錄等完整環節。此外,還提供了可選的逐步追蹤功能,用於診斷不穩定的測試,以及處理已知不穩定測試的隔離與重試策略。整體而言,這項技能讓開發者能夠以結構化且可靠的方式,為 Windows 桌面應用程式建立自動化測試套件。

相容平台✓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極佳全面的 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 元素樹的開發者工具等效項。

可測試性設定(依框架分類)

在編寫測試之前,您可以做的最具影響力的事情是為每個可互動的控制項提供一個穩定的 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 — 請參閱下方的專屬章節


Page Object Model

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", "")           # 透過 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  >  名稱(文字)  >  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")) == "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.

相關技能