Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to use Espresso's IdlingResource to wait until a certain view appears?

In my test I have a stage where after pressing a button application does a lot of asynchronous calculations and requests to the cloud service, after which it displays a certain view.

Is it possible to use Espresso's IdlingResource implementation to wait until a certain view appears?

I've read an answers here and comments seems to suggest that you can use IdlingResource instead, but I don't understand how. Espresso does not seem to have any built-in way to handle long operations, but having to write your own waiting loops feels like a hack.

Any way to solve this or should I just do as the answer in the linked thread suggests?

like image 483
Rinat Veliakhmedov Avatar asked May 31 '18 15:05

Rinat Veliakhmedov


3 Answers

Atte Backenhof's solution has a small bug (or maybe I don't fully understand the logic).

getView should return a null instead of throwing an exception to make IdlingResources work.

Here's a Kotlin solution with the fix:

/**
 * @param viewMatcher The matcher to find the view.
 * @param idleMatcher The matcher condition to be fulfilled to be considered idle.
 */
class ViewIdlingResource(
    private val viewMatcher: Matcher<View?>?,
    private val idleMatcher: Matcher<View?>?
) : IdlingResource {

    private var resourceCallback: IdlingResource.ResourceCallback? = null

    /**
     * {@inheritDoc}
     */
    override fun isIdleNow(): Boolean {
        val view: View? = getView(viewMatcher)
        val isIdle: Boolean = idleMatcher?.matches(view) ?: false
        if (isIdle) {
            resourceCallback?.onTransitionToIdle()
        }
        return isIdle
    }

    /**
     * {@inheritDoc}
     */
    override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback?) {
        this.resourceCallback = resourceCallback
    }

    /**
     * {@inheritDoc}
     */
    override fun getName(): String? {
        return "$this ${viewMatcher.toString()}"
    }

    /**
     * Tries to find the view associated with the given [<].
     */
    private fun getView(viewMatcher: Matcher<View?>?): View? {
        return try {
            val viewInteraction = onView(viewMatcher)
            val finderField: Field? = viewInteraction.javaClass.getDeclaredField("viewFinder")
            finderField?.isAccessible = true
            val finder = finderField?.get(viewInteraction) as ViewFinder
            finder.view
        } catch (e: Exception) {
            null
        }
    }

}

/**
 * Waits for a matching View or throws an error if it's taking too long.
 */
fun waitUntilViewIsDisplayed(matcher: Matcher<View?>) {
    val idlingResource: IdlingResource = ViewIdlingResource(matcher, isDisplayed())
    try {
        IdlingRegistry.getInstance().register(idlingResource)
        // First call to onView is to trigger the idler.
        onView(withId(0)).check(doesNotExist())
    } finally {
        IdlingRegistry.getInstance().unregister(idlingResource)
    }
}

Usage in your UI tests:

    @Test
    fun testUiNavigation() {
        ...
        some initial logic, navigates to a new view
        ...
        waitUntilViewIsDisplayed(withId(R.id.view_to_wait_for))
        ...
        logic on the view that we waited for
        ...
    }

Important update: default timeout for the IdlingResources is 30 seconds, they don't wait forever. To increase a timeout you need to call it in @Before method for example: IdlingPolicies.setIdlingResourceTimeout(3, TimeUnit.MINUTES)

like image 95
Dmytro Karataiev Avatar answered Nov 02 '22 00:11

Dmytro Karataiev


I took inspiration from Anatolii, but instead of using methods from the View.class I still only use ViewMatchers.

/**
 * {@link IdlingResource} that idles until a {@link View} condition is fulfilled.
 */
public class ViewIdlingResource implements IdlingResource {

    private final Matcher<View>    viewMatcher;
    private final Matcher<View>    idleMatcher;
    private       ResourceCallback resourceCallback;

