114

How can I use Espresso to click a specific view inside a RecyclerView item? I know I can click the item at position 0 using:

onView(withId(R.id.recyclerView)) .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));

But I need to click on a specific view inside that item and not on the item itself.

Thanks in advance.

-- edit --

To be more precise: I have a RecyclerView (R.id.recycler_view) which items are CardView (R.id.card_view). Inside each CardView I have four buttons (amongst other things) and I want to click on a specific button (R.id.bt_deliver).

I would like to use the new features of Espresso 2.0, but I'm not sure that is possible.

If not possible, I wanna use something like this (using Thomas Keller code):

onRecyclerItemView(R.id.card_view, ???, withId(R.id.bt_deliver)).perform(click());

but I don't know what to put on the question marks.

Filipe Ramos
  • 1,566
  • 2
  • 16
  • 22

9 Answers9

151

You can do it with customize view action.

public class MyViewAction {

    public static ViewAction clickChildViewWithId(final int id) {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                return null;
            }

            @Override
            public String getDescription() {
                return "Click on a child view with specified id.";
            }

            @Override
            public void perform(UiController uiController, View view) {
                View v = view.findViewById(id);
                v.performClick();
            }
        };
    }

}

Then you can click it with

onView(withId(R.id.rv_conference_list)).perform(
            RecyclerViewActions.actionOnItemAtPosition(0, MyViewAction.clickChildViewWithId(R.id. bt_deliver)));
blade
  • 2,145
  • 1
  • 16
  • 12
  • 3
    I would remove the null check, so that if the child view is not found an exception is thrown rather than do nothing silently. – Alvaro Gutierrez Perez May 09 '16 at 13:46
  • Thanks for the answer. But struck in one problem. In onPerform() function, if I used onView(withId()) function, then the function gets struck. What is the reason for this behavior? – thedarkpassenger Sep 15 '16 at 10:23
  • 3
    works with espresso:espresso-contrib:2.2.2 but not with 2.2.1 any reasons why? I have some dependencies becuase of which i cannot use 2.2.2. – RosAng Sep 26 '16 at 18:27
  • 3
    I was getting a NullPointerException with this originally, so I had to implement getConstraints() to avoid that. The following worked for me: public Matcher getConstraints() { return isAssignableFrom(View.class); } – dfinn Dec 09 '16 at 18:09
  • The solution above is not working for me. I'm getting the following error: android.support.test.espresso.PerformException: Error performing 'android.support.test.espresso.contrib.RecyclerViewActions$ActionOnItemAtPositionViewAction@fad9df' on view 'with id: adamhurwitz.github.io.doordashlite:id/recyclerView'. – Adam Hurwitz Jul 14 '17 at 05:17
  • The root of the error above is it's saying there is "No view holder at position: 0" – Adam Hurwitz Aug 18 '17 at 05:34
72

Now with android.support.test.espresso.contrib it has become easier:

1)Add test dependency

androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.0') {
    exclude group: 'com.android.support', module: 'appcompat'
    exclude group: 'com.android.support', module: 'support-v4'
    exclude module: 'recyclerview-v7'
}

*exclude 3 modules, because very likely you already have it

2) Then do something like

onView(withId(R.id.recycler_grid))
            .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));

Or

onView(withId(R.id.recyclerView))
  .perform(RecyclerViewActions.actionOnItem(
            hasDescendant(withText("whatever")), click()));

Or

onView(withId(R.id.recycler_linear))
            .check(matches(hasDescendant(withText("whatever"))));
Andrew
  • 31,284
  • 10
  • 129
  • 99
  • 1
    Thanks, not excluding those modules results in a java.lang.IncompatibleClassChangeError for me. – hboy Feb 17 '16 at 15:40
  • 8
    and what to do to click let's say in the 5th item of the list a specific view? in provided examples I see only how to click 5th item or click and item with descendant view, but not both together, or did I miss something? – donfuxx May 24 '16 at 15:51
  • 1
    i had to do `exclude group: 'com.android.support' exclude module: 'recyclerview-v7'` – Jemshit Iskenderov Dec 21 '16 at 14:20
  • 1
    It's useless when you want to click on checkbox inside RecyclerView. – Farid Mar 11 '18 at 10:52
  • 2
    In kotlin `actionOnItemAtPosition` requires a type param. Not sure what to put there. – ZeroDivide Jan 01 '19 at 01:53
  • @ZeroDivide The position (Int) - for the test you should know exact position you are gonna click – Andrew Jan 01 '19 at 10:45
  • 1
    After `actionOnItem` should be `actionOnItem`. – Yvgen Mar 02 '20 at 13:23
  • 1
    Even though I use the actionOnItem method for withText approach, I get this error: Caused by: androidx.test.espresso.PerformException: Error performing 'scroll RecyclerView to: . However, the method for atPosition works. – Rowan Gontier May 21 '20 at 01:19
9

Here is, how I resolved issue in kotlin:

fun clickOnViewChild(viewId: Int) = object : ViewAction {
    override fun getConstraints() = null

    override fun getDescription() = "Click on a child view with specified id."

    override fun perform(uiController: UiController, view: View) = click().perform(uiController, view.findViewById<View>(viewId))
}

and then

onView(withId(R.id.recyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(position, clickOnViewChild(R.id.viewToClickInTheRow)))
Vlad Sumtsov
  • 141
  • 1
  • 5
  • I encountered E/TestRunner: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.getLocationOnScreen(int[])' on a null object reference; any idea why? – sayvortana Feb 18 '20 at 17:41
6

Try next approach:

    onView(withRecyclerView(R.id.recyclerView)
                    .atPositionOnView(position, R.id.bt_deliver))
                    .perform(click());

    public static RecyclerViewMatcher withRecyclerView(final int recyclerViewId) {
            return new RecyclerViewMatcher(recyclerViewId);
    }

public class RecyclerViewMatcher {
    final int mRecyclerViewId;

    public RecyclerViewMatcher(int recyclerViewId) {
        this.mRecyclerViewId = recyclerViewId;
    }

    public Matcher<View> atPosition(final int position) {
        return atPositionOnView(position, -1);
    }

    public Matcher<View> atPositionOnView(final int position, final int targetViewId) {

        return new TypeSafeMatcher<View>() {
            Resources resources = null;
            View childView;

            public void describeTo(Description description) {
                int id = targetViewId == -1 ? mRecyclerViewId : targetViewId;
                String idDescription = Integer.toString(id);
                if (this.resources != null) {
                    try {
                        idDescription = this.resources.getResourceName(id);
                    } catch (Resources.NotFoundException var4) {
                        idDescription = String.format("%s (resource name not found)", id);
                    }
                }

                description.appendText("with id: " + idDescription);
            }

            public boolean matchesSafely(View view) {

                this.resources = view.getResources();

                if (childView == null) {
                    RecyclerView recyclerView =
                            (RecyclerView) view.getRootView().findViewById(mRecyclerViewId);
                    if (recyclerView != null) {

                        childView = recyclerView.findViewHolderForAdapterPosition(position).itemView;
                    }
                    else {
                        return false;
                    }
                }

                if (targetViewId == -1) {
                    return view == childView;
                } else {
                    View targetView = childView.findViewById(targetViewId);
                    return view == targetView;
                }

            }
        };
    }
}
Sergey
  • 12,607
  • 2
  • 45
  • 57
3

You can click on 3rd item of recyclerView Like this:

onView(withId(R.id.recyclerView)).perform(
                RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(2,click()))

Do not forget to provide the ViewHolder type so that inference does not fail.

erluxman
  • 13,712
  • 15
  • 67
  • 99
0

All of the answers above didn't work for me so I have built a new method that searches all of the views inside a cell to return the view with the ID requested. It requires two methods (could be combined into one):

fun performClickOnViewInCell(viewID: Int) = object : ViewAction {
    override fun getConstraints(): org.hamcrest.Matcher<View> = click().constraints
    override fun getDescription() = "Click on a child view with specified id."
    override fun perform(uiController: UiController, view: View) {
        val allChildViews = getAllChildrenBFS(view)
        for (child in allChildViews) {
            if (child.id == viewID) {
                child.callOnClick()
            }
        }
    }
}


private fun  getAllChildrenBFS(v: View): List<View> {
    val visited = ArrayList<View>();
    val unvisited = ArrayList<View>();
    unvisited.add(v);

    while (!unvisited.isEmpty()) {
        val child = unvisited.removeAt(0);
        visited.add(child);
        if (!(child is ViewGroup)) continue;
        val group = child
        val childCount = group.getChildCount();
        for (i in 0 until childCount) { unvisited.add(group.getChildAt(i)) }
    }

    return visited;
}

Then final you can use this on Recycler View by doing the following:

onView(withId(R.id.recyclerView)).perform(actionOnItemAtPosition<RecyclerView.ViewHolder>(0, getViewFromCell(R.id.cellInnerView) {
            val requestedView = it
}))

You could use a callback to return the view if you want to do something else with it, or just build out 3-4 different versions of this to do any other tasks.

paul_f
  • 910
  • 9
  • 14
0

I kept trying out various methods to find why @blade's answer was not working for me, to only realize that I have an OnTouchListener(), I modified the ViewAction accordingly:

fun clickTopicToWeb(id: Int): ViewAction {

        return object : ViewAction {
            override fun getDescription(): String {...}

            override fun getConstraints(): Matcher<View> {...}

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

                view?.findViewById<View>(id)?.apply {

                    //Generalized for OnClickListeners as well
                    if(isEnabled && isClickable && !performClick()) {
                        //Define click event
                        val event: MotionEvent = MotionEvent.obtain(
                          SystemClock.uptimeMillis(),
                          SystemClock.uptimeMillis(),
                          MotionEvent.ACTION_UP,
                          view.x,
                          view.y,
                          0)

                        if(!dispatchTouchEvent(event))
                            throw Exception("Not clicking!")
                    }
                }
            }
        }
    }
Rimov
  • 31
  • 3
0

You can even generalize this approach to support more actions not only click. Here is my solution for this:

fun <T : View> recyclerChildAction(@IdRes id: Int, block: T.() -> Unit): ViewAction {
  return object : ViewAction {
    override fun getConstraints(): Matcher<View> {
      return any(View::class.java)
    }

    override fun getDescription(): String {
      return "Performing action on RecyclerView child item"
    }

    override fun perform(
        uiController: UiController,
        view: View
    ) {
      view.findViewById<T>(id).block()
    }
  }
 
}

And then for EditText you can do something like this:

onView(withId(R.id.yourRecyclerView))
        .perform(
            actionOnItemAtPosition<YourViewHolder>(
                0,
                recyclerChildAction<EditText>(R.id.editTextId) { setText("1000") }
            )
        )
-5

First give your buttons unique contentDescriptions, i.e. "delivery button row 5".

<button android:contentDescription=".." />

Then scroll to row:

onView(withId(R.id.recycler_view)).perform(RecyclerViewActions.scrollToPosition(5));

Then select the view based on contentDescription.

onView(withContentDescription("delivery button row 5")).perform(click());

Content Description is a great way to use Espresso's onView and make your app more accessible.

  • 5
    The content description should _not_ be used in such a way - it does not make your app more accessible for views to spout out technical or debug information for users with a11y needs - the CD for this should probably be something like "Order button". `withText("Order")` would work too, and wouldn't require you to know at test time what you're ordering. – ataulm Apr 07 '15 at 15:04