7

I'm wondering how I would be able to find a specific item in a recycler view where the order of items is randomized each run.

Let's assume I have 4 items in the recycler view, each represented by the same type of view holder with a text view in it. A unique title is applied to each view holder/item. For this example let's say the titles are, for simplicity's sake, "A", "B", "C", and "D".

How would I find the position (and then click) item "A" if the order is randomized? I know if the order does not change I could the scrollToPosition RecyclerViewInteraction action, but in this case the order can and will change.

Any thoughts?

Dharman
  • 30,962
  • 25
  • 85
  • 135
Zach
  • 3,909
  • 6
  • 25
  • 50
  • 1
    don't randomize the items, or rather, randomize then in a predictable fashion. You can use the same seed each time in your random generator to make sure the items are where you look for them. – njzk2 Jun 09 '16 at 21:40
  • Assume that the order of the items cannot be controlled by the developer. In an ideal world, yes, a set order (or a predictable order as you suggest) is ideal. For this scenario it's not possible. – Zach Jun 09 '16 at 21:49

4 Answers4

16

I was able to get this to work doing the following:

Matcher<RecyclerView.ViewHolder> matcher = CustomMatcher.withTitle("A");
onView((withId(R.id.recycler_view))).perform(scrollToHolder(matcher), actionOnHolderItem(matcher, click()));

Where CustomMatcher.withTitle is:

    public static Matcher<RecyclerView.ViewHolder> withTitle(final String title)
{
    return new BoundedMatcher<RecyclerView.ViewHolder, CustomListAdapter.ItemViewHolder>(CustomListAdapter.ItemViewHolder.class)
    {
        @Override
        protected boolean matchesSafely(CustomListAdapter.ItemViewHolder item)
        {
            return item.mTitleView.getText().toString().equalsIgnoreCase(title);
        }

        @Override
        public void describeTo(Description description)
        {
            description.appendText("view holder with title: " + title);
        }
    };
}
Zach
  • 3,909
  • 6
  • 25
  • 50
  • 1
    This only works because `CustomListAdapter.ItemViewHolder` exposes `mTitleView` as a public field. If you only do this for testing then you are leaking test code into the app. – tir38 Nov 26 '18 at 20:22
10

You don't need a custom matcher for this, just use this

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

and add espresso-contrib dependency

    androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:<espressso-version>'

Or if you're using AndroidX

    androidTestImplementation 'androidx.test.espresso:espresso-contrib:<espressso-version>'
Anass NAZIH
  • 111
  • 1
  • 7
1

I know it's an old question, while @Zach's answer is a good start, and help me in my answer, we have a recycler view with loads of different types of ViewHolders with different Ids for the texts the answer did not fully suit our use case.

What I ended up doing was using a slightly different matcher, which does not reference any specific Ids or variables:

fun withText(title: String) = object : BoundedMatcher<View, View>(View::class.java) {
    override fun describeTo(description: Description?) {
        description?.appendText("Searching for title with: $title")
    }

    override fun matchesSafely(item: View?): Boolean {
        val views = ArrayList<View>()
        item?.findViewsWithText(views, title, View.FIND_VIEWS_WITH_TEXT)

        return when {
            views.size == 1 -> true
            else -> false
        }
    }
}

And usage:

RecyclerViewActions.scrollTo<ViewHolder>(withText("Some unique text I'm looking for")
-2

Use Espresso.onData using a custom view matcher. Google example

I have used something that could be adapted like this for your case:

onData(new BaseMatcher<String>() {
        @Override
        public void describeTo(Description description) {
            // what?
            description.appendText("Matches A");
        }

        @Override
        public boolean matches(Object item) {
            return item instanceof String && ((String) item).equals("A");
        }
    }).perform(click());

You can replace String by whatever your model type is if it is more complex than that, and also use the built-in matchers if it is simple (this case can be simplified by onData(is("A")).perform(click())).

njzk2
  • 38,969
  • 7
  • 69
  • 107
  • Sounds like a good approach, but I'm getting an error trying to use this: `android.support.test.espresso.NoMatchingViewException: No views in hierarchy found matching: is assignable from class: class android.widget.AdapterView` – Zach Jun 09 '16 at 22:18
  • 1
    Ah, I don't think onData can be used for recycler views, is that correct? – Zach Jun 09 '16 at 22:22
  • 1
    right. I would have thought it could have worked. there this, though: https://stackoverflow.com/questions/27396583/how-to-click-on-an-item-inside-a-recyclerview-in-espresso – njzk2 Jun 10 '16 at 02:31
  • Thanks, this led me to figuring out a way to handle it... will post in an answer now! – Zach Jun 10 '16 at 17:06