Espresso, scrolling not working when NestedScrollView or RecyclerView is in CoordinatorLayout
L

8

43

It looks like CoordinatorLayout breaks the behaviour of Espresso actions such as scrollTo() or RecyclerViewActions.scrollToPosition().

Issue with NestedScrollView

For a layout like this one:

<android.support.design.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        ...

    </android.support.v4.widget.NestedScrollView>

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        ...

    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

If I try to scroll to any view inside the NestedScrollView using ViewActions.scrollTo() the first problem I find is that I get a PerformException. This is because this action only supports ScrollView and NestedScrollView doesn't extend it. A workaround for this problem is explained here, basically we can copy the code in scrollTo() and change the constrains to support NestedScrollView. This seems to work if the NestedScrollView is not in a CoordinatorLayout but as soon as you put it inside a the CoordinatorLayout the scrolling action fails.

Issue with RecyclerView

For the same layout, if I replace the NestedScrollView with a RecyclerView there is also problems with the the scrolling.

In this case I'm using RecyclerViewAction.scrollToPosition(position). Unlike the NestedScrollView, here I can see some scrolling happening. However, it looks like it scrolls to the wrong position. For example, if I scroll to the last position, it makes visible the second to last but not the last one. When I move the RecyclerView out of the CoordinatorLayout the scrolling works as it should.

At the moment we can't write any Espresso test for the screens that use CoordinatorLayout due to this issues. Anyone experiencing the same problems or knows a workaround?

Logistic answered 8/2, 2016 at 15:14 Comment(1)
I have an issue, where the RecycleView is inside a NestedScrollview. I can't use recycleview.scrollToPosition(X); , it just doesn't work. I tried everything in the last 6 days, but I can get over it. any suggestion? I would be very thankful !Barr
C
31

This is happening because the Espresso scrollTo() method explicitly checks the layout class and only works for ScrollView & HorizontalScrollView. Internally it's using View.requestRectangleOnScreen(...) so I'd expect it to actually work fine for many layouts.

My workaround for NestedScrollView was to take ScrollToAction and modify that constraint. The modified action worked fine for NestedScrollView with that change.

Changed method in ScrollToAction class:

public Matcher<View> getConstraints() {
    return allOf(withEffectiveVisibility(Visibility.VISIBLE), isDescendantOfA(anyOf(
            isAssignableFrom(ScrollView.class), isAssignableFrom(HorizontalScrollView.class), isAssignableFrom(NestedScrollView.class))));
}

Convenience method:

public static ViewAction betterScrollTo() {
    return ViewActions.actionWithAssertions(new NestedScrollToAction());
}
Carbo answered 9/3, 2016 at 1:10 Comment(3)
Is your NestedScrollView inside a CoordinatorLayout? I tried this and it only seems to work if the NestedScrollView is not in a CoordinatorLayout.Logistic
@Logistic Works for me in a CoordinatorLayoutNutrient
This answer may help you to scroll to a nested scrollview as it contains full code for the matcher.Start
G
27

Here is how I did the same thing that @miszmaniac did in Kotlin. With delegation in Kotlin, it is much cleaner and easier because I don't have to override the methods I don't need to.

class ScrollToAction(
    private val original: android.support.test.espresso.action.ScrollToAction = android.support.test.espresso.action.ScrollToAction()
) : ViewAction by original {

  override fun getConstraints(): Matcher<View> = anyOf(
      allOf(
          withEffectiveVisibility(Visibility.VISIBLE),
          isDescendantOfA(isAssignableFrom(NestedScrollView::class.java))),
      original.constraints
  )
}
Gatt answered 23/11, 2017 at 9:0 Comment(3)
This worked for me. I also included a scrollTo() method. fun scrollTo(): ViewAction = actionWithAssertions(ScrollToAction())Genie
for androidx variant: private val original: androidx.test.espresso.action.ScrollToAction = androidx.test.espresso.action.ScrollToAction()Testudinal
Wish there was a scrollViewToTop functionOkwu
I
17

I had this issue with CoordinatorLayout->ViewPager->NestedScrollView an easy work around from me to get the same scrollTo() behavior was to just swipe up on the screen:

