Monday 17 August 2020

Espresso tests: Wait until view is visible

Whilst searching online for some suggestions on how to wait for a particular View to become visible within an Espresso test, I noticed that all of the suggestions that defined a new ViewAction class, defined one that operated on the root view. I can see why this is necessary in situations where the View in question is not present in the view hierarchy and will enter the view hierarchy at a later point. However, if you're waiting on a View that's present in the view hierarchy to change from one state to another, it's much more elegant to match and operate on that one View specifically rather than matching and operating on the root view.

So, assuming you're waiting on a View that's present in the view hierarchy to change from INVISIBLE or GONE visibility to VISIBLE, you can define a ViewAction class as follows:

/**
 * A [ViewAction] that waits up to [timeout] milliseconds for a [View]'s visibility value to change to [View.VISIBLE].
 */
class WaitUntilVisibleAction(private val timeout: Long) : ViewAction {

    override fun getConstraints(): Matcher<View> {
        return any(View::class.java)
    }

    override fun getDescription(): String {
        return "wait up to $timeout milliseconds for the view to become visible"
    }

    override fun perform(uiController: UiController, view: View) {

        val endTime = System.currentTimeMillis() + timeout

        do {
            if (view.visibility == View.VISIBLE) return
            uiController.loopMainThreadForAtLeast(50)
        } while (System.currentTimeMillis() < endTime)

        throw PerformException.Builder()
            .withActionDescription(description)
            .withCause(TimeoutException("Waited $timeout milliseconds"))
            .withViewDescription(HumanReadables.describe(view))
            .build()
    }
}

And define a function that creates an instance of this ViewAction when called, as follows:

/**
 * @return a [WaitUntilVisibleAction] instance created with the given [timeout] parameter.
 */
fun waitUntilVisible(timeout: Long): ViewAction {
    return WaitUntilVisibleAction(timeout)
}

You can then call on this ViewAction in your test methods as follows:

onView(withId(R.id.myView)).perform(waitUntilVisible(3000L))

You can run with this concept and similarly define view actions that wait on other properties of the view to change state, e.g. waiting for the text of a TextView to change to some expected text.

For more view actions and view matchers like this one, check out the android-test-utils repository.

No comments: