9

I have a TextView that shows a "loading" string and I need to wait till this view is gone. I don't have a handle on the Asynctask because this method is running in an IntentService and sends a broadcast when the loading is finished.

Any idea about how to wait in an Espresso test for a change in a view's status? I'll need the same with some strings that will change and need to wait for that. I supposed it's similar.

Thanks for the help. There are not many examples or FAQs on the net.

Adil Hussain
  • 30,049
  • 21
  • 112
  • 147
jfcogato
  • 3,359
  • 3
  • 19
  • 19
  • 1
    Possible duplicate of [Espresso - Asserting a TextView with async loaded data](https://stackoverflow.com/questions/21004481/espresso-asserting-a-textview-with-async-loaded-data) – Nizamudeen Sherif Feb 21 '18 at 20:12

3 Answers3

3

You can define a ViewAction that loops the main thread every 50 milliseconds (or a different time of your choosing) until either the visibility of the View changes to View.GONE or a maximum amount of time elapses.

Follow the steps below to achieve this.

Step 1

Define the ViewAction, as follows:

/**
 * A [ViewAction] that waits up to [timeout] milliseconds for a [View]'s visibility value to change to [View.GONE].
 */
class WaitUntilGoneAction(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 be gone"
    }

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

        val endTime = System.currentTimeMillis() + timeout

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

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

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

/**
 * @return a [WaitUntilGoneAction] instance created with the given [timeout] parameter.
 */
fun waitUntilGone(timeout: Long): ViewAction {
    return WaitUntilGoneAction(timeout)
}
Step 3

Call on this ViewAction in your test method, as follows:

onView(withId(R.id.loadingTextView)).perform(waitUntilGone(3000L))
Next steps

Run with this concept and similarly create a WaitForTextAction class that waits until a TextView's text changes to a certain value. In this case, however, you'll probably want to change the Matcher returned by the getConstraints() function from any(View::class.java) to any(TextView::class.java).

Adil Hussain
  • 30,049
  • 21
  • 112
  • 147
  • 1
    hmm. I think ViewActions run on the main thread, so we wouldn't want to leave one running for the entire timeout period. maybe it's better to have the ViewAction just check the visibility once, and the while loop should be in the code which is calling perform? – Adam Burley Jul 26 '21 at 10:30
  • @AdamBurley: Pay attention to the `uiController.loopMainThreadForAtLeast(50)` call in Step 1 in the answer. This ensures that the `view.visibility == View.GONE` check is not made continually on the main thread but only once every 50 milliseconds. If you think 50 milliseconds is too short, you can increase this value to whatever value you think is best. – Adil Hussain Mar 25 '22 at 10:59
2

This has been answered here.

You can handle this case by registering an IdlingResource for your web service with Espresso. Take a look at this write-up.

Most likely, you'll want to use CountingIdlingResource (which uses a simple counter to track when something is idle). This sample test demonstrates how this can be done.

Community
  • 1
  • 1
ValeraZakharov
  • 3,767
  • 2
  • 15
  • 12
1

Here's how I handle this case:

public void waitForViewToDisappear(int viewId, long maxWaitingTimeMs) {
    long endTime = System.currentTimeMillis() + maxWaitingTimeMs;
    while (System.currentTimeMillis() <= endTime) {
        try {
            onView(allOf(withId(viewId), isDisplayed())).matches(not(doesNotExist()));
        } catch (NoMatchingViewException ex) {
            return; // view has disappeared
        }
    }
    throw new RuntimeException("timeout exceeded"); // or whatever exception you want
}

Note: matches(not(doesNotExist())) is kind of a "noop" matcher; it is just there to make sure the onView part actually gets run. You could equally well write a ViewAction which does nothing and enclose it in a perform call, but that would be more lines of code, hence why I went with this way.

Adam Burley
  • 5,551
  • 4
  • 51
  • 72