iOSがマウスで操作出来るようになったのでUIテストの自動化を試してみる

# 概要
- 結論から言うとあまり実用性はないよ
- iOS13/IPadOSからiPhone/IPadをマウスで操作することが可能になったよ
- Raspberry Pi経由でPCからマウスの信号を送ることが出来るよ
- 今まで自動テストが不可能だった機能でも検証が可能になるのでは？
- ニコニコ生放送アプリのカスタムキャスト連携配信機能を自動テストするよ

こんな感じ
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87517/84fa35fd-4910-58b2-bcde-2e7d10764d6d.gif)

# PCからiOS端末のタッチ操作を行う

## iOSでポインティングデバイスを有効にする

iOSでのマウス（ポインティングデバイス）対応は AssistiveTouch の機能として行われているので、
以下に記載された通りに「設定」>「アクセシビリティ」>「タッチ」から設定します。

https://support.apple.com/ja-jp/HT210546

ちなみに自宅にあった安いノーブランドマウスを有線で繋いだら認識しませんでした。
ちゃんとしたマウスを使いましょう。

## Rasberry Pi をマウスとして使う

PCから直接iOS端末に信号を送れると話が早いのですが、そうもいかないので Raspberry Piを間に挟むことにします。

Rasberry Pi を マウスとして使う方法についてはそのものズバリの記事があります。
https://qiita.com/sgrk/items/7a818b2a059d5b06b209
OTGとしてセットアップ後、以下のようなバイト列を `/dev/hidg0` に書き込んでやればiOS端末が操作できます。

```shell

sudo echo -ne "\x1\0\0" > /dev/hidg0 #左ボタンを押す
sudo echo -ne "\0\x1\0" > /dev/hidg0 #x軸方向に1移動
sudo echo -ne "\0\x7f\0" > /dev/hidg0 #x軸方向に127移動
sudo echo -ne "\0\xff\0" > /dev/hidg0 #x軸方向に-1移動
sudo echo -ne "\0\x81\0" > /dev/hidg0 #x軸方向に-127移動
sudo echo -ne "\0\0\x1" > /dev/hidg0 #y軸方向に1移動
```

## PCからRasberry Piに命令を送る
PCからRasberry Piに命令を送る方法はいくつか考えられますが、
今回は一番手軽に、Rasberry Pi側で実行するべきコマンド文字列をPC側で生成してからsshで実行することにします。

カーソルの移動量は一度に-127〜127の範囲しか指定できないのと、繰り返し何度もsshしていると実行に時間がかかってしまうので、以下のようなシェルスクリプトを書いて、ひとまとまりのコマンド文字列を生成してからsshするようにしました。

```shell
# ボタン状態($1)と移動量($2, $3)に対応するコマンド文字列を生成する
# 移動量については-127〜127の範囲である必要がある
move_command() {
	B=`printf "%02x\n" $1 | sed "s/.*\(..\)$/\1/"`
	X=`printf "%02x\n" $2 | sed "s/.*\(..\)$/\1/"`
	Y=`printf "%02x\n" $3 | sed "s/.*\(..\)$/\1/"`
	echo "sudo echo -ne \"\x${B}\x${X}\x${Y}\" > /dev/hidg0"
}
# 各移動量が-127〜127に収まるように move_command 複数回呼び出してコマンド文字列を生成する
move_commands() {
	X=$2; Y=$3; COMMANDS=""
	while [ "$X" -ne 0 -o "$Y" -ne 0 ]
	do
		if [ "$X" -le 0 ]; then
			DIFF_X=`if [ $X -gt -127 ]; then echo $X; else echo -127; fi`;
		else
			DIFF_X=`if [ $X -lt 127 ]; then echo $X; else echo 127; fi`;
		fi
		if [ "$Y" -le 0 ]; then
			DIFF_Y=`if [ $Y -gt -127 ]; then echo $Y; else echo -127; fi`;
		else
			DIFF_Y=`if [ $Y -lt 127 ]; then echo $Y; else echo 127; fi`;
		fi
  
		COMMAND=`move_command $DIFF_X $DIFF_Y $1`
  		COMMANDS="$COMMANDS $COMMAND;"
  		X=$(($X - $DIFF_X)); Y=$(($Y - $DIFF_Y))
	done
	echo $COMMANDS
}
# ボタン状態($1)と移動量($2, $3)を指定してマウスを操作する
move() {
	COMMANDS=`move_commands $1 $2 $3`
	ssh "pi@192.168.1.2" "$COMMANDS"
}
```
# iOS端末にどのような操作を行うかPC側で決定する
## 座標を計算する
↑でカーソルの移動量という言葉を使いましたが、どの程度の移動量を指定すると実際に端末上でどの程度カーソルが移動するのかよくわかりません。

ちなみにこの移動量は前述の設定画面にあるこのUIでも変化します。
これが少ない方が移動に関するコマンドを送る回数が少なくて済むので、右端に設定しておくのが便利そうです。

<img width="414" alt="mouse_speed.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87517/75052a65-42b4-9d8b-4375-6b7d9941de75.jpeg">

カーソル座標系での移動量は、わざわざ書くまでもないですが、以下の式で求めれそうな気がします。

```{カーソル座標系でのX/Y} = {カーソル座標系の横/縦幅} × {描画座標系でのX/Y} / {描画座標系の横/縦幅}```