    /**
     * Constructor.
     *
     * @param viewMatcher The matcher to find the view.
     * @param idlerMatcher The matcher condition to be fulfilled to be considered idle.
     */
    public ViewIdlingResource(final Matcher<View> viewMatcher, Matcher<View> idlerMatcher) {
        this.viewMatcher = viewMatcher;
        this.idleMatcher = idlerMatcher;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isIdleNow() {
        View view = getView(viewMatcher);
        boolean isIdle = idleMatcher.matches(view);

        if (isIdle && resourceCallback != null) {
            resourceCallback.onTransitionToIdle();
        }

        return isIdle;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
        this.resourceCallback = resourceCallback;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getName() {
        return this + viewMatcher.toString();
    }

    /**
     * Tries to find the view associated with the given {@link Matcher<View>}.
     */
    private static View getView(Matcher<View> viewMatcher) {
        try {
            ViewInteraction viewInteraction = onView(viewMatcher);
            Field finderField = viewInteraction.getClass().getDeclaredField("viewFinder");
            finderField.setAccessible(true);
            ViewFinder finder = (ViewFinder) finderField.get(viewInteraction);
            return finder.getView();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

And how to use the idler in your test case, I pass the ViewMatchers.isDisplayed() to be my expected condition in the idler.

private void waitUntilViewIsDisplayed(Matcher<View> matcher) {
        IdlingResource idlingResource = new ViewIdlingResource(matcher, isDisplayed());
        try {
            IdlingRegistry.getInstance().register(idlingResource);
            // First call to onView is to trigger the idler.
            onView(withId(0)).check(doesNotExist());
        } finally {
            IdlingRegistry.getInstance().unregister(idlingResource);
        }
    }

With this you can pass any Matcher.class to the ViewIdlingResource constructor to be the required condition for the view found by the viewMatcher parameter.

like image 29
Atte Backenhof Avatar answered Nov 01 '22 23:11

Atte Backenhof


Your IdlingResource could look like this:

import android.support.test.espresso.IdlingResource;
import android.support.test.espresso.ViewFinder;
import android.support.test.espresso.ViewInteraction;
import android.view.View;

import org.hamcrest.Matcher;

import java.lang.reflect.Field;

import static android.support.test.espresso.Espresso.onView;

public class ViewShownIdlingResource implements IdlingResource {

    private static final String TAG = ViewShownIdlingResource.class.getSimpleName();

    private final Matcher<View> viewMatcher;
    private ResourceCallback resourceCallback;

    public ViewShownIdlingResource(final Matcher<View> viewMatcher) {
        this.viewMatcher = viewMatcher;
    }

    @Override
    public boolean isIdleNow() {
        View view = getView(viewMatcher);
        boolean idle = view == null || view.isShown();

        if (idle && resourceCallback != null) {
            resourceCallback.onTransitionToIdle();
        }

        return idle;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
        this.resourceCallback = resourceCallback;
    }

    @Override
    public String getName() {
        return this + viewMatcher.toString();
    }

    private static View getView(Matcher<View> viewMatcher) {
        try {
            ViewInteraction viewInteraction = onView(viewMatcher);
            Field finderField = viewInteraction.getClass().getDeclaredField("viewFinder");
            finderField.setAccessible(true);
            ViewFinder finder = (ViewFinder) finderField.get(viewInteraction);
            return finder.getView();
        } catch (Exception e) {
            return null;
        }
    }
}

Then, you could create a helper method waiting for your view:

public void waitViewShown(Matcher<View> matcher) {
    IdlingResource idlingResource = new ViewShownIdlingResource(matcher);///
    try {
        IdlingRegistry.getInstance().register(idlingResource);
        onView(matcher).check(matches(isDisplayed()));  
    } finally {
        IdlingRegistry.getInstance().unregister(idlingResource);
    }    
}

Finally, in your test:

@Test
public void someTest() {
    waitViewShown(withId(R.id.<some>));

    //do whatever verification needed afterwards    
} 

You could improve this example by making IdlingResource wait for any condition, not just for the visibility one.

like image 28
Anatolii Avatar answered Nov 01 '22 23:11

Anatolii