Skip to main content

windows-desktop-e2e

E2E testing for Windows native desktop apps (WPF, WinForms, Win32/MFC, Qt) using pywinauto and Windows UI Automation.

Datos de origen

Repositorio
affaan-m/ECC
Última actividad en el origen
15 de junio de 2026 a las 17:48
Idioma detectado de SKILL.md
japonés
Estrellas
276.549
Forks
41.243

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
windows-desktop-e2e
description
E2E testing for Windows native desktop apps (WPF, WinForms, Win32/MFC, Qt) using pywinauto and Windows UI Automation.
origin
ECC
# Windows デスクトップ E2E テスト **pywinauto** と Windows UI Automation(UIA)を使用したWindowsネイティブデスクトップアプリケーションのエンドツーエンドテスト。WPF、WinForms、Win32/MFC、Qt(5.x / 6.x)をカバーし、Qt固有のガイダンスは専用セクションとして提供します。 ## アクティベートするタイミング - Windowsネイティブデスクトップアプリケーションのエンドツーエンドテストを書くまたは実行するとき - デスクトップGUIテストスイートをゼロから設定するとき - 不安定または失敗するデスクトップオートメーションテストを診断するとき - 既存のアプリにテスタビリティ(AutomationId、アクセシブルな名前)を追加するとき - デスクトップエンドツーエンドを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要素のツリーを公開します: ``` テスト(Python) └── pywinauto(UIAバックエンド) └── Windows UI Automation API ← Windowsに組み込み、フレームワーク非依存 └── アプリのUIAプロバイダー ← 各フレームワークが独自に実装 └── 実行中の .exe ``` **フレームワーク別UIA品質:** | フレームワーク | AutomationId | 信頼性 | 注記 | |-----------|-------------|-------------|-------| | WPF | 5/5 | 優秀 | `x:Name` が直接AutomationIdにマッピング | | WinForms | 4/5 | 良好 | `AccessibleName` = AutomationId | | UWP / WinUI 3 | 5/5 | 優秀 | Microsoftの完全サポート | | Qt 6.x | 5/5 | 優秀 | アクセシビリティがデフォルトで有効;クラス名が `Qt6*` に変更 | | Qt 5.15+ | 4/5 | 良好 | Accessibilityモジュールが改善 | | Qt 5.7–5.14 | 3/5 | 普通 | `QT_ACCESSIBILITY=1` が必要;objectNameは手動設定 | | Win32 / MFC | 3/5 | 普通 | コントロールIDにアクセス可能;テキストマッチングが一般的 | ## セットアップと前提条件 ```bash # Python 3.8+、Windowsのみ pip install pywinauto pytest pytest-html Pillow pytest-timeout # オプション:画面録画 # ffmpegをインストールしてPATHに追加:https://ffmpeg.org/download.html ``` UIAが到達可能か確認: ```python from pywinauto import Desktop Desktop(backend="uia").windows() # すべてのトップレベルウィンドウを一覧表示 ``` **Accessibility Insights for Windows**をインストールしてください(Microsoft提供、無料)— テストを書く前にUIA要素ツリーを検査するためのDevTools相当のツールです。 ## テスタビリティのセットアップ(フレームワーク別) テストを書く前に**全てのインタラクティブなコントロールに安定したAutomationIdを設定すること**が最も効果的です。 ### WPF ```xml <!-- XAML: x:Name が自動的にAutomationIdになる --> <TextBox x:Name="usernameInput" /> <PasswordBox x:Name="passwordInput" /> <Button x:Name="btnLogin" Content="Login" /> <TextBlock x:Name="lblError" /> ``` ### WinForms ```csharp // デザイナーまたはコードで設定 usernameInput.AccessibleName = "usernameInput"; passwordInput.AccessibleName = "passwordInput"; btnLogin.AccessibleName = "btnLogin"; lblError.AccessibleName = "lblError"; ``` ### Win32 / MFC ```cpp // .rcファイルのコントロールリソースIDがAutomationId文字列として公開される // IDC_EDIT_USERNAME -> AutomationId "1001" // 名前にはSetWindowTextを優先;より豊かなサポートにはIAccessibleを追加する ``` ### Qt — 以下の専用セクションを参照 --- ## ページオブジェクトモデル ``` tests/ ├── conftest.py # アプリ起動フィクスチャ、失敗時スクリーンショット ├── 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 ```python 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 ```python 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 > 新しいプロジェクトでは**Tier 1サンドボックスフィクスチャ**(以下参照)を優先してください — 追加コストゼロでファイルシステムの分離が追加されます。この基本フィクスチャは最小限/レガシーセットアップ専用です。 ```python 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() ではなく wait_for_process_exit() を使用 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 ```python 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 ```ini [pytest] testpaths = tests markers = smoke: 重要なパスの高速スモークテスト flaky: 既知の不安定なテスト addopts = -v --tb=short --html=artifacts/report.html --self-contained-html ``` ## ロケーター戦略 ``` AutomationId > Name(テキスト) > ClassName + インデックス > XPath (安定) (可読) (脆弱) (最後の手段) ``` Accessibility Insights → **Properties** ペインで検査 → まず `AutomationId` を確認。 ```python # 実行時の検査 — REPLに貼り付けてツリーを探索 win.print_control_identifiers() # またはスコープを絞る: win.child_window(auto_id="groupBox1").print_control_identifiers() ``` ## ウェイトパターン ```python # コントロールが表示されるのを待つ page.wait_visible(page.by_id("statusLabel")) # コントロールが消えるのを待つ(ローディングスピナーなど) page.wait_gone(page.by_id("spinnerOverlay")) # ダイアログが表示されるのを待つ dlg = page.wait_window("Confirm Delete") # カスタム条件(テキストの変化など) page.wait_until(lambda: page.get_text(page.by_id("lblStatus")) == "Ready") ``` **`time.sleep()` を主要な同期手段として使用しないこと** — `wait()` または `wait_until()` を使用してください。 ## アーティファクト管理 ```python # オンデマンドスクリーンショット 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) ``` ## 不安定なテストの対処 ```python # 隔離 — PlaywrightのtestのFixmeと同等 @pytest.mark.skip(reason="不安定:遅いCIでのアニメーションレース。Issue #42") def test_animated_transition(self, app): ... # CIのみでスキップ @pytest.mark.skipif(os.environ.get("CI") == "true", reason="CIで不安定 #43") def test_heavy_load(self, app): ... ``` 一般的な原因と修正: | 原因 | 修正 | |-------|-----| | コントロールが準備できていない | `time.sleep` を `wait_visible` に置き換える | | ウィンドウがフォーカスされていない | インタラクション前に `win.set_focus()` を追加する | | アニメーション進行中 | `wait_until(lambda: not loading_indicator.exists())` | | ダイアログのタイミング | `wait_window(title, timeout=15)` | | CI環境のディスプレイが準備できていない | `DISPLAY` を設定するかCIで仮想デスクトップを使用する | ## テスト分離とサンドボックス 分離の3つの階層 — ニーズを満たす最も軽い階層を使用してください。 ### Tier 1 — ファイルシステム分離(デフォルト、常に使用) 各テストは `subprocess.Popen` と `Application.connect()` を通じて独自の `APPDATA` / `LOCALAPPDATA` / `TEMP` を取得します。pytestの `tmp_path` フィクスチャがクリーンアップを自動的に処理します。 ```python # conftest.py — 基本的な `app` フィクスチャをこれに置き換える import os, subprocess, pytest from pywinauto import Application from config import APP_PATH, APP_ARGS, APP_TITLE, LAUNCH_TIMEOUT, ACTION_TIMEOUT, ARTIFACT_DIR @pytest.fixture(scope="function") def app(request, tmp_path): """テストごとに新しいプロセス + 分離されたユーザーデータディレクトリ。""" if not APP_PATH: pytest.exit("APP_PATH が設定されていない", returncode=1) # 全てのユーザーストレージを分離されたtmpディレクトリにリダイレクト sandbox_env = os.environ.copy() sandbox_env["QT_ACCESSIBILITY"] = "1" sandbox_env["APPDATA"] = str(tmp_path / "AppData" / "Roaming")
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub