Working with Recycler Views in Espresso Tests

In this short post, I will introduce you to a few utility methods that might be useful while working with RecyclerView in Espresso Tests.

Introduction

In this post, I am assuming that you are already using a RecyclerView in your app. Take a look at this post if you need help in using a RecyclerView with an Android Paged list:

Using the Paged List with Boundary Callback in Android

If you are just getting started with UI tests I would suggest that you take a look at my previous posts on using Espresso tests in Android.

How To Improve Coverage for your Android App Using Mockito and Espresso — Part 2

Custom Recycler View actions

If you have a RecyclerView.ViewHolder you can use it to access the child items and perform any action on them. The basic idea is to define a custom view action and then use it. The custom ViewAction takes the following form:

fun customAction(id: Int): ViewAction {
    return object : ViewAction {
        override fun getConstraints(): Matcher<View>? {
            return null
        }

        override fun getDescription(): String {
            return "Action Description"
        }

        override fun perform(uiController: UiController, view: View) {
            val v = view.findViewById<View>(id) as View 
            // your action
        }
    }
}

Using the above template lets take a few examples.

Click a Recycler View Item

This is one of the most common scenarios while working with a RecyclerView where you are required to click an item. For eg. in the screen shown below, I need to click on the nth item.

Define a custom ViewAction like shown below:

fun clickItemWithId(id: Int): ViewAction {
    return object : ViewAction {
        override fun getConstraints(): Matcher<View>? {
            return null
        }

        override fun getDescription(): String {
            return "Click on a child view with specified id."
        }

        override fun perform(uiController: UiController, view: View) {
            val v = view.findViewById<View>(id) as View
            v.performClick()
        }
    }
}

And use it in your RecyclerView in the following way:

onView(withId(R.id.recycler_view))
  .perform(RecyclerViewActions
  .actionOnItemAtPosition<MyAdapter.ViewHolder>(1,clickItemWithId(R.id.button)))

The following two scenarios might not be a very common use case but I came across this scenario while working with the Wikimedia Commons Android app where we have a RecyclerView with a spinner and text view. The user can click on the (+) icon to add a new item to the list.

Type text in Recycler View’s child item

Define a custom ViewAction like shown below:

fun typeTextInChildViewWithId(id: Int, textToBeTyped: String): ViewAction {
    return object : ViewAction {
        override fun getConstraints(): Matcher<View>? {
            return null
        }

        override fun getDescription(): String {
            return "Click on a child view with specified id."
        }

        override fun perform(uiController: UiController, view: View) {
            val v = view.findViewById<View>(id) as EditText
            v.setText(textToBeTyped)
        }
    }
}

And use it in your RecyclerView in the following way:

onView(withId(R.id.my_recycler_view))
  .perform(RecyclerViewActions
    .actionOnItemAtPosition<MyAdapter.ViewHolder>(1, 
      typeTextInChildViewWithId(R.id.my_edit_text, "your text")))

Select a child Spinner Item from Recycler View

Define a custom ViewAction like shown below:

fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction {
    return object : ViewAction {
        override fun getConstraints(): Matcher<View>? {
            return null
        }

        override fun getDescription(): String {
            return "Click on a child view with specified id."
        }

        override fun perform(uiController: UiController, view: View) {
            val v = view.findViewById<View>(id) as AppCompatSpinner
            v.setSelection(position)
        }
    }
}

And use it in your RecyclerView in the following way:

onView(withId(R.id.my_recycler_view))
  .perform(RecyclerViewActions
    .actionOnItemAtPosition<MyAdapter.ViewHolder>(1, 
      selectSpinnerItemInChildViewWithId(R.id.spinner_item, 2)))

Click Recycler View Item without Referencing the View Holder

If you have a simple Recycler view without a custom view holder you can use try matching the first or nth-child view without referencing the view holder.

You can use the following function to get the first matching view if you have a scenario where multiple views on the screen match your Matcher condition. This could happen when you are working with a list/recycler view.

fun <T> first(matcher: Matcher<T>): Matcher<T>? {
    return object : BaseMatcher<T>() {
        var isFirst = true
        override fun matches(item: Any): Boolean {
            if (isFirst && matcher.matches(item)) {
                isFirst = false
                return true
            }
            return false
        }

        override fun describeTo(description: Description) {
            description.appendText("should return first matching item")
        }
    }
}

For eg., if you want to click on the first item of your recycler view(ie. R.id.rv_categories) then you could use the above method in the following way:

onView(allOf(isDisplayed(), 
  first(withParent(withId(R.id.rv_categories)))))
   .perform(click())

Conclusion

These were just a few examples of working with RecyclerView while writing Espresso tests. Feel free to browse the Commons Android app repository on Github to check out other Espresso tests.

commons-app/apps-android-commons

You can buy me a coffee if this post really helped you learn something or fix a nagging issue!


Written on March 10, 2020 by Vivek Maskara.

Originally published on Medium

Vivek Maskara
Vivek Maskara
GRA at The Luminosity Lab, ASU | Ex Senior Software Engineer, Zeta | Volunteer, Wikimedia Foundation

Former SDE Intern @ Amazon | Graduated from MS CS @ ASU | Ex-Zeta

Related