LoginSignup
2
1

More than 1 year has passed since last update.

【Android Test】特定のViewが表示されるまで待機+RootViewWithoutFocusExceptionの対策

Posted at

Viewが表示されるまで待機する

ユーザの操作に応じてメッセージを表示するDialogを表示したり、別のActivityへ移動したりと新しいViewを表示するケースは多々あると思います。この挙動をUIテストで確認しようとすると、特定のViewが表示されるまで待機するようなActionが必要となります。

実装例

  1. 引数Matcher<View>で待機するViewを指定する
  2. onView()
  3. 取得したViewInteractionに対応するViewが存在するか確認する
    関数isExists()StackOverflow - Espresso does not wait until dialog is shown and failを参考に実装
  4. 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を検索するようなViewActionStackOverflow - 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()
    }
}

調べると色々な人が試行錯誤している様子…求むベストプラクティス

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1