Viewが表示されるまで待機する
ユーザの操作に応じてメッセージを表示するDialogを表示したり、別のActivityへ移動したりと新しいViewを表示するケースは多々あると思います。この挙動をUIテストで確認しようとすると、特定のViewが表示されるまで待機するようなActionが必要となります。
実装例
- 引数
Matcher<View>
で待機するViewを指定する onView()
- 取得した
ViewInteraction
に対応するViewが存在するか確認する
関数isExists()
は StackOverflow - Espresso does not wait until dialog is shown and failを参考に実装 - Viewが存在しない場合は一定時間待機して再度2.から繰り返す
fun waitForView(matcher: Matcher<View>, timeout: Long = 3000L): ViewInteraction {
val start = System.currentTimeMillis()
do {
val view = onView(matcher)
if (view.isExists()) return view
Thread.sleep(200L)
} while (System.currentTimeMillis() < start + timeout)
throw RuntimeException("timeout, no view found: $matcher")
}
多くの場合はこれで動きます
IdlingResourceの使用の検討
Thread.sleep
を直接使うのはあまりお行儀が良くないのでIdlingResource
を使用する方法もあるようです
Medium - Wait for it… IdlingResource and ConditionWatcher
ただし、状態の変換を検知する時間幅が5秒で固定となっており、テスト項目が増えると待機の時間が長くなりすぎる問題があります。
RootViewWithoutFocusException
待機したいViewが新たに表示するDialogや別Activity上にある場合、時々問題が発生します
androidx.test.espresso.base.RootViewPicker$RootViewWithoutFocusException: Waited for the root of the view hierarchy to have window focus and not request layout for 10 seconds. If you specified a non default root matcher, it may be picking a root that never takes focus. Root:
待機したいViewが乗っているwindowにfocusが当たる前にwaitForView()
を実行するとダメっぽいです。
そもそもViewInteraction
はViewインスタンスの実態に紐付いている訳ではなく、まだ存在しないViewを指定したMatcher<View>
に対してonView
関数を呼び出しても例外は発生しません。しかし、存在していないViewに対するViewInteraction#perform()
の挙動が想定できないケースがある様子。
RootViewから検索する
そこでonView(isRoot())
で必ず存在するViewTreeのルート要素を取ってきて、そこから対象Viewを検索するようなactionをperform()
する手法に置き換えます。
Viewを検索するようなViewAction
は StackOverflow - Android Espresso wait for text to appear を参考に実装。
fun waitForView(viewMatcher: Matcher<View>, rootMatcher: Matcher<Root>, timeout: Long = 3000L): ViewInteraction {
val start = System.currentTimeMillis()
do {
try {
// Activityの遷移などRootViewが変化する場合に対応するため onView を毎度繰り返す
onView(ViewMatchers.isRoot()).inRoot(rootMatcher).perform(searchView(viewMatcher))
return onView(viewMatcher).inRoot(rootMatcher)
} catch (e: NoMatchingViewException) {
Thread.sleep(200L)
}
} while (System.currentTimeMillis() < start + timeout)
throw RuntimeException("timeout, no view found: $viewMatcher")
}
fun searchView(matcher: Matcher<View>) = object : ViewAction {
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isRoot()
}
override fun getDescription(): String {
return "wait for view: $matcher"
}
override fun perform(uiController: UiController, view: View) {
for (child in TreeIterables.breadthFirstViewTraversal(view)) {
if (matcher.matches(child)) {
return
}
}
throw NoMatchingViewException.Builder()
.withRootView(view)
.withViewMatcher(matcher)
.build()
}
}
調べると色々な人が試行錯誤している様子…求むベストプラクティス