Espresso: wait for element

7 April 2020 Stephan Petzl Leave a comment Tech-Help

It’s a common issue that Espresso does not wait for an element to show up before executing the next step. In this post we show how to make it wait.

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} 

In the run of solving some of the most common Espresso challenges, we covered how to use Espresso idling resources to tackle this issue and also had a look at the problems of idling resources.

So a cleaner 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))
}

Waiting for an element in Jetpack Compose

You can use the waitUntil function in Compose, which accepts a function that returns a boolean.

The function blocks until the specified condition is met or the specified timeout has passed.
If the main clock auto advancement is enabled, as it is by default, the clock will also continue to advance frame by frame.

Example:

// Click on a button
composeTestRule.onNodeWithText("Login").performClick()

// Wait until there's one element with a "Success" text
composeTestRule.waitUntil {
    composeTestRule
        .onAllNodesWithText("Successfully logged in")
        .fetchSemanticsNodes().size == 1
}

// Assert that the right username is shown
composeTestRule.onNodeWithText("Martin").assertExists()

 

Some more “Wait for …” solutions for Espresso:

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

Leave a Reply