WI

windows-desktop-e2e

Automates Windows desktop GUI testing using pywinauto and UI Automation.

Install

mkdir -p .claude/skills/windows-desktop-e2e && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/15906" && unzip -o skill.zip -d .claude/skills/windows-desktop-e2e && rm skill.zip

Installs to .claude/skills/windows-desktop-e2e

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

E2E testing for Windows native desktop apps (WPF, WinForms, Win32/MFC, Qt) using pywinauto and Windows UI Automation.
117 charsno explicit “when” trigger
Intermediate

Key capabilities

  • Write E2E tests for Windows desktop apps
  • Set up a desktop GUI test suite
  • Diagnose flaky desktop automation tests
  • Add testability to existing Windows apps

How it works

The skill uses pywinauto backed by Windows UI Automation (UIA) to interact with and test Windows native desktop applications.

Inputs & outputs

You give it
Windows native desktop application
You get back
E2E test suite

When to use windows-desktop-e2e

  • Write E2E test suite
  • Automate Windows GUI testing
  • Integrate desktop tests in CI
  • Test desktop controls

About this skill

Windows Desktop E2E Testing

End-to-end testing for Windows native desktop applications using pywinauto backed by Windows UI Automation (UIA). Covers WPF, WinForms, Win32/MFC, and Qt (5.x / 6.x) — with Qt-specific guidance as a dedicated section.

When to Activate

  • Writing or running E2E tests for a Windows native desktop application
  • Setting up a desktop GUI test suite from scratch
  • Diagnosing flaky or failing desktop automation tests
  • Adding testability (AutomationId, accessible names) to an existing app
  • Integrating desktop E2E into a CI/CD pipeline (GitHub Actions windows-latest)

When NOT to Use

  • Web applications → use e2e-testing skill (Playwright)
  • Electron / CEF / WebView2 apps → the HTML layer needs browser automation, not UIA
  • Mobile apps → use platform-specific tools (UIAutomator, XCUITest)
  • Pure unit or integration tests that don't need a running GUI

Core Concepts

All Windows desktop automation relies on UI Automation (UIA), a Windows-built-in accessibility API. Every supported framework exposes a tree of UIA elements with properties Claude can read and act on:

Your test (Python)
    └── pywinauto (UIA backend)
        └── Windows UI Automation API   ← built into Windows, framework-agnostic
            └── App's UIA provider      ← each framework ships its own
                └── Running .exe

UIA quality by framework:

FrameworkAutomationIdReliabilityNotes
WPF★★★★★Excellentx:Name maps directly to AutomationId
WinForms★★★★☆GoodAccessibleName = AutomationId
UWP / WinUI 3★★★★★ExcellentFull Microsoft support
Qt 6.x★★★★★ExcellentAccessibility enabled by default; class names change to Qt6*
Qt 5.15+★★★★☆GoodImproved Accessibility module
Qt 5.7–5.14★★★☆☆FairNeeds QT_ACCESSIBILITY=1; objectName manual
Win32 / MFC★★★☆☆FairControl IDs accessible; text matching common

Setup & Prerequisites

# Python 3.8+, Windows only
pip install pywinauto pytest pytest-html Pillow pytest-timeout
# Optional: screen recording
# Install ffmpeg and add to PATH: https://ffmpeg.org/download.html

Verify UIA is reachable:

from pywinauto import Desktop
Desktop(backend="uia").windows()  # lists all top-level windows

Install Accessibility Insights for Windows (free, from Microsoft) — your DevTools equivalent for inspecting the UIA element tree before writing any test.

Testability Setup (by Framework)

The single most impactful thing you can do is give every interactive control a stable AutomationId before writing tests.

WPF

<!-- XAML: x:Name becomes AutomationId automatically -->
<TextBox x:Name="usernameInput" />
<PasswordBox x:Name="passwordInput" />
<Button x:Name="btnLogin" Content="Login" />
<TextBlock x:Name="lblError" />

WinForms

// Set in designer or code
usernameInput.AccessibleName = "usernameInput";
passwordInput.AccessibleName = "passwordInput";
btnLogin.AccessibleName = "btnLogin";
lblError.AccessibleName = "lblError";

Win32 / MFC

// Control resource IDs in .rc file are exposed as AutomationId strings
// IDC_EDIT_USERNAME -> AutomationId "1001"
// Prefer SetWindowText for Name; add IAccessible for richer support

Qt — see dedicated section below


Page Object Model