onView(withId(android.R.id.content)).perform(ViewActions.swipeUp());
Incommodious answered 14/12, 2016 at 21:26 Comment(2)
If you have a button in the bottom of the screen, this solution won't work =/Arbitress
this workaround worked for me but only using in combination with another. I did a copy of ActionOnItemAtPositionViewAction and disabled the scroll removing the line new ScrollToPositionViewAction(position).perform(uiController, view); So I have: onView(withId(android.R.id.content)).perform(ViewActions.swipeUp()); onView(withId(R.id.my_list)).perform(new CopyOfActionOnItemAtPositionViewAction(13, click()));Tuberosity
A
6

The solution of Mr Mido may work in some situations, but not always. If you have some view in the bottom of screen, the scroll of your RecyclerView will not happen because the click will start outside the RecyclerView.

One way to workaround this problem is to write a custom SwipeAction. Like this:

1 - Create the CenterSwipeAction

public class CenterSwipeAction implements ViewAction {

    private final Swiper swiper;
    private final CoordinatesProvider startCoordProvide;
    private final CoordinatesProvider endCoordProvide;
    private final PrecisionDescriber precDesc;

    public CenterSwipeAction(Swiper swiper, CoordinatesProvider startCoordProvide,
                             CoordinatesProvider endCoordProvide, PrecisionDescriber precDesc) {
        this.swiper = swiper;
        this.startCoordProvide = startCoordProvide;
        this.endCoordProvide = endCoordProvide;
        this.precDesc = precDesc;
    }

    @Override public Matcher<View> getConstraints() {
        return withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE);
    }

    @Override public String getDescription() {
        return "swipe from middle of screen";
    }

    @Override
    public void perform(UiController uiController, View view) {
        float[] startCoord = startCoordProvide.calculateCoordinates(view);
        float[] finalCoord = endCoordProvide.calculateCoordinates(view);
        float[] precision =  precDesc.describePrecision();

        // you could try this for several times until Swiper.Status is achieved or try count is reached
        try {
            swiper.sendSwipe(uiController, startCoord, finalCoord, precision);
        } catch (RuntimeException re) {
            throw new PerformException.Builder()
                    .withActionDescription(this.getDescription())
                    .withViewDescription(HumanReadables.describe(view))
                    .withCause(re)
                    .build();
        }

        // ensures that the swipe has been run.
        uiController.loopMainThreadForAtLeast(ViewConfiguration.getPressedStateDuration());
    }
}

2 - Create the method to return the ViewAction

    private static ViewAction swipeFromCenterToTop() {
        return new CenterSwipeAction(Swipe.FAST,
                GeneralLocation.CENTER,
                view -> {
                    float[] coordinates =  GeneralLocation.CENTER.calculateCoordinates(view);
                    coordinates[1] = 0;
                    return coordinates;
                },
                Press.FINGER);
    }

3 - Then use it to scroll the screen:

onView(withId(android.R.id.content)).perform(swipeFromCenterToTop());

And that's it! This way you can control how the scroll is going to happen in your screen.

Arbitress answered 1/5, 2017 at 18:44 Comment(0)
G
5

Barista's scrollTo(R.id.button) works on all kinds of scrollable views, also on NestedScrollView.

It's useful to fix this kind of issues with Espresso. We develop and use it just to write Espresso tests in a fast and reliable way. And here's a link: https://github.com/SchibstedSpain/Barista

Gylys answered 8/6, 2017 at 14:42 Comment(0)
L
4

This issue has been reported (perhaps by the OP?), see Issue 203684

One of the comments to that issue suggests a work-around to the problem when the NestedScrollView is inside of a CoordinatorLayout:

you need to remove the @string/appbar_scrolling_view_behavior layout behaviour of the ScrollingView or any parent view this ScrollingView is included in

Here is an implementation of that work-around:

    activity.runOnUiThread(new Runnable() {
        @Override
        public void run() {
            // remove CoordinatorLayout.LayoutParams from NestedScrollView
            NestedScrollView nestedScrollView = (NestedScrollView)activity.findViewById(scrollViewId);
            CoordinatorLayout.LayoutParams params =
                    (CoordinatorLayout.LayoutParams)nestedScrollView.getLayoutParams();
            params.setBehavior(null);
            nestedScrollView.requestLayout();
        }
    });

I was able to get my tests working by:

  1. Making a custom scrollTo() action (as referenced by the OP and Turnsole)
  2. Removing the NestedScrollView's layout params as shown here
Louanneloucks answered 9/8, 2016 at 19:34 Comment(1)
That worked well for me. I had another view between the NestedScrollView and my CoordinatorLayout, so I had to remove the layout params of this view, the direct child of the CoordinatorLayout.Modillion
F
2

I've made a NestedScrollViewScrollToAction class.

I think it's better place to make activity specific stuff there instead.

The only thing worth mentioning is that code searches for parent nestedScrollView and removes it's CoordinatorLayout behaviour.

https://gist.github.com/miszmaniac/12f720b7e898ece55d2464fe645e1f36

Frug answered 12/9, 2016 at 12:52 Comment(1)
In my case i have CoordinatorLayout->ViewPager->NestedScrollView and scrollTo() doesnt work. I updated the script and it scrolls now, thanks @miszmaniac. Updated script: gist.github.com/maydin/677c983c11d6a75c90186c09366fef2f You can use like below ViewInteraction appCompatButton3 = onView( allOf(withId(R.id.profile_btn_save), withText("Save Changes"))); appCompatButton3.perform(NestedScrollViewScrollToAction.scrollTo(),click());Mattson
B
1

I had to test recyclerview items. My RecyclerView was in NestedScrollView inside an CoordinatorLayout.

Following is the solution worked for me, I feel it is the most suitable solution to test RecyclerView items in an NestedScrollView.

Step 1 : Copy and paste the below function

Following will return the desired child view from recyclerView which we are about to test.

fun atPositionOnView(recyclerViewId: Int, position: Int, childViewIdToTest: Int): Matcher<View?>? {
    return object : TypeSafeMatcher<View?>() {
        var resources: Resources? = null
        var childView: View? = null
        override fun describeTo(description: Description?) {
            var idDescription = Integer.toString(recyclerViewId)
            if (resources != null) {
                idDescription = try {
                    resources!!.getResourceName(recyclerViewId)
                } catch (var4: Resources.NotFoundException) {
                    String.format("%s (resource name not found)",
                            *arrayOf<Any?>(Integer.valueOf(recyclerViewId)))
                }
            }
            description?.appendText("with id: $idDescription")
        }

        override fun matchesSafely(view: View?): Boolean {
            resources = view?.getResources()
            if (childView == null) {
                val recyclerView = view?.getRootView()?.findViewById(recyclerViewId) as RecyclerView
                childView = if (recyclerView != null && recyclerView.id == recyclerViewId) {
                    recyclerView.findViewHolderForAdapterPosition(position)!!.itemView
                } else {
                    return false
                }
            }

            return if (viewId == -1) {
                view === childView
            } else {
                val targetView = childView!!.findViewById<View>(viewId)
                view === targetView
            }
        }
    }
}

Step 2: Now copy and paste below function

Following will check if your child in recyclerView is being displayed or not.

fun ViewInteraction.isNotDisplayed(): Boolean {
    return try {
        check(matches(not(isDisplayed())))
        true
    } catch (e: Error) {
        false
    }
}

Step 3: Test your recyclerView Items and scroll If they are off screen

Following will scroll the non displayed child and make it appear on screen.

if (onView(atPositionOnView(R.id.rv_items, pos, R.id.tv_item_name)).isNotDisplayed()) {
            val appViews = UiScrollable(UiSelector().scrollable(true))
            appViews.scrollForward() //appViews.scrollBackward()
        }

Once your view is being displayed, you can perform your test cases.

Beaulahbeaulieu answered 4/5, 2020 at 14:3 Comment(1)
Where does variable viewId come from? It is not in the scope of the first function.Brufsky

© 2022 - 2024 — McMap. All rights reserved.