Using Espresso to click view inside RecyclerView item

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.

64933 次浏览

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.

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)));

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"))));

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;
}


}
};
}
}

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)))

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.

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.

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!")
}
}
}
}
}

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") }
)
)