`{カーソルの座標系における縦幅/横幅}`を調べるには実際に動かして測れば良さそうなので実際に前述のコマンドを繰り返し実行してカーソルを端から端まで移動させてみたところ、左端（亀）の設定で横幅/縦幅ともに**6000**, 右端（うさぎ）の設定で横幅/縦幅ともに**300**になりました。特にiPhoneだと縦横比が随分と違うのにマウスカーソルの移動座標の幅は同じというのは意外ですね。

また、手元でiPhone 11とiPad OS Pro (12.9inch)で試してもそれぞれ同じだったので、この値は恐らく端末に依らず一定と思われます。

## どこをタップ（クリック）するか決める

座標計算方法はわかったので、画面のどこをタップさせたいかということを考えていきます。方法としては以下が考えられます。

1. 決め打ちで直接座標を指定する
2. iOS端末画面の映像から判断する
    1. 特定のパターン画像の表示位置を検出する
    2. 特定の文字列の表示位置を検出する

当たり前ですが1.が楽ですし、アプリ操作による結果をサーバー側でリクエストするなどの手段でPC側から確認できる場合はそれもアリなんじゃないかと思います。

とはいえそれではアプリ側の表示が期待動作になっているか知ることが出来ないので、2.が欲しいところです。

このあたりまで深く話し始めると収拾が付かなくなるし、そのあたりをちゃんと実装するには余力がなかったので、今回は「PCからRaspberry Pi経由でiOSを操作する」ことを主題にすることにして、この記事では詳細は省略します。

あくまでデモ向けにシェルスクリプトで乱雑に書いたコードになりますが、一応Githubに上げておきました。
（画面ミラーリングでPC側に映像を写し、それをスクショしてパターン認識するという単純な構造になってます。）
https://github.com/sgymtic/ios-automation-by-raspberrypi

# UIテストの自動化に使ってみる
ということでタイトルの通り、UIテストの自動化を試します。
## 検証する機能
ニコニコ生放送アプリの、カスタムキャスト連携配信機能を検証します。

ニコニコ生放送
https://apps.apple.com/jp/app/id823479272

カスタムキャスト
https://apps.apple.com/jp/app/id1436588949

配信機能の流れは以下の動画がわかりやすいかと思います。
https://www.youtube.com/watch?v=wT5_y9964RA

この一連の手順の検証は、以下の理由により既存の手法では自動テストすることが困難でした。

1. 端末の画面を収録して配信する機能がシミュレーターでは動作しない
2. (セキュリティ上の都合から？)最後はiOS側の提供するUI上でのユーザー操作が必要である
3. 別のアプリからの呼び出しが必要である

1.の事情により実機で検証するしかないものの、2.の事情があるので Appium などの既存の実機テストの手法を用いても自動テストすることが出来ません。
3.に関しては、呼び出される側のアプリの立場としてはその呼び出し時の仕様さえ満たしておけばよく、こちら側でそれをモックしてやればよいのですが、実際の動作と同じ条件で検証することが望ましいでしょう。

## 検証の流れ

手順を説明するだけでも長くなってしまうので、以下の実際の検証手順を記述したシェルスクリプトで何となくイメージを掴んでいただければと思います。
`tap`, `swipe`, `point` などはこちらで定義した処理で、それぞれのパラメータには座標(ポイントで指定、負数は右/下端からの距離)、座標（画面上の比率で指定）、パターン画像、文字列が指定出来る感じです。

ちなみに nicolive_broadcast.png はこんな画像です。
<img width="65" alt="nicolive_broadcast.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87517/85efdcff-a73a-7621-8fe7-c2d8306b5db9.png">

```shell
tap "カスタムキャスト" "(0,-44)"
tap "(-50,80)" #歯車アイコンが薄くて画像で検出出来なかったので座標指定している
tap "配信開始"
tap "はい"
tap "patterns/nicolive_broadcast.png"
nicolive_broadcast_point=`point "ブロードキャストを開始"` #後で画面ミラーリングを切断するので座標を記憶しておく
swipe "(95%,0%)" "(95%,50%)" #画面の右上から下にスワイプしてコントロールセンターを表示する
tap "自動テスト" # PC上で動作させている画面ミラーリング受信ソフトウェアで設定している名前
tap "ミラーリングを停止" #ここから先はもうアプリの画面を知る方法はない…
tap "(90%,90%)" #画面端をタップしてコントロールセンターを閉じる
tap "${nicolive_broadcast_point}"
tap "(90%,90%)" #画面端をタップして画面収録UIを閉じる
```

前述の通り今回はパターン認識に画面ミラーリングを使っているのですが、画面収録機能によるライブ配信を行う場合はiOSの仕様で同時に画面ミラーリングを行うことが出来ないので、自分からミラーリングを切断しに行くという無理やりな検証手順になってしまいました。

# デモ

<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://t.co/DwR8qU35OM">pic.twitter.com/DwR8qU35OM</a></p>&mdash; sgymtic (@sgymtic_com) <a href="https://twitter.com/sgymtic_com/status/1203301701882540039?ref_src=twsrc%5Etfw">December 7, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 感想

今回とりあえずPC側からアプリを操作し自動テストを試すという目的は達成しましたが、やはり色々と無理があるので、これを実際の検証に役立てるのは厳しいかなとは思います。

とはいえ前述したように、例えば画面収録を利用したライブ配信機能のような通常の手法で検証が困難な一部の機能について、このような自動テストを行う価値はあるのかもしれません。