Espresso: wait for element

7 April 2020 stoefln Leave a comment Uncategorized

It’s a common issue that espresso does not wait for an element to show up before executing the next step. The result is an error similar to this one:


androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching: with id: com.mycompany.myapp:id/gone
    
    View Hierarchy:
    +>DecorView{id=-1, visibility=VISIBLE, width=1080, height=2160, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params={(0,0)(fillxfill) ty=BASE_APPLICATION wanim=0x10302f8
      fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
      pfl=FORCE_DRAW_STATUS_BAR_BACKGROUND}, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=3} 

We covered how to use espresso idling resources and also the problems of idling resources. A nicer way to handle this is to use ViewActions.
The ViewAction class is part of the public test framework API which allows devs to write their own ViewAction implementations when necessary. Here you can see how a wait-for-ui-element implementation would look like:

// CustomViewActions.kt: 
/**
 * This ViewAction tells espresso to wait till a certain view is found in the view hierarchy.
 * @param viewId The id of the view to wait for.
 * @param timeout The maximum time which espresso will wait for the view to show up (in milliseconds)
 */
fun waitForView(viewId: Int, timeout: Long): ViewAction {
    return object : ViewAction {
        override fun getConstraints(): Matcher {
            return isRoot()
        }

        override fun getDescription(): String {
            return "wait for a specific view with id $viewId; during $timeout millis."
        }

        override fun perform(uiController: UiController, rootView: View) {
            uiController.loopMainThreadUntilIdle()
            val startTime = System.currentTimeMillis()
            val endTime = startTime + timeout
            val viewMatcher = withId(viewId)

            do {
                // Iterate through all views on the screen and see if the view we are looking for is there already
                for (child in TreeIterables.breadthFirstViewTraversal(rootView)) {
                    // found view with required ID
                    if (viewMatcher.matches(child)) {
                        return
                    }
                }
                // Loops the main thread for a specified period of time.
                // Control may not return immediately, instead it'll return after the provided delay has passed and the queue is in an idle state again.
                uiController.loopMainThreadForAtLeast(100)
            } while (System.currentTimeMillis() < endTime) // in case of a timeout we throw an exception -> test fails
            throw PerformException.Builder()
                .withCause(TimeoutException())
                .withActionDescription(this.description)
                .withViewDescription(HumanReadables.describe(rootView))
                .build()
        }
    }
}

And then it’s actually quite simple to use:

// LoginTest.kt: 
@Test
fun shouldCloseDialog(){
    onView(withId(R.id.show_dialog_button)).perform(click())
    // here espresso needs to wait till the dialog actually shows and the close_button is visible
    onView(isRoot()).perform(waitForView(R.id.close_button, 5000))
}

Tags: ,

Like this article? there’s more where that came from.

Leave a Reply

Your email address will not be published. Required fields are marked *