Playwrightで遠隔からAndroidデバイスをぐりぐり動かす。

この記事は、[ソフトウェアテスト Advent Calendar 2022](https://qiita.com/advent-calendar/2022/softwaretesting)の11日目の記事となります。
前日11日の記事は、@gremito さんによる「[Appium on Azure Pipelines](https://qiita.com/gremito/items/f2c981a05b8df1a21acb)」でした。本日の記事もアプリの自動操作関連です。

https://qiita.com/advent-calendar/2022/softwaretesting

https://qiita.com/gremito/items/f2c981a05b8df1a21acb


## 概要

ChromeやWebKitを自動操作できるPlaywrightはもう皆さんご存知ですよね？

https://playwright.dev/

PlaywrightにはAndroidを自動操作する機能が実は備わっていて、AndroidネイティブアプリケーションもChromeブラウザも自動操作することができます。

ただ、これまではPCに直接ADB接続されているAndroidデバイスしか自動操作することができませんでした。
ところが、割と最近リリースされたv1.28では `launchServer` （ADB接続したAndroidデバイスをWebSocket経由で自動操作する機能）が追加されました。これにより、デバイスファームのような使い方もできるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/de73d1f2-e8db-36b8-e21c-b2745f59e2b9.png)

「これはAppium以外の選択肢がもう一つできたのでは？」と思い、実際に触ってみました。

## インストール＆準備

```
$ npm install playwright-core
```

するだけです。（ブラウザをダウンロードする必要がないのでplaywrightではなくplaywright-coreを選択しています。）

https://playwright.dev/docs/api/class-android の冒頭に書かれているように

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/588d4bc8-0fe1-8043-57ba-f0d23e9bffe4.png)


まずはAndroidはエミュレータでも実機でもいいのでADB接続できる状態にしておきます。

```
% adb devices
List of devices attached
emulator-5554	device
```

そして、AndroidのChrome上で `chrome:flags` を打ち、コマンドラインアクセスを有効化します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/3a2f2326-c4ca-7811-e79c-19f9952d726b.png)

## まずは単体で自動操作してみる

```standalone.js
const playwright = require('playwright-core')

playwright._android.devices().then(async ([device]) => {
    const browserContext = await device.launchBrowser()
    const page = await browserContext.newPage()

    await page.goto('https://playwright.dev/')
    console.log('Taking browser screenshot')
    await page.screenshot({ path: 'playwright.dev.png' })
    console.log('Taking device screenshot')
    await device.screenshot({ path: 'device.png' })
    await browserContext.close()

    await device.close()
})
```

```
% node standalone.js      
Taking browser screenshot
Taking device screenshot
```

| playwright.dev.png | device.png |
|:---:|:--:|
|![playwright.dev.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/0517ef22-5594-41c1-9eac-b60caa87c7d0.png)|![device.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/401b4d7b-da7c-668f-5c6b-fca386c4c3f3.png)|

page.screenshotはブラウザのキャプチャなのに対し、device.screenshotはADBのスクリーンショット機能を使っているようですね。

## サーバーを起動する

いよいよネットワーク越しに自動操作するためのサーバーを立てます。
とはいえ、全然大したコードではありません。

```server.js
const playwright = require('playwright-core')

playwright._android.launchServer({ port: 12345, wsPath: 'my-emulator' }).then(server => {
    console.log(`Android server is now available! -> ${server.wsEndpoint()}`)
})
```

```
% node server.js 
Android server is now available! -> ws://127.0.0.1:12345/my-emulator
```

ちなみに、portもwsPathも省略できますが、省略すると以下のようにランダムなポート、ランダムなパス文字列になります。

```
% node server.js
Android server is now available! -> ws://127.0.0.1:60633/159d66315e21098a73ce0f0142c12fc7
```

ランダムでは、クライアント側のコードをいちいち変えないといけないので、デバイスファームのような使い方をするにはちょっと面倒です。なので（Playwright公式のグリッド機能を使わないなら）portとwsPathオプションは付けておくと良さそうです。

## クライアントから接続する

まずは、さっきの単体実行のコードをベースに、WebSocket接続で同じことをやってみます。

```client.js
const playwright = require('playwright-core')

playwright._android.connect('ws://localhost:12345/my-emulator').then(async (device) => {
    const browserContext = await device.launchBrowser()
    const page = await browserContext.newPage()
    await page.goto('https://playwright.dev/')
    console.log('Taking browser screenshot')
    await page.screenshot({ path: 'playwright.dev.png' })
    console.log('Taking device screenshot')
    await device.screenshot({ path: 'device.png' })
    await browserContext.close()

    await device.close()
})
```

```
% node client.js
Taking browser screenshot
Taking device screenshot
```

| playwright.dev.png | device.png |
|:---:|:--:|
|![playwright.dev.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/7eb0e734-a946-a06d-68af-b84dde47a299.png)|![device.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/a79265fa-3d38-7628-68c5-ce8823b6a257.png)|

まぁ当然同じ結果です。何の面白みもありません。

### Dockerコンテナでサーバーに接続してみる

どうせならADBの入っていない環境で違うクライアントから接続してみましょう。

https://playwright.dev/docs/docker

Playwright公式のDockerイメージを取ってきます。

```
% docker pull mcr.microsoft.com/playwright:v1.28.0-focal
% docker run -it --rm -v $(pwd):/app -w /app mcr.microsoft.com/playwright:v1.28.0-focal bash
root@8313fa40b980:/app# npm install playwright-core
```

接続先だけ、Dockerから見たサーバーの場所に変えます。

```client.js
const playwright = require('playwright-core')

playwright._android.connect('ws://host.docker.internal:12345/my-emulator').then(async (device) => {
```

あとは、実行してみると...

```
root@8313fa40b980:/app# adb
bash: adb: command not found
root@8313fa40b980:/app# node client.js 
Taking browser screenshot
Taking device screenshot
```

ADBのない環境でも無事、自動操作でスクリーンショットがとれました。

| playwright.dev.png | device.png |
|:---:|:--:|
|![playwright.dev.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/c1af4c8d-5e53-be83-a217-f8b36a68ddee.png)|![device.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/5dde2158-9ee3-8b3b-982e-955e2ca56349.png)|

## まとめ

Playwrightを使うと、SeleniumやAppiumを使うのにに比べて、とてもかんたんにAndroidのアプリ/ブラウザを操作することができます。

また、Playwright 1.28以降ではWebSocket接続でサーバー/クライアント接続ができるようになり、クライアント側にはADB不要で、サーバーをデバイスファームのようにして使うことができるようになりました。

まだ知らなかった方は一度ためしてみてはいかがでしょうか。