How to assert inside a RecyclerView in Espresso?

JavaAndroidAndroid Espresso

Java 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.

  1. Scroll RecyclerView to the view matched by itemViewMatcher
  2. 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. no NullPointerException 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 about RecyclerView 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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionjuhlilaView Question on Stackoverflow
Solution 1 - JavariwnodennykView Answer on Stackoverflow
Solution 2 - JavaHowieHView Answer on Stackoverflow
Solution 3 - JavaYou QiView Answer on Stackoverflow
Solution 4 - JavaHiten BahriView Answer on Stackoverflow
Solution 5 - JavaVinceView Answer on Stackoverflow
Solution 6 - JavaawaqarView Answer on Stackoverflow
Solution 7 - JavaThomasView Answer on Stackoverflow
Solution 8 - JavaI_AM_PANDAView Answer on Stackoverflow
Solution 9 - JavaMax BusterView Answer on Stackoverflow
Solution 10 - JavaJavad ShirkhaniView Answer on Stackoverflow
Solution 11 - JavaAlexandr LarinView Answer on Stackoverflow
Solution 12 - JavaMichael OsofskyView Answer on Stackoverflow
Solution 13 - JavaBjörn KechelView Answer on Stackoverflow
Solution 14 - JavatompadreView Answer on Stackoverflow
Solution 15 - Javagmk57View Answer on Stackoverflow