0

I started writing UI tests for Android application using Espresso framework. I'm facing an issue that, often, test fails because check that some element is present on the screen happens before that element is displayed. In my case - if I run my test 10 times - 3-4 times it will fail because it won't find an element on the screen

I know that Google suggest to use such thing like idling resources. But they also suggest to use it in a way of modifying your application code - which is kind of a shitty advice, and I don't want to create spaghetti code by mixing my application code with testing code

So my question - Is it possible to 'force' espresso to wait for an element on the screen by using Idling resources?

I was also thinking about creating some helper method with some fluent wait for an element - and then use it like

waitForElement(onView(withId(R.id.elementID))

so I would really appreciate any advice in this direction. Thanks

Raj Paliwal
  • 943
  • 1
  • 9
  • 22
Jake Green
  • 53
  • 1
  • 7
  • Does this answer your question? [Android Espresso wait for text to appear](https://stackoverflow.com/questions/49796132/android-espresso-wait-for-text-to-appear) – jeprubio Dec 03 '19 at 08:28
  • hey @jeprubio - thanks for your reply. yes I saw that thread too - and for now I've implemented the second answer from that thread which suggests to use Robot pattern. That improved stability of my tests - but still there is about 10% failures because of the same problem - Espresso can't find an element on the screen though it is present – Jake Green Dec 08 '19 at 12:12

1 Answers1

1

You can try to create a custom wait action with both IdlingResource and ViewAction. The simplest way is to create an IdlingResource callback that listens to ViewTreeObserver.OnDrawListener and determines when the app is idling by matching with a Matcher<View>:

private class ViewPropertyChangeCallback(private val matcher: Matcher<View>, private val view: View) : IdlingResource, ViewTreeObserver.OnDrawListener {

    private lateinit var callback: IdlingResource.ResourceCallback
    private var matched = false

    override fun getName() = "View property change callback"

    override fun isIdleNow() = matched

    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
        this.callback = callback
    }

    override fun onDraw() {
        matched = matcher.matches(view)
        callback.onTransitionToIdle()
    }
}

Then create a custom ViewAction to wait for a match:

fun waitUntil(matcher: Matcher<View>): ViewAction = object : ViewAction {

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

    override fun getDescription(): String {
        return StringDescription().let {
            matcher.describeTo(it)
            "wait until: $it"
        }
    }

    override fun perform(uiController: UiController, view: View) {
        if (!matcher.matches(view)) {
            ViewPropertyChangeCallback(matcher, view).run {
                try {
                    IdlingRegistry.getInstance().register(this)
                    view.viewTreeObserver.addOnDrawListener(this)
                    uiController.loopMainThreadUntilIdle()
                } finally {
                    view.viewTreeObserver.removeOnDrawListener(this)
                    IdlingRegistry.getInstance().unregister(this)
                }
            }
        }
    }
}

And perform this action on a root view:

fun waitForElement(matcher: Matcher<View>) {
    onView(isRoot()).perform(waitUntil(hasDescendant(matcher))
}

...

waitForElement(allOf(withId(R.id.elementID), isDisplayed()))

Or if the view's property can change asynchronously, you can do:

onView(withId(R.id.elementID)).perform(waitUntil(withText("textChanged!")))

Also, this action may not be suitable for activity transitions.

Aaron
  • 3,764
  • 2
  • 8
  • 25
  • Hey @Aaron, thanks for your reply. Unfortunately I was not able to make your advice work in my case. Everytime I get `W/IdlingPolicy: These resources are not idle: [View property change callback]` error and then testcase fails because of timeout – Jake Green Dec 09 '19 at 14:29
  • Oh sorry, if you use `onView(isRoot())` then our matcher has to be wrapped in `hasDescendant` in `waitUntil`. – Aaron Jan 02 '20 at 20:01