AppiumでAndroidアプリのスクリーンショットをとってみた

#Appiumとは

[Appium](http://appium.io/)

Appiumはネイティブもしくはハイブリッドなモバイルアプリのためのオープンソーステスト自動化フレームワークです。

私は毎週毎週Androidアプリの最新版のスクリーンショットを手動で取るのが嫌になってAppium導入しました:sweat_smile:

UIテストの自動化もできますが、今回はひとまずスクリーンショット取れるところまで。

##環境構築
まずはAppiumを動かせる環境を構築します。
Node.jsをインストールしてからnpmでappiumとwd(Webdriver/Selenium 2クライアント)をインストールするだけです。

以下、公式ページから引用

```
brew install node      # get node.js
npm install -g appium  # get appium
npm install wd         # get appium client
appium &               # start appium
node your-appium-test.js
```

これでNode.jsで書いたスクリプトを動かせるのですが、スクリプトを全部自分で書くのはとても面倒です。
UIコンポーネントのパスとか自分の頭で考えたくない。。
ですので、appium-desktopをインストールして自動でスクリプトに落としてもらいましょう。

[GitHub \- appium/appium\-desktop: Appium Server and Inspector in Desktop GUIs for Mac, Windows, and Linux](https://github.com/appium/appium-desktop)

上記からダウンロードしてインストールします。

##appium-desktop
appium-desktopを起動すると以下のような画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23385/23676370-059f-3fc6-b679-a6f52cab854f.png)

「Start Server v1.15.1」をクリックしてサーバを起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23385/03f59ee6-3330-81de-af70-f29527c3d9ae.png)

右上の虫眼鏡ボタンをクリックします。
すると、以下の画面が表示されます。
「JSON Representation」にエミュレータや端末の情報をJSON形式で設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23385/2b083f33-5031-afdd-21bf-51986df7c151.png)

編集ボタンをクリックして、JSONを記載します。
JSONの記載方法は以下のサイトに詳しく解説があります。

[Desired Capabilities \- Appium](http://appium.io/docs/en/writing-running-appium/caps/)

最低限必要な項目を設定します。
今回はAndroidの設定を行います。

例）
例えばこんな感じです。値は適宜変更してください。

```json
{
    "platformName": "Android",
    "platformVersion": "9",
    "deviceName": "emulator-5554",
    "app": "/Users/miyatay/Documents/flutter_app/build/app/outputs/apk/debug/app-debug.apk"
}
```
|key|value|
|:-----------------|:------------------|
|platformName|Android|
|platformVersion|9|
|deviceName|emulator-5554|
|app |[apkの絶対パス]|




platformNameはAndroidの場合は"Android"固定
platformVersionはAndroidのOSのバージョンを設定します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23385/a56436dd-6aba-8d52-e958-fd00726e348c.png)

deviceNameは```adb devices```コマンドで表示されたデバイス名を設定します

```bash
$ adb devices
List of devices attached
emulator-5554	device
```

以下のキャプチャは上記の設定をした状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23385/a0c119bc-d91d-21ab-ba83-01229dc083d3.png)

「Start Session」ボタンをクリックしてセッションを開始します。


設定が正しいければ以下の画面が表示されます。
このアプリはAndroidStudioでFlutterプロジェクトを作成すると自動で生成されるカウントアップアプリです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23385/a2e5d067-7dbf-3813-4eaf-0caef3d81d8c.png)

もし、画面が真っ白な場合はリロードボタンをクリックしてみてください。
ボタンをクリックしてみます。
画面上部の目のマークのボタンをクリックしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23385/4474825d-0b27-01c3-03c1-0332e89195f0.png)

画面左側にカーソルを合わせると、選択できるコンポーネントが黄色く表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23385/c159bc0e-d727-f9eb-cdf0-84076a0463d0.png)


TapやSend Keysなどを選択すると動作がキャプチャされます。
画面上部にスクリプトが自動生成されるのでこちらをコピーすると同じ動作を何度でもさせることが可能になります。
Show/Hide Boilerplate Codeを選択することで初期処理をコードに含めるか否か選ぶことができます。
Showすると、ドライバーの初期化や接続の設定情報などを吐き出してくれるのでとても便利です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23385/9e9a84c1-fd62-e2e1-358f-92723ee5dade.png)

Boilerplate CodeをShowにしてコードをテキストエディタにコピーします。
今回はJS-WD(Promise)を選択しました。

こんな感じになりました。

