AppiumにPull Requestを出して改良されたWebView処理の話
こんにちは、Magic Podの開発をしている脇坂です。
本日、Appium 1.19.0がリリースされました。このリリースでは、弊社でプルリクエストしたAndroid向けのmobile: getContextsというAPIが追加されています。今回はAppiumのドキュメントに書かなかった部分も含めて、この新機能について紹介します。
AppiumのWebViewアプリテストのおさらい(知ってる人は読み飛ばし可)
WebViewとは、モバイルアプリの画面要素(HTMLタグみたいなもの)のことで、Webのコンテンツを表示することが可能です。早い話、Webの技術でモバイルアプリを作ることができます。
そして、Webのコンテンツなので、Webアプリケーションとしてテストすることも可能です。つまり、技術的にはSeleniumでWebView内の画面を操作することもできます。
もうひとつ、Webアプリケーションとしてテストする大前提として、(こちらの方が大事なのですが)モバイル端末からWebコンテンツをいかに取得するか、という話があります。Androidの場合、Chrome DevTools Protocolを使っています。
Chrome DevToolsは Chromeブラウザで「chrome://inspect」と叩くと画面に表示されます。もしかしたら皆さんの中にも使ったことがあるかも知れません。WebView要素が組み込まれたアプリの画面を端末で表示すると※1 、以下のようにWebViewのWebコンテンツを取得することができます。
ざっくりですが、AppiumのWebViewアプリテストというのはこうした技術を応用したものになります。
プルリクエストの背景
そもそも端末のWebコンテンツは色々見えます。例えば、下記の例だとフォアグラウンドアプリとして立ち上がっているChromeとバックグラウンドアプリとして立ち上がっている弊社テストアプリのWebコンテンツ両方が見えています。
また、1画面内に複数のWebViewがある場合にも、同様に複数個のWebコンテンツを見えるようになります。このように複数個のWebコンテンツが存在する場合、どのWebコンテンツに対して画面操作を行うか、というのを指定する必要があります。方法としては、そのWebコンテンツのURLやタイトルを頼りに対象のWebコンテンツを絞り込むテクニックが知られています※2。ようはURLやタイトルは普通被らないだろう、という前提があるわけですね。
確かにほとんどの場合、それらは被らないのですが、実は次の画像のように、イレギュラーなケースで被ることがあります。
この状態はWebViewのある同じ画面に複数回行き来すると再現します。これらのWebコンテンツのうち、「detached」と表示されているのは古いWebコンテンツなのでしばらくすると破棄されます。そのWebコンテンツは古いものなので、そのWebコンテンツを通じて端末の画面操作をすることはできません※3。このdetachedという状態ですが、実は今までAppiumのAPIでは確認することができませんでした。2つ同じWebコンテンツがある時にどちらが正しいWebコンテンツかわからなかったわけです。※4
Magic Pod DesktopアプリでWebViewの画面をキャプチャされたことのある方は、次のような画面を見たことがあるかも知れません。
あまり直感的ではないこのメッセージは、「detached」なWebコンテンツが見えていて、どれが正しいWebコンテンツか区別がつかない場合に表示されます。テスト対象のアプリを閉じれば治るのですが、あまり良いユーザ体験ではなかったと思います。Android WebViewの画面キャプチャは不安定であるという印象を与えてしまっていたと思います。※5
解決策
必要なことはWebコンテンツがdetachedかわかるかどうかです。Chromeのインスペクタで見えるのだから、Appiumからも同様にその情報を取得できていいはずです。Appiumのサーバログを眺めると、以下のようなログを見つけました。実体はChromedriverのログです。※6
[Chromedriver] [STDERR] [1603149866.964][DEBUG]: DevTools HTTP Request: http://localhost:60005/json [Chromedriver] [STDERR] [1603149866.969][DEBUG]: DevTools HTTP Response: [ { [Chromedriver] [STDERR] "description": "{\"attached\":true,\"empty\":false,\"height\":1392,\"screenX\":0,\"screenY\":384,\"visible\":true,\"width\":1080}", [Chromedriver] [STDERR] "devtoolsFrontendUrl": "http://chrome-devtools-frontend.appspot.com/serve_rev/@22955682f94ce09336197bfb8dffea991fa32f0d/inspector.html?ws=localhost:60009/devtools/page/0B25BBA6F33C2A83D34ED848FB0D5EE3", [Chromedriver] [STDERR] "faviconUrl": "https://github.githubassets.com/favicons/favicon.png", [Chromedriver] [STDERR] "id": "0B25BBA6F33C2A83D34ED848FB0D5EE3", [Chromedriver] [STDERR] "title": "Releases · appium/appium · GitHub", [Chromedriver] [STDERR] "type": "page", [Chromedriver] [STDERR] "url": "https://github.com/appium/appium/releases", [Chromedriver] [STDERR] "webSocketDebuggerUrl": "ws://localhost:60009/devtools/page/0B25BBA6F33C2A83D34ED848FB0D5EE3" [Chromedriver] [STDERR] } ]
http://localhost:60005/json のJSONレスポンスの中にdescription.attached: trueという値があります。なので、このレスポンスを取れるエンドポイントを叩けば目的は達成できそうですね。最終的にChrome DevToolsがそのエンドポイントを持っていることがわかりました。以前、AppiumがChrome DevToolsを利用してensureWebviewsHavePagesなるcapabilityを追加していたので、それに手を加えてmobile: getContextsを作ることにしました。
mobile: getContextsの返り値について
Android向けのmobile: getContexts APIは次のようなレスポンスを返します。
[ { "proc": "@webview_devtools_remote_22138", "webview": "WEBVIEW_22138", "info": { "Android-Package": "io.appium.settings", "Browser": "Chrome/74.0.3729.185", "Protocol-Version": "1.3", "User-Agent": "Mozilla/5.0 (Linux; Android 10; Android SDK built for x86 Build/QSR1.190920.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.185 Mobile Safari/537.36", "V8-Version": "7.4.288.28", "WebKit-Version": "537.36 (@22955682f94ce09336197bfb8dffea991fa32f0d)", "webSocketDebuggerUrl": "ws://127.0.0.1:10005/devtools/browser" }, "pages": [ { "description": "{\"attached\":true,\"empty\":false,\"height\":1458,\"screenX\":0,\"screenY\":336,\"visible\":true,\"width\":1080}", "devtoolsFrontendUrl": "http://chrome-devtools-frontend.appspot.com/serve_rev/@22955682f94ce09336197bfb8dffea991fa32f0d/inspector.html?ws=127.0.0.1:10900/devtools/page/27325CC50B600D31B233F45E09487B1F", "id": "27325CC50B600D31B233F45E09487B1F", "title": "Releases · appium/appium · GitHub", "type": "page", "url": "https://github.com/appium/appium/releases", "webSocketDebuggerUrl": "ws://127.0.0.1:10900/devtools/page/27325CC50B600D31B233F45E09487B1F" } ], "webviewName": "WEBVIEW_com.io.appium.setting" } ]
このレスポンスは Chrome DevTools Protocol のGET /json/versionとGET /jsonが元となっています。リスト構造になっており、Android端末で起動(バックグラウンドを含む)しているアプリの数だけリストの要素が増えます。ドキュメントが見つからなかったので、いくつか分かってる範囲でざっくりかいつまむと以下の通りです。
- webviewName: コンテキスト名。setContextで使う
- info.Android-Package: Webコンテンツを持つAUTのパッケージ名
- pages: Webコンテンツのリスト
- id: WebコンテンツのID。"CDwindow-" + id とするとwindow handle名に相当するので setWindowHandleメソッドに使える
- title: Webコンテンツのタイトル
- url: WebコンテンツのURL
- description: Webコンテンツのメタ?情報が詰まった文字列。Mobile Chromeの場合は空文字になる模様
- attached: 基本的にtrue。Webコンテンツがdetachedになったらfalse
- visible: 基本的にtrue。Webコンテンツがバックグラウンドに移動したり、画面外に出たらfalse
- height, screenX, screenY, width: WebView要素の位置とサイズに相当
やり残したこと
まだAppium1.19.0を試せてはいないのですが、Magic Podでは Version 0.62.0 で同様の機能を追加しています。概ね期待値通り動いている印象ですが、まれにWebコンテンツが存在するにもかかわらず、pagesが空のリストになる場合があるようです。こちらで把握している再現手順※7 を端末のChromeのバージョンを切り替えてながら試したところ、次のように再現するケースと再現しないケースがあることがわかりました。
Chrome Version | 結果 |
---|---|
83.0.4103.39 | 再現しない |
84.0.4147.30 | 再現しない |
85.0.4183.87 | 再現する |
86.0.4240.22 | 再現する |
87.0.4280.20(beta) | 再現しない |
表1: 2020/10/20 時点のもの
まだ原因は調べられていないですが、Chromeの一時的な問題か、今回の実装がChromeの変更に追随できていない可能性があります。あと、再現手順に Magic Pod も絡んでいるので、Magic Pod側に何かしら問題がある可能性もあります。
Magic Podではこの問題が起きた時には フォールバックとして既存のGet Window Handles APIを使うようにしています。テスト実行中に以下のようなメッセージが出てる時にそれをやっています。※8
まとめ
さて、今回はAppium 1.19.0に追加された mobile: getContexts を紹介しました。これにより従来のGet All Contexts API と Get Window Handles APIよりも詳細なWebコンテンツの情報が取得可能になりました。機会あれば使ってみてください。やり残した課題は別の機会に調べて解決できればと考えてます。
宣伝
今年のソフトウェアテスト自動化カンファレンスにて、AppiumのWebViewアプリテストの話をする予定です。なんと800人近い参加登録がされてますが、まだ枠はあるようなので、よかったら参加してみてください。
注釈・出典
- 実際には少し設定が必要。参考
- https://appiumpro.com/editions/73-working-with-multile-webviews-in-android-hybrid-apps
- より正確にはfind elementなどの命令は受け付けるが画面は動かない
- 両方のWebコンテンツに対して画面操作を行う剛の者手法もとれるかもしれませんが。。
- 特にMagic Podのクラウド端末。現状だとコンソールログが見えないので。
- AndroidのWebViewテストでは内部でChromeDriverが使われています
- ローカルのAndroid端末をMagic Pod Desktopアプリに接続し、Magic Podでテストケース編集画面からWebView画面を含むAndroidのテストを2回行う。
- ちなみにMagic Podのクラウド端末は、少し別の仕組みを使っているため、この問題が起きる可能性は低いと思います