Windows Application Driver (WinAppDriver) を利用してウインドウズアプリを操作する

Microsoft 謹製ウインドウズアプリ操作用 WebDriver の [Windows Application Driver (WinAppDriver)](https://github.com/Microsoft/WinAppDriver)
を利用してウインドウズアプリを操作します。
Java での実装を前提に記述していますが、他の言語でも基本的には同じです。
また、 WebDriver をある程度理解している人向けの内容です。

## インストール

WebDriver をよしなにダウンロードして起動してくれる [WebDriverManager](https://github.com/bonigarcia/webdrivermanager) は対応していないので、自前でダウンロードし、インストールする必要があります。

1. [GitHub のリリースページ](https://github.com/Microsoft/WinAppDriver/releases) から  `WindowsApplicationDriver.msi` をダウンロードする
2. ダウンロードした `WindowsApplicationDriver.msi` を実行する

## 起動

Selenium の ChromeDriver などのブラウザ用のドライバーはクラス生成時に自動で起動してくれますが、まだそのような実装がないため、自前でドライバを起動する必要[^1]があります。

デフォルトのインストール先は `C:\Program Files (x86)\Windows Application Driver\WinAppDriver.exe` になります。

実行した際に以下のようなエラーメッセージが出た場合には、メッセージにあるように開発者モードを有効にしてからドライバを再起動する必要があります。

```
Developer mode is not enabled. Enable it through Settings and restart Windows Application Driver
Failed to initialize: 0x80004005
```

開発者モードを有効にするには [デバイスを開発用に有効にする](
https://docs.microsoft.com/ja-jp/windows/uwp/get-started/enable-your-device-for-development) という Microsoft の記事を参考にしてください。

ドライバが正常に起動すると以下のようなメッセージが表示されます。

```
Windows Application Driver listening for requests at: http://127.0.0.1:4723/
Press ENTER to exit.
```

### 起動時引数

起動時引数に何も渡さずに起動すると　`http://127.0.0.1:4723/` でリッスンしますが、起動時引数で待ち受けるアドレスやポートを変更することができます。

```
WinAppDriver.exe 4727
WinAppDriver.exe 10.0.0.10 4725
WinAppDriver.exe 10.0.0.10 4723/wd/hub
```

IPアドレスを変更したい場合には管理者権限で起動する必要があります。

### 管理者権限での起動

IPアドレスを変更する際以外でも以下のような場合に管理者権限で起動する必要があります。

- 管理者権限で実行する(している)アプリを操作する

#### 管理者権限で実行する(している)アプリを操作する際の注意点

管理者権限で実行する(している)アプリを操作するにはアプリ起動時にUACの確認ダイアログが表示されてしまうため、確認ダイアログを無効にしないと操作できません。

UACの無効化については [PowerShell：Windows 10 ユーザーアカウント制御設定編](http://miyamon-se-exp.hatenablog.jp/entry/2016/08/13/200717) などを参考にしてください。

## 操作対象アプリを起動する

Windows用ドライバインスタンス生成時に渡す Capabilities に基づいて操作対象アプリが起動されます。

- Universal Windows Platform (UWP) アプリの場合

`app` にUWPアプリのIDを渡します。

```java
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("app", "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App");
WindowsDriver driver = new WindowsDriver(new URL("http://127.0.0.1:4723"), capabilities);
```

- その他のアプリの場合

`app` に操作するアプリのフルパス、 `appArguments` にアプリの起動引数、 `appWorkingDir` にアプリの作業ディレクトリを渡します。

```java
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("app", "C:\\Windows\\System32\\notepad.exe");
capabilities.setCapability("appArguments", "MyTestFile.txt");
capabilities.setCapability("appWorkingDir", "C:\\MyTestFolder\\");
WindowsDriver driver = new WindowsDriver(new URL("http://127.0.0.1:4723"), capabilities);
```

## 起動済みのアプリを操作する

常駐アプリなど起動済みのアプリを操作する場合、

1. デスクトップ操作用ドライバインスタンスを生成する
1. 起動済みアプリを表示する
1. デスクトップ生成用ドライバインスタンスから起動済みアプリのハンドルを取得する
1. 起動済みアプリのハンドルからアプリ操作用ドライバインスタンスを生成する

という手順が必要になります。

以下は Cortana を操作する際の例です。

```java
// デスクトップ操作用ドライバインスタンスを生成
DesiredCapabilities desktopCapabilities = new DesiredCapabilities();
desktopCapabilities.setCapability("app", "Root");
WindowsDriver desktopSession = new WindowsDriver(new URL("http://127.0.0.1:4723"), desktopCapabilities);

// Cortana を表示
desktopSession.getKeyboard().pressKey(Keys.COMMAND + "a" + Keys.COMMAND);

// Cortana のハンドルを取得
WebElement cortanaWindow = desktopSession.findElementByName("Cortana");
String cortanaTopLevelWindowHandle = cortanaWindow.getAttribute("NativeWindowHandle");
cortanaTopLevelWindowHandle = Integer.valueOf(cortanaTopLevelWindowHandle).toHexString();

// アプリ操作用ドライバインスタンスを生成
DesiredCapabilities appCapabilities = new DesiredCapabilities();
appCapabilities.setCapability("appTopLevelWindow", cortanaTopLevelWindowHandle);
WindowsDriver cortanaSession = new WindowsDriver(new URL("http://127.0.0.1:4723"), appCapabilities);
```

## 要素の取得

### 要素の属性の確認方法

要素の属性を確認するには Windows SDK に含まれる `inspect.exe` というツールを用います。
ダウンロードは https://developer.microsoft.com/ja-jp/windows/downloads/sdk-archive から行えます。
操作方法については [Inspectツールを使ってUIを調査するには？［Windows 8／Windows 8.1ストアアプリ開発］](http://www.atmarkit.co.jp/ait/articles/1404/03/news004.html) などを参考にしてください。

### ロケータ

要素を取得するためのAPIと要素の属性の関係について、 [公式](https://github.com/Microsoft/WinAppDriver#supported-locators-to-find-ui-elements) では以下のように説明されています。

| API                   	| 属性       	| 例      	|
|------------------------------	|----------------------------------------	|--------------	|
| FindElementByAccessibilityId 	| AutomationId                           	| AppNameTitle 	|
| FindElementByClassName       	| ClassName                              	| TextBlock    	|
| FindElementById              	| RuntimeId (decimal)                    	| 42.333896.3.1	|
| FindElementByName            	| Name                                   	| Calculator   	|
| FindElementByTagName         	| LocalizedControlType (upper camel case)	| Text         	|
| FindElementByXPath           	| Any                                    	| //Button[0]  	|

しかし、実際のところ `FindElementByTagName` については、 `LocalizedControlType` の値をそのまま利用しようとすると日本語環境だと inspect.exe には `チェック ボックス` と表示され、これをそのまま渡すとエラーになります。[^2]


## 要素の操作

要素を取得できたあとは、 `click` や `sendKeys` などのメソッドを利用して操作したり、 `getAttribute` で各種属性の取得が行えます。

### コンボボックスの選択ができない場合

コンボボックスでコンボボックスをクリック、ドロップダウン内のアイテムをクリックして選択するというような操作をしようとするとエラーは出ないが選択できないという場合があります。

```java
WindowsElement comboBox = windowsDriver.findElementByName("コンボボックス");
comboBox.click(); // コンボボックスをクリックしてドロップダウンを表示
WindowsElement item = windowsDriver.findElementByName("アイテム");
item.click(); // 選択したいアイテムをクリック
```

このような場合、 GitHub の [issue](https://github.com/Microsoft/WinAppDriver/issues/318) にあるように座標を指定してクリックしてやる必要があります。

```java
WindowsElement comboBox = windowsDriver.findElementByName("コンボボックス");
comboBox.click(); // コンボボックスをクリックしてドロップダウンを表示
WindowsElement item = windowsDriver.findElementByName("アイテム");
windowsDriver.getMouse().click(item.getCoordinates()); // 選択したいアイテムの座標をクリック
```

[^1]: Selenium の [DriverService](https://github.com/SeleniumHQ/selenium/blob/master/java/client/src/org/openqa/selenium/remote/service/DriverService.java) を利用して起動させようとしたところ、起動はするがすぐに終了してしまう事象に遭遇しています。同様の[問題](https://github.com/Microsoft/WinAppDriver/issues/367)が報告されており、現時点では別途起動する必要があります。
[^2]: `ControlType` の `UIA_` と `ControlTypeId` を取り除くという[情報](https://www.cresco.co.jp/blog/entry/3893/)もあります。