Playwrightでネイティブアプリの操作を自動化してみる【初歩中の初歩】

スマホのネイティブアプリのテスト自動化ってできないの？という相談を最近よく頂くのと、Playwrightのお勉強したいなあという個人的な願望もあり、導入だけやってみた備忘録を残します。

::: note warn
なお、Playwrightのネイティブモバイル機能は現在AndroidのADBプロトコルを利用したものに限定されているため、Androidのみの話になります。iOSユーザーの皆さんごめんなさい;;
:::

## Step 0: 準備編 - テストを始める前に

そもそもPCとAndroidスマホをどうやって繋ぐの？という部分から。

### 1. Android SDK (adb) のセットアップ
何はともあれ、まずは`adb`コマンドを使えるようにしましょう。ちなみに`adb`は、PCからスマホに命令を送るための必須のツールです。

- **入手方法**: Android Studioの公式サイトから「**Command line tools only**」をダウンロードするのが一番手軽です。（Android Studio本体を全部インストールする必要はありません）
  - [Android Studio 公式ダウンロードページ](https://developer.android.com/studio#command-line-tools-only) (ページ下部にあります)
- **設定**:
    1. ダウンロードしたzipファイルを解凍し、適当な場所（例: `C:\android-sdk`）に置きます。
    2. 中にある `platform-tools` というフォルダのパスを、Windowsの**環境変数 `Path`** に追加します。
    3. ターミナルを再起動し、`adb --version` と打ってバージョン情報が出れば成功です。
```powershell
adb --version
# 出力例
# Android Debug Bridge version 1.0.41
# Version 35.0.2-12147458
# Installed as C:\Users\hoge\AppData\Local\Android\Sdk\platform-tools\adb.exe
# Running on Windows 10.0.22631
```

### 2. Androidデバイスの開発者向けオプションを有効化
次にスマホ側の設定。PCからのデバッグ命令を受け付けられるようにします。

1. スマホの「設定」→「デバイス情報」（または「タブレット情報」）を開きます。
2. 「ビルド番号」という項目を、**7回連続でタップ**します。（「デベロッパーになるまであと〇ステップです」と表示が変わっていきます）
3. 「これでデベロッパーになりました！」と表示されたら成功です。
4. 「設定」メニューに戻ると、新しく「**開発者向けオプション**」という項目が出現しているので、それを開きます。
5. 中にある「**USBデバッグ**」と「**ワイヤレスデバッグ**」の両方をONにしておきましょう。

### 3. Playwrightプロジェクトの作成
最後に、今回の主役であるPlaywrightのプロジェクトを準備します。

```powershell
# 適当な作業フォルダを作り、そこで以下を実行
npm init playwright@latest
```
いくつか質問されますが、基本はEnter連打でOKです。
これで、`playwright.config.ts` や `tests` フォルダなどが自動で作成されます。



## Step 1: 接続確認と初テスト

準備が整ったので、いよいよPlaywrightとデバイスを繋いでみます。

### 1. 接続確立 (adb)
ここでいきなり最初の壁にぶつかりました。当初、USBケーブルでPCとスマホを繋いで「USBデバッグ」を試したのですが、何度やっても`adb devices`でデバイスを認識してくれませんでした（どなたか有識者いらっしゃればご教授賜りたい.....）

そこで「**ワイヤレスデバッグ**」に切り替えたところ一瞬で繋がりました。。
**（重要：PCとスマホが同じWi-Fiなど、同一のネットワーク内にいる必要があります）**

1.  スマホの「開発者向けオプション」→「ワイヤレスデバッグ」を開きます。
2.  「ペア設定コードによるデバイスのペア設定」をタップすると、IPアドレスとポート番号が表示されます。（例: `192.168.1.10:45678`）
3.  PCのターミナルで、表示されたIPとポートを使って接続コマンドを打ちます。

    ```powershell
    adb connect 192.168.1.10:45678 # ←スマホに表示されたIPとポートに置き換える
    ```
4.  接続が成功したら、`adb devices`で確認します。

    ```powershell
    adb devices
    # List of devices attached
    # 192.168.1.10:45678   device  <- ワイヤレスデバッグだとIPアドレスで表示される
    ```

### 2. Playwrightから接続（初回テスト）
`adb`で認識できれば、次はPlaywrightから接続できるか試します。
いきなりテストを実行しても、中身が空だと何も起きないので、まずはおまじないとして、`tests/example.spec.ts`に以下の最小限のテストコードをコピペして保存します。

```typescript
// tests/example.spec.ts
import { test, _android } from '@playwright/test';

test('デバイス接続テスト', async () => {
  // デバイスに接続する
  const [device] = await _android.devices();
  console.log(`接続成功！ モデル名: ${device.model()}`);
  
  // デバイスとの接続を閉じる
  await device.close();
});
```
この状態でテストを実行し、ターミナルにモデル名が表示されれば成功です！

```powershell
npx playwright test
# 出力例:
# 接続成功！ モデル名: Pixel 5
```

> **【ここでの学び】**
> - ADB接続で躓いたら、USB接続に固執せず、ワイヤレスデバッグを試してみるとあっさり解決することがある。
> - `adb connect`コマンドを忘れずに！
> - Playwrightのテストは、まず「デバイスに接続して情報を表示するだけ」の最小構成で動かし、接続自体に問題がないことを確認するのが確実。

## Step 2: 本命のネイティブアプリを起動する

ブラウザの自動化はよくあるので、本題のネイティブアプリに進みます。

### 1. パッケージ名を知る
アプリを操作する前に、そもそもアプリってどうやって識別するん？と思ったら、どうやら「パッケージ名」という固有IDで識別しているらしい。

これも`adb`コマンドで調べられるらしいので調べてみます。

```powershell
# インストールされてるアプリのパッケージ名を全部出す
adb shell pm list packages

# 多すぎるので、アプリ名で絞り込むのがおすすめ（PowerShellの場合）
adb shell pm list packages | Select-String "your-app-name"
# -> package:com.example.app ←これ
```

### 2. アプリの起動と「待機」
`tests/example.spec.ts`の中身を、今度はアプリを起動するコードに書き換えていきます。

```typescript
// tests/example.spec.ts
import { test, _android } from '@playwright/test';

test('ネイティブアプリ起動テスト', async () => {
  const [device] = await _android.devices();
  console.log(`モデル名: ${device.model()}`);
  
  const pkg = 'com.example.app'; // ←さっき調べたアプリのパッケージ名に書き換える

  // テスト前に一旦アプリを強制終了させて、まっさらな状態から始める
  await device.shell(`am force-stop ${pkg}`);

  // いざ起動！
  await device.shell(`monkey -p ${pkg} -c android.intent.category.LAUNCHER 1`);

  // ★超重要：待つこと★
  // これがないと、アプリが立ち上がる前に次の処理に進んでしまう
  await device.shell('sleep 5');

  console.log('アプリを起動しました。');
  await device.close();
});
```

ここでも見事にハマりました。起動コマンドの直後にスクリーンショットを撮ったら、まだホーム画面のままでした。コンピュータの処理は人間が思うより遥かに速いので、UIが絡む部分は意図的に**待ち**の処理を入れてあげる必要があります。

> **【ここでの学び】**
> UIの起動や画面遷移など、見た目が変わる操作の直後には、必ず`sleep`や後述の「要素待機」を入れる。


## Step 3: テストの証拠を残す

「動いた！」「動かなかった！」だけだと、後から見返したときに何が起きたか分からないので、エビデンスとしてスクリーンショットや録画は超重要です。

### 1. スクリーンショットを撮ってレポートに貼る
Playwrightのテストレポートに画像を添付すると、ブラウザで結果を見ながら画像も確認できて良きです。
`tests/example.spec.ts`にさらに処理を加えていきましょう。

```typescript
// tests/example.spec.ts
import { test, _android } from '@playwright/test';
import * as fs from 'fs';

test('ネイティブアプリ起動テスト', async ({}, testInfo) => { // ← testInfo を追加
  // スクリーンショット保存用のフォルダを作成
  fs.mkdirSync('screenshots', { recursive: true });

  const [device] = await _android.devices();
  console.log(`モデル名: ${device.model()}`);
  
  const pkg = 'com.example.app';

  await device.shell(`am force-stop ${pkg}`);
  await device.shell(`monkey -p ${pkg} -c android.intent.category.LAUNCHER 1`);
  await device.shell('sleep 5');

  // スクショを撮る
  const screenshotPath = 'screenshots/app-startup.png';
  await device.screenshot({ path: screenshotPath });

  // レポートに「起動直後」という名前で画像を添付
  await testInfo.attach('起動直後', {
    path: screenshotPath,
    contentType: 'image/png'
  });

  console.log('アプリを起動し、スクリーンショットを撮りました。');
  await device.close();
});
```

> **【ここでの学び】**
> テストの証拠は必ず残す。成功・失敗に関わらず、実行前後のスクリーンショットがあれば、問題解決のスピードが段違いになる。


## Step 4: 最大の壁：ボタンが押せない問題との戦い

ここからが本番。アプリのダイアログの「次へ」ボタンを押そうとして、地獄のトラブルシューティングが始まりました。

### 試行1：ボタンのテキストでタップ (玉砕)
まず最初に試す、一番簡単な方法。普通に目に見えてるラベル名で押させてみる。
```typescript
await device.tap({ text: '次へ' });
```
結果は「見つかりませんでした」。まあ、そんなもんだろうと思った。

### 試行2：リソースIDでタップ (推奨だが…玉砕)
テキストがダメなら、次は`resource-id`。これは各UI要素に振られた固有のIDで、テキストより遥かに信頼性が高いです。

**IDの調べ方 (コマンドライン)**
1.  `adb shell uiautomator dump` を実行。
2.  `adb pull /sdcard/window_dump.xml .` でPCにファイルを持ってくる。
3.  `window_dump.xml`を開いて、`text="次へ"`の近くにある`resource-id`を探す。
    → `com.example.app:id/next_button` を発見！

これを使って再挑戦。
```typescript
await device.tap({ res: 'com.example.app:id/next_button' });
```
しかし、これも結果は「見つかりませんでした」。どうやら`device.tap`とこのアプリのボタンの相性が悪いようです（ダイアログ上のボタンだから？）

### 試行3：uiautomatorコマンドを直接実行 (惜しい！)
Playwrightの`tap`がダメなら、Androidのテストツール`uiautomator`を直接コマンドで呼び出します。
```typescript
const resourceId = 'com.example.app:id/next_button';
await device.shell(`uiautomator click --selector resourceId ${resourceId}`);
```
すると、テストログ上は「成功」しました！しかし、**実際のデバイス画面ではボタンが押されていません**。テスト自動化で一番厄介な「偽りの成功（False Positive）」ってやつです。

### 試行4：最終手段、座標でタップ (勝利！)
もう要素がどうとか言ってられません。最後の手段は、**画面の特定の座標を物理的にタップする**ことです。
`window_dump.xml`には、ボタンの表示領域`bounds`も書かれています。

`bounds="[125,1662][955,1777]"`

この対角線の中心を計算すれば、ボタンのど真ん中の座標が分かります。
- X座標: `(125 + 955) / 2 = 540`
- Y座標: `(1662 + 1777) / 2 = 1719.5` (まあ1719でいいでしょう)

これを`input tap`コマンドで実行します。
```typescript
// まずは念のため、ボタンが存在することだけ確認
const resourceId = 'com.example.app:id/next_button';
await device.shell(`uiautomator dump && cat /sdcard/window_dump.xml | grep 'resource-id="${resourceId}"'`);

// 存在したら、座標を直接叩く！
await device.shell('input tap 540 1719');
```
これで、ついに「次へ」ボタンをタップすることに成功しました！

> **【学び(?)】**
> UI操作の自動化には「優先順位」がある。
> 1.  まず`tap`など高レベルなAPIを試す。
> 2.  ダメなら`resource-id`を使う。
> 3.  それでもダメなら`uiautomator`コマンドを直接叩く。
> 4.  最後の手段は`座標指定タップ`。
> この引き出しの多さが、一筋縄ではいかないアプリの自動化を成功させる鍵となる。



## まとめと今後の展望

というわけで、紆余曲折ありましたが、なんとかPlaywrightでAndroidネイティブアプリの基本的な操作（起動→ボタンクリック）までを自動化することができました。

正直、Webブラウザの自動化に比べると、UI要素の特定でハマる部分が多く、一筋縄ではいかない印象です。ただ、一度`resource-id`や最終手段の`座標タップ`まで経験してしまえば、大抵のことは何とかなるんじゃないか、という自信もつきました。

今後は、

-   今回作ったテストをベースに、ログインフォームへのテキスト入力(`device.fill()`)やアサーション（期待値のチェック）を追加して、もっと「テストらしい」テストに育てていく。
-   複数のテストケースを並列実行させてみる。
-   CI/CDツール（GitHub Actionsなど）と連携させて、夜間に自動でテストが走る環境を構築する。

あたりに挑戦してみたいですね。
この備忘録が、これから同じ道を通る誰かの助けになれば幸いです。