Utilisation d'Espresso pour cliquer sur la vue à l'intérieur de RecyclerView item

100

Comment puis-je utiliser Espresso pour cliquer sur une vue spécifique à l'intérieur d'un article RecyclerView ? Je sais que je peux cliquer sur l'élément à la position 0 en utilisant:

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

Mais je dois cliquer sur une vue spécifique à l'intérieur de cet élément et non sur l'élément lui-même.

Merci d'avance.

-- Éditer --

Pour être plus précis: j'ai un RecyclerView ( R.id.recycler_view) dont les éléments sont CardView ( R.id.card_view). À l'intérieur de chaque CardView, j'ai quatre boutons (entre autres) et je veux cliquer sur un bouton spécifique ( R.id.bt_deliver).

J'aimerais utiliser les nouvelles fonctionnalités d'Espresso 2.0, mais je ne suis pas sûr que ce soit possible.

Si ce n'est pas possible, je veux utiliser quelque chose comme ça (en utilisant le code Thomas Keller):

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

mais je ne sais pas quoi mettre sur les points d'interrogation.

Filipe Ramos
la source
Vérifiez ceci
Skizo-ozᴉʞS
1
Salut, réservoirs pour la réponse rapide! :-) J'ai vu cette question, mais je ne trouve aucune aide sur la façon d'utiliser RecyclerViewActions pour faire ce que je veux. Dois-je utiliser l' ancienne réponse ? Merci.
Filipe Ramos
Avez-vous trouvé une solution à votre problème?
HowieH
1
@HowieH: Non, j'ai abandonné, et j'utilise maintenant Robotium ...
Filipe Ramos

Réponses:

136

Vous pouvez le faire avec une action de vue personnalisée.

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

}

Ensuite, vous pouvez cliquer dessus avec

onView(withId(R.id.rv_conference_list)).perform(
            RecyclerViewActions.actionOnItemAtPosition(0, MyViewAction.clickChildViewWithId(R.id. bt_deliver)));
lame
la source
2
Je supprimerais la vérification nulle, de sorte que si la vue enfant n'est pas trouvée, une exception est levée plutôt que de ne rien faire en silence.
Alvaro Gutierrez Perez
Merci d'avoir répondu. Mais frappé dans un problème. Dans la fonction onPerform (), si j'ai utilisé la fonction onView (withId ()), la fonction est frappée. Quelle est la raison de ce comportement?
thedarkpassenger
3
fonctionne avec espresso: espresso-contrib: 2.2.2 mais pas avec 2.2.1 des raisons pour lesquelles? J'ai quelques dépendances car je ne peux pas utiliser 2.2.2.
RosAng le
3
J'obtenais une exception NullPointerException avec ceci à l'origine, donc j'ai dû implémenter getConstraints () pour éviter cela. Ce qui suit a fonctionné pour moi: public Matcher <View> getConstraints () {return isAssignableFrom (View.class); }
dfinn
La solution ci-dessus ne fonctionne pas pour moi. J'obtiens l'erreur suivante: android.support.test.espresso.PerformException: Erreur lors de l'exécution de 'android.support.test.espresso.contrib.RecyclerViewActions$ActionOnItemAtPositionViewAction@fad9df' on view 'avec id: adamhurwitz.github.io.doordashlite : id / recyclerView '.
Adam Hurwitz le
67

Maintenant, avec android.support.test.espresso.contrib, c'est devenu plus facile:

1) Ajouter une dépendance de test

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

* exclure 3 modules, car vous l'avez probablement déjà

2) Ensuite, faites quelque chose comme

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

Ou

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

Ou

onView(withId(R.id.recycler_linear))
            .check(matches(hasDescendant(withText("whatever"))));
Andreï
la source
1
Merci, ne pas exclure ces modules se traduit par une erreur java.lang.IncompatibleClassChangeError pour moi.
hboy
8
et que faire pour cliquer disons dans le 5ème élément de la liste une vue spécifique? dans les exemples fournis, je ne vois que comment cliquer sur le cinquième élément ou cliquer sur un élément avec une vue descendante, mais pas les deux ensemble, ou ai-je manqué quelque chose?
donfuxx
1
je devais faireexclude group: 'com.android.support' exclude module: 'recyclerview-v7'
Jemshit Iskenderov
1
Il est inutile de cliquer sur la case à cocher dans RecyclerView.
Farid
2
Dans kotlin actionOnItemAtPositionnécessite un paramètre de type. Je ne sais pas quoi y mettre.
ZeroDivide
7

Voici comment j'ai résolu le problème dans kotlin:

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

puis

onView(withId(R.id.recyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(position, clickOnViewChild(R.id.viewToClickInTheRow)))
user2768856
la source
J'ai rencontré E / TestRunner: java.lang.NullPointerException: tentative d'appeler la méthode virtuelle «void android.view.View.getLocationOnScreen (int [])» sur une référence d'objet nulle; une idée pourquoi?
sayvortana
6

Essayez la prochaine approche:

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

            }
        };
    }
}
Sergey
la source
3

Vous pouvez cliquer sur le 3ème élément de recyclerViewLike this:

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

N'oubliez pas de fournir le ViewHoldertype pour que l'inférence n'échoue pas.

Erluxman
la source
1
Je <RecyclerView.ViewHolder>
manquais
0

Toutes les réponses ci-dessus ne fonctionnaient pas pour moi, j'ai donc créé une nouvelle méthode qui recherche toutes les vues à l'intérieur d'une cellule pour renvoyer la vue avec l'ID demandé. Il nécessite deux méthodes (pouvant être combinées en une seule):

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

Enfin, vous pouvez l'utiliser sur Recycler View en procédant comme suit:

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

Vous pouvez utiliser un rappel pour renvoyer la vue si vous voulez faire autre chose avec elle, ou simplement en créer 3-4 versions différentes pour effectuer d'autres tâches.

paul_f
la source
0

J'ai continué à essayer différentes méthodes pour trouver pourquoi la réponse de @ blade ne fonctionnait pas pour moi, pour me rendre compte seulement que j'avais un OnTouchListener(), j'ai modifié ViewAction en conséquence:

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!")
                    }
                }
            }
        }
    }
Rimov
la source
0

Vous pouvez même généraliser cette approche pour soutenir plus d'actions non seulement click. Voici ma solution pour cela:

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

Et puis pour EditTextvous pouvez faire quelque chose comme ça:

onView(withId(R.id.yourRecyclerView))
        .perform(
            actionOnItemAtPosition<YourViewHolder>(
                0,
                recyclerChildAction<EditText>(R.id.editTextId) { setText("1000") }
            )
        )
Jokubas Trinkunas
la source
-5

Commencez par donner à vos boutons des descriptions de contenu uniques, c'est-à-dire «ligne de bouton de livraison 5».

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

Puis faites défiler jusqu'à la ligne:

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

Sélectionnez ensuite la vue en fonction de contentDescription.

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

La description du contenu est un excellent moyen d'utiliser onView d'Espresso et de rendre votre application plus accessible.

RPGObjects
la source
5
La description du contenu ne doit pas être utilisée de cette manière - cela ne rend pas votre application plus accessible pour que les vues diffusent des informations techniques ou de débogage pour les utilisateurs ayant tous les besoins - le CD pour cela devrait probablement être quelque chose comme "Commande <résumé de l'élément> bouton". withText("Order")fonctionnerait aussi et ne vous obligerait pas à savoir au moment du test ce que vous commandez.
ataulm