简体   繁体   中英

How to assert inside a RecyclerView in Espresso?

I am using espresso-contrib to perform actions on a RecyclerView<\/code> , and it works as it should, ex:

//click on first item
onView(withId(R.id.recycler_view))
    .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));

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

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

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

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

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,
        @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);
        }
    };
}

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 .

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

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 !

I have written a function that can check every view of view holder in recyclerview. Here is my function :

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

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!

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"

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<RecyclerView.ViewHolder>(46, scrollTo()))
       .check(matches(hasDescendant(withText("TextToMatch"))))

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM