How to assert inside a RecyclerView in Espresso?
JavaAndroidAndroid EspressoJava Problem Overview
I am using espresso-contrib to perform actions on a RecyclerView
, and it works as it should, ex:
//click on first item
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
and I need to perform assertions on it. Something like this:
onView(withId(R.id.recycler_view))
.perform(
RecyclerViewActions.actionOnItemAtPosition(
0, check(matches(withText("Test Text")))
)
);
but, because RecyclerViewActions is, of course, expecting an action, it says wrong 2nd argument type. There's no RecyclerViewAssertions
on espresso-contrib.
Is there any way to perform assertions on a RecyclerView
?
Java Solutions
Solution 1 - Java
Pretty easy. No extra library is needed. Do:
onView(withId(R.id.recycler_view))
.check(matches(atPosition(0, withText("Test Text"))));
if your ViewHolder uses ViewGroup, wrap withText()
with a hasDescendant()
like:
onView(withId(R.id.recycler_view))
.check(matches(atPosition(0, hasDescendant(withText("Test Text")))));
with method you may put into your Utils
class.
public static Matcher<View> atPosition(final int position, @NonNull final Matcher<View> itemMatcher) {
checkNotNull(itemMatcher);
return new BoundedMatcher<View, RecyclerView>(RecyclerView.class) {
@Override
public void describeTo(Description description) {
description.appendText("has item at position " + position + ": ");
itemMatcher.describeTo(description);
}
@Override
protected boolean matchesSafely(final RecyclerView view) {
RecyclerView.ViewHolder viewHolder = view.findViewHolderForAdapterPosition(position);
if (viewHolder == null) {
// has no item on such position
return false;
}
return itemMatcher.matches(viewHolder.itemView);
}
};
}
If your item may be not visible on the screen at first, then scroll to it before:
onView(withId(R.id.recycler_view))
.perform(scrollToPosition(87))
.check(matches(atPosition(87, withText("Test Text"))));
Solution 2 - Java
You should check out Danny Roa's solution Custom RecyclerView Actions And use it like this:
onView(withRecyclerView(R.id.recycler_view)
.atPositionOnView(1, R.id.ofElementYouWantToCheck))
.check(matches(withText("Test text")));
Solution 3 - Java
Just to enhance riwnodennyk's answer, if your ViewHolder
is a ViewGroup
instead of a direct View
object like TextView
, then you can add hasDescendant
matcher to match the TextView
object in the ViewGroup
. Example:
onView(withId(R.id.recycler_view))
.check(matches(atPosition(0, hasDescendant(withText("First Element")))));
Solution 4 - Java
I have been struggling on the same issue where I have a recycler view with 6 items and then I have to choose the one at position 5 and the 5th one is not visible on the view.
The above solution also works. Also, I know its a bit late but thought its worth sharing.
I did this with a simple solution as below:
onView(withId(R.id.recylcer_view))
.perform(RecyclerViewActions.actionOnItem(
hasDescendant(withText("Text of item you want to scroll to")),
click()));
RecyclerViewActions.actionOnItem
-
Performs a ViewAction
on a view matched by viewHolderMatcher
.
- Scroll
RecyclerView
to the view matched byitemViewMatcher
- Perform an action on the matched view
More details can be found at : RecyclerViewActions.actionOnItem
Solution 5 - Java
In my case, it was necessary to merge Danny Roa's and riwnodennyk's solution:
onView(withId(R.id.recyclerview))
.perform(RecyclerViewActions.scrollToPosition(80))
.check(matches(atPositionOnView(80, withText("Test Test"), R.id.targetview)));
and the method :
public static Matcher<View> atPositionOnView(final int position, final Matcher<View> itemMatcher,
final int targetViewId) {
return new BoundedMatcher<View, RecyclerView>(RecyclerView.class) {
@Override
public void describeTo(Description description) {
description.appendText("has view id " + itemMatcher + " at position " + position);
}
@Override
public boolean matchesSafely(final RecyclerView recyclerView) {
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(position);
View targetView = viewHolder.itemView.findViewById(targetViewId);
return itemMatcher.matches(targetView);
}
};
}
Solution 6 - Java
For Kotlin users: I know I am very late, and I am bemused why people are having to customize RecycleView Actions. I have been meaning to achieve exactly the same desired outcome: here is my solution, hope it works and helps newcomers. Also be easy on me, as I might get blocked.
onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition<ViewHolder>(46, scrollTo()))
.check(matches(hasDescendant(withText("TextToMatch"))))
//Edited 18/12/21: After investigating Bjorn's comment, please see a better approach below:
onView(withId(R.id.recyclerView))
.perform(RecyclerViewActions.scrollToPosition<ViewHolder>(12))
.check(matches(hasDescendant(withText("TexttoMatch"))))
Solution 7 - Java
I combined a few answers above into a reusable method for testing other recycler views as the project grows. Leaving the code here in the event it helps anyone else out...
Declare a function that may be called on a RecyclerView Resource ID:
fun Int.shouldHaveTextAtPosition(text:String, position: Int) {
onView(withId(this))
.perform(scrollToPosition<RecyclerView.ViewHolder>(position))
.check(matches(atPosition(position, hasDescendant(withText(text)))))
}
Then call it on your recycler view to check the text displayed of multiple adapter items in the list at a given position by doing:
with(R.id.recycler_view_id) {
shouldHaveTextAtPosition("Some Text at position 0", 0)
shouldHaveTextAtPosition("Some Text at position 1", 1)
shouldHaveTextAtPosition("Some Text at position 2", 2)
shouldHaveTextAtPosition("Some Text at position 3", 3)
}
Solution 8 - Java
Danny Roa's solution is awesome, but it didn't work for the off-screen items I had just scrolled to. The fix is replacing this:
View targetView = recyclerView.getChildAt(this.position)
.findViewById(this.viewId);
with this:
View targetView = recyclerView.findViewHolderForAdapterPosition(this.position)
.itemView.findViewById(this.viewId);
... in the TestUtils
class.
Solution 9 - Java
This thread is pretty old but I recently extended the RecyclerViewAction support to be usable for assertions as well. This allows you to test off-screen elements without specifying a position while still using view matchers.
Check out this article > Better Android RecyclerView Testing!
Solution 10 - Java
I have written a function that can check every view of view holder in recyclerview. Here is my function :
recyclerViewId is your recyclerview id
position is the position of your recyclerview
itemMatcher is your matcher function
subViewId is the id of recyclerview sub view (view of viewHolder)
public static void checkRecyclerSubViews(int recyclerViewId, int position, Matcher<View> itemMatcher, int subViewId)
{
onView(withId(recyclerViewId)).perform(RecyclerViewActions.scrollToPosition(position))
.check(matches(atPositionOnView(position, itemMatcher, subViewId)));
}
public static Matcher<View> atPositionOnView(
final int position, final Matcher<View> itemMatcher, @NonNull final int targetViewId)
{
return new BoundedMatcher<View, RecyclerView>(RecyclerView.class)
{
@Override
public void describeTo(Description description)
{
description.appendText("has view id " + itemMatcher + " at position " + position);
}
@Override
public boolean matchesSafely(final RecyclerView recyclerView)
{
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(position);
View targetView = viewHolder.itemView.findViewById(targetViewId);
return itemMatcher.matches(targetView);
}
};
}
And this is the examaple of using :
checkRecyclerSubViews(R.id.recycler_view, 5, withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), R.id.text_view_plan_tittle);
checkRecyclerSubViews(R.id.recycler_view, 5, withText("test"), R.id.text_view_delete);
Solution 11 - Java
For testing all items (include off-screen) use code bellow
Sample:
fun checkThatRedDotIsGone(itemPosition: Int) {
onView(
withId(R.id.recycler_view)).
check(itemViewMatches(itemPosition, R.id.inside_item_view,
withEffectiveVisibility(Visibility.GONE)))
}
Class:
object RecyclerViewAssertions {
fun itemViewMatches(position: Int, viewMatcher: Matcher<View>): ViewAssertion {
return itemViewMatches(position, -1, viewMatcher)
}
/**
* Provides a RecyclerView assertion based on a view matcher. This allows you to
* validate whether a RecyclerView contains a row in memory without scrolling the list.
*
* @param viewMatcher - an Espresso ViewMatcher for a descendant of any row in the recycler.
* @return an Espresso ViewAssertion to check against a RecyclerView.
*/
fun itemViewMatches(position: Int, @IdRes resId: Int, viewMatcher: Matcher<View>): ViewAssertion {
assertNotNull(viewMatcher)
return ViewAssertion { view, noViewException ->
if (noViewException != null) {
throw noViewException
}
assertTrue("View is RecyclerView", view is RecyclerView)
val recyclerView = view as RecyclerView
val adapter = recyclerView.adapter
val itemType = adapter!!.getItemViewType(position)
val viewHolder = adapter.createViewHolder(recyclerView, itemType)
adapter.bindViewHolder(viewHolder, position)
val targetView = if (resId == -1) {
viewHolder.itemView
} else {
viewHolder.itemView.findViewById(resId)
}
if (viewMatcher.matches(targetView)) {
return@ViewAssertion // Found a matching view
}
fail("No match found")
}
}
}
Solution based of this article > Better Android RecyclerView Testing!
Solution 12 - Java
The solutions above are great, I'm contributing one more in Kotlin.
Espresso needs to scroll to the position and then check it. To scroll to the position, I'm using RecyclerViewActions.scrollToPosition
. Then use findViewHolderForAdapterPosition to perform the check. Instead of calling findViewHolderForAdapterPosition
directly, I am using a nicer convenience function childOfViewAtPositionWithMatcher
.
In the example code below, Espresso scrolls to row 43 of the RecyclerView
and the checks that the row's ViewHolder
has two TextViews and that those TextViews contain the text "hello text 1 at line 43" and "hello text 2 at line 43" respectively:
import androidx.test.espresso.contrib.RecyclerViewActions
import it.xabaras.android.espresso.recyclerviewchildactions.RecyclerViewChildActions.Companion.childOfViewAtPositionWithMatcher
// ...
val index = 42 // index is 0 based, corresponds to row 43 from user's perspective
onView(withId(R.id.myRecyclerView))
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(43))
.check(
matches(
allOf(
childOfViewAtPositionWithMatcher(
R.id.myTextView1,
index,
withText("hello text 1 at line 43")
),
childOfViewAtPositionWithMatcherMy(
R.id.myTextView2,
index,
withText("hello text 2 at line 43")
)
)
)
)
Here are my build.gradle dependencies (newer versions may exist):
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.2.0"
androidTestImplementation "it.xabaras.android.espresso:recyclerview-child-actions:1.0"
Solution 13 - Java
Here is my solution using kotlin with proper "not found" descriptions and it should apply to most usecases:
If you only want to check that something exists within the entry at the given index, you can match using withPositionInRecyclerView
with position and recyclerview id to find the row and use hasDescendant(...)
combined with whatever matcher you like:
onView(withPositionInRecyclerView(positionToLookAt, R.id.my_recycler_view))
.check(matches(hasDescendant(withText("text that should be somewhere at given position"))))
Please note that you might have to scroll on the recyclerView using actionOnItemAtPosition
first so that the item is displayed.
If you want to be more precise and make sure that a specific view within the entry at the given index matches, just add the view id as a third parameter:
onView(withPositionInRecyclerView(positionToLookAt, R.id.my_recycler_view, R.id.my_view))
.check(matches(withText("text that should be in view with id R.id.my_view at given position")))
So here is the helper method you will need to include somewhere in your test code:
fun withPositionInRecyclerView(position: Int, recyclerViewId: Int, childViewId: Int = -1): Matcher<View> =
object : TypeSafeMatcher<View>() {
private var resources: Resources? = null
private var recyclerView: RecyclerView? = null
override fun describeTo(description: Description?) {
description?.appendText("with position $position in recyclerView")
if (resources != null) {
try {
description?.appendText(" ${resources?.getResourceName(recyclerViewId)}")
} catch (exception: Resources.NotFoundException) {
description?.appendText(" (with id $recyclerViewId not found)")
}
}
if (childViewId != -1) {
description?.appendText(" with child view ")
try {
description?.appendText(" ${resources?.getResourceName(childViewId)}")
} catch (exception: Resources.NotFoundException) {
description?.appendText(" (with id $childViewId not found)")
}
}
}
override fun matchesSafely(view: View?): Boolean {
if (view == null) return false
if (resources == null) {
resources = view.resources;
}
if (recyclerView == null) {
recyclerView = view.rootView.findViewById(recyclerViewId)
}
return if (childViewId != -1) {
view == recyclerView?.findViewHolderForAdapterPosition(position)?.itemView?.findViewById(childViewId)
} else {
view == recyclerView?.findViewHolderForAdapterPosition(position)?.itemView;
}
}
}
Solution 14 - Java
Modified matcher solution for when you check if your item(Text in TextView) is first in RecyclerView.
// True if first item in the RV and input text
fun <T> customMatcherForFirstItem(name: String, matcher: Matcher<T>): Matcher<T> {
return object: BaseMatcher<T> () {
var isFirst = true
override fun matches(item: Any): Boolean {
if (isFirst && (item as TextView).text == name) {
isFirst = false
return true
}
return false
}
override fun describeTo(description: Description?) {}
}
}
Usage: (returns true if child textview in the item has the same text as we expect/search)
onView(withText("TEXT_VIEW_TEXT")).check(matches(
customMatcherForFirstItem("TEXT_VIEW_TEXT", withParent(withId(R.id.recycler_view)))))
Based on -> https://proandroiddev.com/working-with-recycler-views-in-espresso-tests-6da21495182c
Solution 15 - Java
Here is my take in Kotlin, based on some already published solutions. Objectives:
- Helpful error message in case
RecyclerView
or target view can't be found (e.g. noNullPointerException
if position is out-of-bounds) - Helpful error message in case target view does not match expectations (all solutions starting with
onView(withId(R.id.recycler_view))
just print details aboutRecyclerView
itself in this case) - Checks item at exact position, not just any visible child that matches
- Can be used to match either item's root view or its specific descendant
- Can be used to perform actions on matched view
- Tests real view displayed on the screen, not artificially created one
- Avoids unnecessary work
- Minimal boilerplate, concise, but readable
Unfortunately, Matcher
can't scroll RecyclerView
automatically (matchesSafely
runs on UI thread and can't wait until it's idle), so it should be done separately. Usage:
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(42))
onRecyclerViewItem(R.id.recycler_view, 42)
.check(matches(isAssignableFrom(ConstraintLayout::class.java)))
onRecyclerViewItem(R.id.recycler_view, 42, R.id.text)
.check(matches(withText("Item 42")))
onRecyclerViewItem(R.id.recycler_view, 42, R.id.button).perform(click())
Code:
fun onRecyclerViewItem(
recyclerViewId: Int,
position: Int,
targetViewId: Int? = null
): ViewInteraction = onView(object : TypeSafeMatcher<View>() {
private lateinit var resources: Resources
private var recyclerView: RecyclerView? = null
private var holder: RecyclerView.ViewHolder? = null
private var targetView: View? = null
override fun describeTo(description: Description) {
fun Int.name(): String = try {
"R.id.${resources.getResourceEntryName(this)}"
} catch (e: Resources.NotFoundException) {
"unknown id $this"
}
val text = when {
recyclerView == null -> "RecyclerView (${recyclerViewId.name()})"
holder == null || targetViewId == null -> "in RecyclerView (${recyclerViewId.name()}) at position $position"
else -> "in RecyclerView (${recyclerViewId.name()}) at position $position and with ${targetViewId.name()}"
}
description.appendText(text)
}
override fun matchesSafely(view: View): Boolean {
// matchesSafely will be called for each view in the hierarchy (until found),
// it makes no sense to perform lookup over and over again
if (!::resources.isInitialized) {
resources = view.resources
recyclerView = view.rootView.findViewById(recyclerViewId) ?: return false
holder = recyclerView?.findViewHolderForAdapterPosition(position) ?: return false
targetView = holder?.itemView?.let {
if (targetViewId != null) it.findViewById(targetViewId) else it
}
}
return view === targetView
}
})
I've merged onView
and matcher class directly into it, because I only use it this way, and onRecyclerViewItem(...)
looks more readable then onView(withPositionInRecyclerView(...))
. Matcher function and/or class can be extracted if you prefer so. The same code "in the wild", with imports, KDoc and my rewrite of scrollToPosition
can be found here.