tests/
├── conftest.py          # app launch fixture, failure screenshot
├── pytest.ini
├── config.py
├── pages/
│   ├── __init__.py      # required for imports
│   ├── base_page.py     # locators, wait, screenshot helpers
│   ├── login_page.py
│   └── main_page.py
├── tests/
│   ├── __init__.py
│   ├── test_login.py
│   └── test_main_flow.py
└── artifacts/           # screenshots, videos, logs

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

    # --- Locators (priority order) ---

    def by_id(self, auto_id, **kw):
        """AutomationId — most stable. Use as first choice."""
        return self.window.child_window(auto_id=auto_id, **kw)

    def by_name(self, name, **kw):
        """Visible text / accessible name."""
        return self.window.child_window(title=name, **kw)

    def by_class(self, cls, index=0, **kw):
        """Control class + index — fragile, avoid if possible."""
        return self.window.child_window(class_name=cls, found_index=index, **kw)

    # --- Waits ---

    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):
        """Wait for a new top-level window (dialogs, child windows)."""
        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):
        """Poll an arbitrary condition — use when UIA events are unreliable."""
        deadline = time.time() + timeout
        while time.time() < deadline:
            try:
                if fn():
                    return True
            except Exception:
                pass
            time.sleep(interval)
        raise TimeoutError(f"Condition not met within {timeout}s")

    # --- Actions ---

    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 fallback: UIA Value Pattern may be incomplete
            import sys, pywinauto.keyboard as kb
            print(f"[windows-desktop-e2e] set_edit_text failed ({e}), using keyboard fallback", 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 ""

    # --- Artifacts ---

    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

For new projects prefer the Tier 1 sandbox fixture (see below) — it adds filesystem isolation at zero extra cost. This basic fixture is for minimal/legacy setups only.

import os, pytest
os.environ["QT_ACCESSIBILITY"] = "1"  # Required for Qt 5.x UIA support

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 environment variable is not set", 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
    # Screenshot on failure
    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
    # Graceful exit first, force-kill as fallback
    # proc is a pywinauto Application — use wait_for_process_exit(), not 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", "")           # set via env — no default 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: fast smoke tests for critical paths
    flaky: known-unstable

---

*Content truncated.*

When not to use it

  • Web applications
  • Electron / CEF / WebView2 apps
  • Mobile apps

Prerequisites

Python 3.8+pywinautopytestAccessibility Insights for Windows

Limitations

  • Not for web applications
  • Not for Electron / CEF / WebView2 apps
  • Not for mobile apps

How it compares

This skill provides a framework for automated E2E testing of Windows desktop applications using UIA, unlike manual GUI testing.

Compared to similar skills

windows-desktop-e2e side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
windows-desktop-e2e (this skill)02moReviewIntermediate
dev26moReviewAdvanced
examples-auto-run23moReviewIntermediate
lora-manager-e2e16moReviewAdvanced

Try saying

Example prompts that trigger this skill in your AI assistant.

You might also like

dev

atopile

LLM-focused workflow for working in this repo: compile Zig, run the orchestrated test runner, consume test-report.json/html artifacts, and discover/debug ConfigFlags.

28

examples-auto-run

openai

Run python examples in auto mode with logging, rerun helpers, and background control.

23

lora-manager-e2e

willmiao

End-to-end testing and validation for LoRa Manager features. Use when performing automated E2E validation of LoRa Manager standalone mode, including starting/restarting the server, using Chrome DevTools MCP to interact with the web UI at http://127.0.0.1:8188/loras, and verifying frontend-to-backend functionality. Covers workflow validation, UI interaction testing, and integration testing between the standalone Python backend and the browser frontend.

13

migrate

alirezarezvani

Migrate from Cypress or Selenium to Playwright. Use when user mentions "cypress", "selenium", "migrate tests", "convert tests", "switch to playwright", "move from cypress", or "replace selenium".

13

mobile-use-setup

minitap-ai

Interactive setup wizard for Minitap mobile-use SDK. USE WHEN user wants to set up mobile automation, configure mobile-use SDK, connect iOS or Android devices, or create a new mobile testing project.

12

create-environments

PrimeIntellect-ai

Create or migrate verifiers environments for the Prime Lab ecosystem. Use when asked to build a new environment from scratch, port an eval or benchmark from papers or other libraries, start from an environment on the Hub, or convert existing tasks into a package that exposes load_environment and installs cleanly with prime env install.

11

Search skills

Search the agent skills registry