```javascript::sample.js
// Requires the admc/wd client library
// (npm install wd)
// Then paste this into a .js file and run with Node 7.6+

const wd = require('wd');
const driver = wd.promiseChainRemote("http://localhost:4723/wd/hub");
const caps = {"platformName":"Android","platformVersion":"9","deviceName":"emulator-5554","app":"/Users/miyatay/Documents/flutter_app/build/app/outputs/apk/debug/app-debug.apk"};

async function main () {
  await driver.init(caps);
  let el1 = await driver.elementByXPath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.widget.Button");
  await el1.click();
  await driver.quit();
}

main().catch(console.log);
```

実行するとこうなりました。

```
$ node sample.js
{ Error: [elementByXPath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.widget.Button")] Error response status: 7, , NoSuchElement - An element could not be located on the page using the given search parameters. Selenium error: An element could not be located on the page using the given search parameters.
    at exports.newError (/Users/miyatay/node_modules/wd/lib/utils.js:152:13)
    at /Users/miyatay/node_modules/wd/lib/callbacks.js:94:19
    at /Users/miyatay/node_modules/wd/lib/webdriver.js:196:5
    at Request._callback (/Users/miyatay/node_modules/wd/lib/http-utils.js:89:7)
    at Request.self.callback (/Users/miyatay/node_modules/request/request.js:185:22)
    at Request.emit (events.js:197:13)
    at Request.<anonymous> (/Users/miyatay/node_modules/request/request.js:1161:10)
    at Request.emit (events.js:197:13)
    at IncomingMessage.<anonymous> (/Users/miyatay/node_modules/request/request.js:1083:12)
    at Object.onceWrapper (events.js:285:13)
  message:
   '[elementByXPath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.widget.Button")] Error response status: 7, , NoSuchElement - An element could not be located on the page using the given search parameters. Selenium error: An element could not be located on the page using the given search parameters.',
  status: 7,
  cause:
   { status: 7,
     value:
      { message:
         'An element could not be located on the page using the given search parameters.' },
     sessionId: '43f216e2-8b0b-481e-baae-63ae74b73c29' },
  inspect: [Function],
  'jsonwire-error':
   { status: 7,
     summary: 'NoSuchElement',
     detail:
      'An element could not be located on the page using the given search parameters.' } }
```

ボタンのelementが見つけられなくてエラーになっているようです。
elementが見つかるまでの待機時間を設定してみます。
[Implicit Wait \- Appium](http://appium.io/docs/en/commands/session/timeouts/implicit-wait/#set-implicit-wait-timeout)

UIAutomatorを使うとNoSuchElementが出にくくなるとの記事も見た気がしますが今回はこのまま突き進みます。。

```javascript:sample.js
// Requires the admc/wd client library
// (npm install wd)
// Then paste this into a .js file and run with Node 7.6+

const wd = require('wd');
const driver = wd.promiseChainRemote("http://localhost:4723/wd/hub");
const caps = { "platformName": "Android", "platformVersion": "9", "deviceName": "emulator-5554", "app": "/Users/miyatay/Documents/flutter_app/build/app/outputs/apk/debug/app-debug.apk" };

async function main() {
    await driver.init(caps);
    await driver.setImplicitWaitTimeout(5000); // 暗黙的な待機時間を設定
    let el1 = await driver.elementByXPath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.widget.Button");
    await el1.click();
    await driver.quit();
}

main().catch(console.log);
```

コピペで5秒に設定しました。
再度、```node sample.js```でスクリプトを起動します。
一瞬アプリが起動して終了すると思います。
これでスクリプトを動かせるようになりました。

今回はスクリーンショットを取得したいので、以下のページを参考にスクリーンショットを取得してみます。
[Screenshot \- Appium](http://appium.io/docs/en/commands/session/screenshot/)


```javascript:sample.js
// Requires the admc/wd client library
// (npm install wd)
// Then paste this into a .js file and run with Node 7.6+

const wd = require('wd');
const fs = require('fs')
const driver = wd.promiseChainRemote("http://localhost:4723/wd/hub");
const caps = { "platformName": "Android", "platformVersion": "9", "deviceName": "emulator-5554", "app": "/Users/miyatay/Documents/flutter_app/build/app/outputs/apk/debug/app-debug.apk" };

async function main() {
    await driver.init(caps);
    await driver.setImplicitWaitTimeout(5000);
    let el1 = await driver.elementByXPath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.widget.Button");

    // スクリーンショット取得
    let base64 = await driver.takeScreenshot();
    const buffer = Buffer.from(base64, 'base64');
    fs.writeFileSync("screen.jpg", buffer)

    await el1.click();
    await driver.quit();
}

main().catch(console.log);

```

これで実行すると、screen.jpgが保存されます。
あとは全画面でスクリーンショットを取るスクリプトを書くだけ:rolling_eyes:
モックサーバとの連携も必要ですね。。

以上、Appiumでスクリーンショットを取る方法でした。