Использование Espresso для просмотра внутри элемента RecyclerView

100

Как я могу использовать Espresso, чтобы щелкнуть определенное представление внутри элемента RecyclerView ? Я знаю, что могу щелкнуть элемент в позиции 0, используя:

onView(withId(R.id.recyclerView)) .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));

Но мне нужно щелкнуть конкретный вид внутри этого элемента, а не сам элемент.

Заранее спасибо.

-- редактировать --

Чтобы быть более точным: у меня есть RecyclerView ( R.id.recycler_view), элементы которого - CardView ( R.id.card_view). Внутри каждого CardView у меня есть четыре кнопки (среди прочего), и я хочу нажать на определенную кнопку ( R.id.bt_deliver).

Я хотел бы использовать новые функции Espresso 2.0, но не уверен, что это возможно.

Если это невозможно, я хочу использовать что-то вроде этого (используя код Томаса Келлера):

onRecyclerItemView(R.id.card_view, ???, withId(R.id.bt_deliver)).perform(click());

но я не знаю, что ставить вопросительные знаки.

Филипе Рамос
источник
Проверьте это
Skizo-ozᴉʞS
1
Привет, танки за быстрый ответ! :-) Я видел этот вопрос, но не могу найти никакой справки о том, как использовать RecyclerViewActions, чтобы делать то, что я хочу. Стоит ли использовать старый ответ ? Спасибо.
Филипе Рамос,
Вы нашли решение своей проблемы?
HowieH
1
@HowieH: Нет. Я сдался, и теперь я использую Robotium ...
Филипе Рамос,

Ответы:

136

Вы можете сделать это с помощью действия настройки просмотра.

public class MyViewAction {

    public static ViewAction clickChildViewWithId(final int id) {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                return null;
            }

            @Override
            public String getDescription() {
                return "Click on a child view with specified id.";
            }

            @Override
            public void perform(UiController uiController, View view) {
                View v = view.findViewById(id);
                v.performClick();
            }
        };
    }

}

Затем вы можете щелкнуть по нему с помощью

onView(withId(R.id.rv_conference_list)).perform(
            RecyclerViewActions.actionOnItemAtPosition(0, MyViewAction.clickChildViewWithId(R.id. bt_deliver)));
лезвие
источник
2
Я бы удалил нулевую проверку, чтобы, если дочернее представление не было найдено, выдается исключение, а не ничего не делать молча.
Альваро Гутьеррес Перес,
Спасибо за ответ. Но поразила одна проблема. В функции onPerform (), если я использовал функцию onView (withId ()), функция получает удар. В чем причина такого поведения?
темный пассажир
3
работает с espresso: espresso-contrib: 2.2.2, но не с 2.2.1 по каким причинам? У меня есть зависимости, из-за которых я не могу использовать 2.2.2.
РосАнг
3
Изначально я получал исключение NullPointerException с этим, поэтому мне пришлось реализовать getConstraints (), чтобы этого избежать. У меня сработало следующее: public Matcher <View> getConstraints () {return isAssignableFrom (View.class); }
dfinn
Приведенное выше решение не работает для меня. Я получаю следующую ошибку: android.support.test.espresso.PerformException: Ошибка при выполнении android.support.test.espresso.contrib.RecyclerViewActions$ActionOnItemAtPositionViewAction@fad9df «на просмотре» с идентификатором: adamhurwitz.github.io.doordashlite : id / recyclerView '.
Адам Гурвиц
67

Теперь с android.support.test.espresso.contrib стало проще:

1) Добавить тестовую зависимость

androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.0') {
    exclude group: 'com.android.support', module: 'appcompat'
    exclude group: 'com.android.support', module: 'support-v4'
    exclude module: 'recyclerview-v7'
}

* исключить 3 модуля, потому что скорее всего он у вас уже есть

2) Затем сделайте что-нибудь вроде

onView(withId(R.id.recycler_grid))
            .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));

Или

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

Или

onView(withId(R.id.recycler_linear))
            .check(matches(hasDescendant(withText("whatever"))));
Андрей
источник
1
Спасибо, что не исключение этих модулей приводит к ошибке java.lang.IncompatibleClassChangeError для меня.
hboy
8
а что делать, чтобы щелкнуть допустим в 5-м пункте списка конкретный вид? в приведенных примерах я вижу только, как щелкнуть 5-й элемент или щелкнуть и элемент с представлением потомков, но не оба вместе, или я что-то пропустил?
donfuxx
1
я должен был сделатьexclude group: 'com.android.support' exclude module: 'recyclerview-v7'
Джемшит Искендеров
1
Это бесполезно, если вы хотите установить флажок внутри RecyclerView.
Фарид
2
В котлине actionOnItemAtPositionтребуется параметр типа. Не уверен, что туда поставить.
ZeroDivide
7

Вот как я решил проблему в котлине:

fun clickOnViewChild(viewId: Int) = object : ViewAction {
    override fun getConstraints() = null

    override fun getDescription() = "Click on a child view with specified id."

    override fun perform(uiController: UiController, view: View) = click().perform(uiController, view.findViewById<View>(viewId))
}

а потом

onView(withId(R.id.recyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(position, clickOnViewChild(R.id.viewToClickInTheRow)))
Влад Сумцов
источник
Я столкнулся с E / TestRunner: java.lang.NullPointerException: попытка вызвать виртуальный метод void android.view.View.getLocationOnScreen (int []) для ссылки на нулевой объект; есть идеи почему?
sayvortana
6

Попробуйте следующий подход:

    onView(withRecyclerView(R.id.recyclerView)
                    .atPositionOnView(position, R.id.bt_deliver))
                    .perform(click());

    public static RecyclerViewMatcher withRecyclerView(final int recyclerViewId) {
            return new RecyclerViewMatcher(recyclerViewId);
    }

public class RecyclerViewMatcher {
    final int mRecyclerViewId;

    public RecyclerViewMatcher(int recyclerViewId) {
        this.mRecyclerViewId = recyclerViewId;
    }

    public Matcher<View> atPosition(final int position) {
        return atPositionOnView(position, -1);
    }

    public Matcher<View> atPositionOnView(final int position, final int targetViewId) {

        return new TypeSafeMatcher<View>() {
            Resources resources = null;
            View childView;

            public void describeTo(Description description) {
                int id = targetViewId == -1 ? mRecyclerViewId : targetViewId;
                String idDescription = Integer.toString(id);
                if (this.resources != null) {
                    try {
                        idDescription = this.resources.getResourceName(id);
                    } catch (Resources.NotFoundException var4) {
                        idDescription = String.format("%s (resource name not found)", id);
                    }
                }

                description.appendText("with id: " + idDescription);
            }

            public boolean matchesSafely(View view) {

                this.resources = view.getResources();

                if (childView == null) {
                    RecyclerView recyclerView =
                            (RecyclerView) view.getRootView().findViewById(mRecyclerViewId);
                    if (recyclerView != null) {

                        childView = recyclerView.findViewHolderForAdapterPosition(position).itemView;
                    }
                    else {
                        return false;
                    }
                }

                if (targetViewId == -1) {
                    return view == childView;
                } else {
                    View targetView = childView.findViewById(targetViewId);
                    return view == targetView;
                }

            }
        };
    }
}
Сергей
источник
3

Вы можете щелкнуть по 3-му пункту в виде recyclerViewэтого:

onView(withId(R.id.recyclerView)).perform(
                RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(2,click()))

Не забудьте указать ViewHolderтип, чтобы логический вывод не завершился ошибкой.

Erluxman
источник
1
Я скучал <RecyclerView.ViewHolder> спасибо, сэр;)
Skizo-ozᴉʞS
0

Все приведенные выше ответы не сработали для меня, поэтому я создал новый метод, который просматривает все представления внутри ячейки, чтобы вернуть представление с запрошенным идентификатором. Для этого требуются два метода (их можно объединить в один):

fun performClickOnViewInCell(viewID: Int) = object : ViewAction {
    override fun getConstraints(): org.hamcrest.Matcher<View> = click().constraints
    override fun getDescription() = "Click on a child view with specified id."
    override fun perform(uiController: UiController, view: View) {
        val allChildViews = getAllChildrenBFS(view)
        for (child in allChildViews) {
            if (child.id == viewID) {
                child.callOnClick()
            }
        }
    }
}


private fun  getAllChildrenBFS(v: View): List<View> {
    val visited = ArrayList<View>();
    val unvisited = ArrayList<View>();
    unvisited.add(v);

    while (!unvisited.isEmpty()) {
        val child = unvisited.removeAt(0);
        visited.add(child);
        if (!(child is ViewGroup)) continue;
        val group = child
        val childCount = group.getChildCount();
        for (i in 0 until childCount) { unvisited.add(group.getChildAt(i)) }
    }

    return visited;
}

Затем, наконец, вы можете использовать это в Recycler View, выполнив следующие действия:

onView(withId(R.id.recyclerView)).perform(actionOnItemAtPosition<RecyclerView.ViewHolder>(0, getViewFromCell(R.id.cellInnerView) {
            val requestedView = it
}))

Вы можете использовать обратный вызов, чтобы вернуть представление, если вы хотите сделать с ним что-то еще, или просто создать 3-4 разные версии этого для выполнения любых других задач.

paul_f
источник
0

Я продолжал пробовать различные методы, чтобы выяснить, почему ответ @ blade не работает для меня, только чтобы понять, что у меня есть OnTouchListener(), я соответствующим образом изменил ViewAction:

fun clickTopicToWeb(id: Int): ViewAction {

        return object : ViewAction {
            override fun getDescription(): String {...}

            override fun getConstraints(): Matcher<View> {...}

            override fun perform(uiController: UiController?, view: View?) {

                view?.findViewById<View>(id)?.apply {

                    //Generalized for OnClickListeners as well
                    if(isEnabled && isClickable && !performClick()) {
                        //Define click event
                        val event: MotionEvent = MotionEvent.obtain(
                          SystemClock.uptimeMillis(),
                          SystemClock.uptimeMillis(),
                          MotionEvent.ACTION_UP,
                          view.x,
                          view.y,
                          0)

                        if(!dispatchTouchEvent(event))
                            throw Exception("Not clicking!")
                    }
                }
            }
        }
    }
Римов
источник
0

Вы даже можете обобщить этот подход не только для поддержки большего количества действий click. Вот мое решение для этого:

fun <T : View> recyclerChildAction(@IdRes id: Int, block: T.() -> Unit): ViewAction {
  return object : ViewAction {
    override fun getConstraints(): Matcher<View> {
      return any(View::class.java)
    }

    override fun getDescription(): String {
      return "Performing action on RecyclerView child item"
    }

    override fun perform(
        uiController: UiController,
        view: View
    ) {
      view.findViewById<T>(id).block()
    }
  }
 
}

И тогда EditTextвы можете сделать что-то вроде этого:

onView(withId(R.id.yourRecyclerView))
        .perform(
            actionOnItemAtPosition<YourViewHolder>(
                0,
                recyclerChildAction<EditText>(R.id.editTextId) { setText("1000") }
            )
        )
Йокубас Тринкунас
источник
-5

Сначала дайте вашим кнопкам уникальное описание содержимого, например, «строка кнопки доставки 5».

<button android:contentDescription=".." />

Затем прокрутите до строки:

onView(withId(R.id.recycler_view)).perform(RecyclerViewActions.scrollToPosition(5));

Затем выберите представление на основе описания содержимого.

onView(withContentDescription("delivery button row 5")).perform(click());

Описание содержимого - отличный способ использовать onView от Espresso и сделать ваше приложение более доступным.

RPGOобъекты
источник
5
Описание содержимого не следует использовать таким образом - это не делает ваше приложение более доступным для представлений, чтобы изливать техническую или отладочную информацию для пользователей с любыми потребностями - компакт-диск для этого, вероятно, должен быть чем-то вроде «Заказать <краткое описание элемента> кнопка ". withText("Order")тоже будет работать, и вам не потребуется знать во время тестирования, что вы заказываете.
ataulm