IgorGanapolsky
IgorGanapolsky

Reputation: 26821

Espresso NestedScrollView

I am trying to test EditTexts in my form that are within a NestedScrollView. I am running the following code:

onView(withId(R.id.register_scroll_view)).perform(scrollTo()).perform(click());

where register_scroll_view is my NestedScrollView. However, I am getting an exception:

android.support.test.espresso.PerformException: Error performing 'scroll to' on view 'with id: com.eazyigz.myapp:id/register_scroll_view'. Caused by: java.lang.RuntimeException: Action will not be performed because the target view does not match one or more of the following constraints: (view has effective visibility=VISIBLE and is descendant of a: (is assignable from class: class android.widget.ScrollView or is assignable from class: class android.widget.HorizontalScrollView))

How do I properly devise this test so that I can test my EditTexts which need to be scrolled to to become visible?

Upvotes: 13

Views: 7577

Answers (4)

Leonardo Sibela
Leonardo Sibela

Reputation: 2189

Here's a kotlin version of @F1sher's solution with some improvements:

import android.view.View
import android.view.ViewParent
import androidx.core.view.get
import androidx.core.widget.NestedScrollView
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.util.HumanReadables
import org.hamcrest.Matcher
import org.hamcrest.Matchers

object CustomActions {

    const val TOP_X = 0

    fun nestedScrollTo(): ViewAction {
        return object : ViewAction {
            override fun getConstraints(): Matcher<View> {
                return Matchers.allOf(
                    isDescendantOfA(isAssignableFrom(NestedScrollView::class.java)),
                    withEffectiveVisibility(Visibility.VISIBLE)
                )
            }

            override fun getDescription(): String {
                return "Find parent with type " + NestedScrollView::class.java +
                        " of matched view and programmatically scroll to it."
            }

            override fun perform(uiController: UiController, view: View) {
                try {
                    findParentOrNull<NestedScrollView>(view) ?: throw Exception("Unable to find NestedScrollView parent.")
                } catch (e: Exception) {
                    throw PerformException.Builder()
                        .withActionDescription(description)
                        .withViewDescription(HumanReadables.describe(view))
                        .withCause(e)
                        .build()
                }
                uiController.loopMainThreadUntilIdle()
            }
        }
    }

    private inline fun <reified T> findFirstParentOrNull(view: View): T? {
        var parent: ViewParent? = null
        var incrementView: ViewParent? = null
        var isMatchInitialParent = false
        while (parent?.javaClass != T::class.java) {
            parent = if (isMatchInitialParent.not()) {
                isMatchInitialParent = true
                view.parent
            } else {
                incrementView?.parent
            }
            incrementView = parent
        }
        return parent as? T
    }
}

Upvotes: 1

Raphael Alencar
Raphael Alencar

Reputation: 51

Use

onView(withId(R.id.register_scroll_view))
        .perform(swipeUp(), click())

Upvotes: 5

F1sher
F1sher

Reputation: 7310

I have written single ViewAction for handling scroll to views that are children of NestedScrollView. It also takes into account that CoordinatorLayout might be a root - so you don't need to be afraid of toolbar changing it's size.

There is some code. You need to copy paste this class to your project somewhere. And then you can use it for example like that:

onView(withId(R.id.register_scroll_view))
        .perform(CustomScrollActions.nestedScrollTo, click());

Important: it is not a replacement for scrollTo() it is another scrolling ViewAction that you should use instead in cases when you deal with NestedScrollView.

So there is a ViewAction I was talking about:

public class CustomScrollActions {

    public static ViewAction nestedScrollTo() {
        return new ViewAction() {

            @Override
            public Matcher<View> getConstraints() {
                return Matchers.allOf(
                        isDescendantOfA(isAssignableFrom(NestedScrollView.class)),
                        withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE));
            }

            @Override
            public String getDescription() {
                return "Find parent with type " + NestedScrollView.class +
                        " of matched view and programmatically scroll to it.";
            }

            @Override
            public void perform(UiController uiController, View view) {
                try {
                    NestedScrollView nestedScrollView = (NestedScrollView)
                            findFirstParentLayoutOfClass(view, NestedScrollView.class);
                    if (nestedScrollView != null) {
                        CoordinatorLayout coordinatorLayout =
                                (CoordinatorLayout) findFirstParentLayoutOfClass(view, CoordinatorLayout.class);
                        if (coordinatorLayout != null) {
                            CollapsingToolbarLayout collapsingToolbarLayout =
                                    findCollapsingToolbarLayoutChildIn(coordinatorLayout);
                            if (collapsingToolbarLayout != null) {
                                int toolbarHeight = collapsingToolbarLayout.getHeight();
                                nestedScrollView.startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                                nestedScrollView.dispatchNestedPreScroll(0, toolbarHeight, null, null);
                            }
                        }
                        nestedScrollView.scrollTo(0, view.getTop());
                    } else {
                        throw new Exception("Unable to find NestedScrollView parent.");
                    }
                } catch (Exception e) {
                    throw new PerformException.Builder()
                            .withActionDescription(this.getDescription())
                            .withViewDescription(HumanReadables.describe(view))
                            .withCause(e)
                            .build();
                }
                uiController.loopMainThreadUntilIdle();
            }
        };
    }

    private static CollapsingToolbarLayout findCollapsingToolbarLayoutChildIn(ViewGroup viewGroup) {
        for (int i = 0; i < viewGroup.getChildCount(); i++) {
            View child = viewGroup.getChildAt(i);
            if (child instanceof CollapsingToolbarLayout) {
                return (CollapsingToolbarLayout) child;
            } else if (child instanceof ViewGroup) {
                return findCollapsingToolbarLayoutChildIn((ViewGroup) child);
            }
        }
        return null;
    }

    private static View findFirstParentLayoutOfClass(View view, Class<? extends View> parentClass) {
        ViewParent parent = new FrameLayout(view.getContext());
        ViewParent incrementView = null;
        int i = 0;
        while (parent != null && !(parent.getClass() == parentClass)) {
            if (i == 0) {
                parent = findParent(view);
            } else {
                parent = findParent(incrementView);
            }
            incrementView = parent;
            i++;
        }
        return (View) parent;
    }

    private static ViewParent findParent(View view) {
        return view.getParent();
    }

    private static ViewParent findParent(ViewParent view) {
        return view.getParent();
    }
}

Upvotes: 9

Be_Negative
Be_Negative

Reputation: 4972

I don't have any experience with NestedScrollView, but it appears that requestRectangleOnScreen(), which is how espresso scrolls in the regular ScrollView, should work with NestedScrollView just the same.

The only problem is that ScrollView constrain is hardcoded into the scrollTo() action and NestedScrollView doesn't inherit the regular ScrollView.

I believe the only solution here is to copy and paste entire ScrollToAction class into your own implementation of this action and replace the pesky constrain.

Upvotes: 15

Related Questions