Android Espresso wait for text to appear

Anna Puskarjova picture Anna Puskarjova · Apr 12, 2018 · Viewed 12.9k times · Source

I am trying to automate an Android app that is a chatbot using Espresso. I can say that I am completely new to Android app automation. Right now I am struggled with waiting. If I use Thread.sleep, it works perfectly fine. However, I would like to wait until a specific text appears on the screen. How can I do that?

@Rule
public ActivityTestRule<LoginActivity> mActivityTestRule = new ActivityTestRule<>(LoginActivity.class);

@Test
public void loginActivityTest() {
ViewInteraction loginName = onView(allOf(withId(R.id.text_edit_field),
childAtPosition(childAtPosition(withId(R.id.email_field),0), 1)));
loginName.perform(scrollTo(), replaceText("[email protected]"), closeSoftKeyboard());

ViewInteraction password= onView(allOf(withId(R.id.text_edit_field),
childAtPosition(childAtPosition(withId(R.id.password_field),0), 1)));
password.perform(scrollTo(), replaceText("12345678"), closeSoftKeyboard());

ViewInteraction singInButton = onView(allOf(withId(R.id.sign_in), withText("Sign In"),childAtPosition(childAtPosition(withId(R.id.scrollView), 0),2)));
singInButton .perform(scrollTo(), click());

//Here I need to wait for the text "Hi ..."

Some explanations: after pressing the sign in button, the chatbot says "Hi" and gives some more information. I would like to wait for the last one message to appear on the screen.

Answer

manbradcalf picture manbradcalf · Jun 7, 2019

I like @jeprubio's answer above, however I ran into the same issue @desgraci mentioned in the comments, where their matcher is consistently looking for a view on an old, stale rootview. This happens frequently when trying to have transitions between activities in your test.

My implementation of the traditional "Implicit Wait" pattern lives in the two Kotlin files below.

EspressoExtensions.kt contains a function searchFor which returns a ViewAction once a match has been found within supplied rootview.

class EspressoExtensions {

    companion object {

        /**
         * Perform action of waiting for a certain view within a single root view
         * @param matcher Generic Matcher used to find our view
         */
        fun searchFor(matcher: Matcher<View>): ViewAction {

            return object : ViewAction {

                override fun getConstraints(): Matcher<View> {
                    return isRoot()
                }

                override fun getDescription(): String {
                    return "searching for view $matcher in the root view"
                }

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

                    var tries = 0
                    val childViews: Iterable<View> = TreeIterables.breadthFirstViewTraversal(view)

                    // Look for the match in the tree of childviews
                    childViews.forEach {
                        tries++
                        if (matcher.matches(it)) {
                            // found the view
                            return
                        }
                    }

                    throw NoMatchingViewException.Builder()
                        .withRootView(view)
                        .withViewMatcher(matcher)
                        .build()
                }
            }
        }
    }
}

BaseRobot.kt calls the searchFor() method, checks if a matcher was returned. If no match is returned, it sleeps a tiny bit and then fetches a new root to match on until it has tried X times, then it throws an exception and the test fails. Confused about what a "Robot" is? Check out this fantastic talk by Jake Wharton about the Robot pattern. Its very similar to the Page Object Model pattern

open class BaseRobot {

    fun doOnView(matcher: Matcher<View>, vararg actions: ViewAction) {
        actions.forEach {
            waitForView(matcher).perform(it)
        }
    }

    fun assertOnView(matcher: Matcher<View>, vararg assertions: ViewAssertion) {
        assertions.forEach {
            waitForView(matcher).check(it)
        }
    }

    /**
     * Perform action of implicitly waiting for a certain view.
     * This differs from EspressoExtensions.searchFor in that,
     * upon failure to locate an element, it will fetch a new root view
     * in which to traverse searching for our @param match
     *
     * @param viewMatcher ViewMatcher used to find our view
     */
    fun waitForView(
        viewMatcher: Matcher<View>,
        waitMillis: Int = 5000,
        waitMillisPerTry: Long = 100
    ): ViewInteraction {

        // Derive the max tries
        val maxTries = waitMillis / waitMillisPerTry.toInt()

        var tries = 0

        for (i in 0..maxTries)
            try {
                // Track the amount of times we've tried
                tries++

                // Search the root for the view
                onView(isRoot()).perform(searchFor(viewMatcher))

                // If we're here, we found our view. Now return it
                return onView(viewMatcher)

            } catch (e: Exception) {

                if (tries == maxTries) {
                    throw e
                }
                sleep(waitMillisPerTry)
            }

        throw Exception("Error finding a view matching $viewMatcher")
    }
}

To use it

// Click on element withId
BaseRobot().doOnView(withId(R.id.viewIWantToFind, click())

// Assert element withId is displayed
BaseRobot().assertOnView(withId(R.id.viewIWantToFind, matches(isDisplayed()))

I know that IdlingResource is what Google preaches to handle asynchronous events in Espresso testing, but it usually requires that you have test specific code (i.e hooks) embedded within your app code in order to synchronize the tests. That seems weird to me, and working on a team with a mature app and multiple developers committing code everyday, it seems like it would be a lot of extra work to retrofit idling resources everywhere in the app just for the sake of tests. Personally, I prefer to keep the app and test code as separate as possible. /end rant