{
    "app": {
        "name": "windows-desktop-e2e",
        "description": "E2E testing for Windows native desktop apps (WPF, WinForms, Win32/MFC, Qt) using pywinauto and Windows UI Automation. Use when writing E2E tests for a Windows native desktop app with pywinauto or UI Automation.",
        "mode": "advanced-chat",
        "model_config": {
            "provider": "deepseek",
            "model": "deepseek-chat",
            "parameters": {
                "temperature": 0.7,
                "max_tokens": 4096
            }
        }
    },
    "instructions": "name windows-desktop-e2e description E2E testing for Windows native desktop apps (WPF, WinForms, Win32/MFC, Qt) using pywinauto and Windows UI Automation. Use when writing E2E tests for a Windows native desktop app with pywinauto or UI Automation. metadata {\"origin\":\"ECC\"} 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: Framework AutomationId Reliability Notes WPF 5/5 Excellent x:Name maps directly to AutomationId WinForms 4/5 Good AccessibleName = AutomationId UWP / WinUI 3 5/5 Excellent Full Microsoft support Qt 6.x 5/5 Excellent Accessibility enabled by default; class names change to Qt6* Qt 5.15+ 4/5 Good Improved Accessibility module Qt 5.7–5.14 3/5 Fair Needs QT_ACCESSIBILITY=1 ; objectName manual Win32 / MFC 3/5 Fair Control 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 tests addopts = -v --tb=short --html=artifacts/report.html --self-contained-html Locator Strategy AutomationId > Name (text) > ClassName + index > XPath (stable) (readable) (fragile) (last resort) Inspect with Accessibility Insights → Properties pane → look for AutomationId first. # Inspect at runtime — paste into a REPL to explore the tree win.print_control_identifiers() # or narrow scope: win.child_window(auto_id= \"groupBox1\" ).print_control_identifiers() Wait Patterns # Wait for control to appear page.wait_visible(page.by_id( \"statusLabel\" )) # Wait for control to disappear (e.g. loading spinner) page.wait_gone(page.by_id( \"spinnerOverlay\" )) # Wait for a dialog to pop up dlg = page.wait_window( \"Confirm Delete\" ) # Custom condition (e.g. text changes) page.wait_until( lambda : page.get_text(page.by_id( \"lblStatus\" )) == \"Ready\" ) Never use time.sleep() as primary synchronization — use wait() or wait_until() . Artifact Management # Screenshot on demand page.screenshot( \"after_login\" ) # Full-screen capture (when window is off-screen or minimised) import pyautogui pyautogui.screenshot( \"artifacts/fullscreen.png\" ) # Screen recording with ffmpeg (start before test, stop after) 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 ) Per-Step Trace (opt-in) The default failure screenshot is often too thin for diagnosing flaky tests. The step-level trace below is off by default — enable it only when reproducing a flaky case. Enable E2E_TRACE=1 pytest tests/test_login.py -v # Include typed text in the JSONL log (DO NOT use on tests that type credentials/PII): E2E_TRACE=1 E2E_TRACE_INCLUDE_TEXT=1 pytest ... Patch into 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(",
    "variables": [],
    "opening_statement": "你好，我是 windows-desktop-e2e，E2E testing for Windows native desktop apps (WPF, ...",
    "suggested_questions": [],
    "source": "DeepseekModel",
    "source_url": "https://deepseekmodel.com/skill?id=affaan-m-ecc-skills-windows-desktop-e2e-skill-md"
}