Utilizzo di Espresso per fare clic su Visualizza all'interno dell'elemento RecyclerView


100

Come posso utilizzare Espresso per fare clic su una vista specifica all'interno di un elemento RecyclerView ? So di poter fare clic sull'elemento nella posizione 0 utilizzando:

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

Ma devo fare clic su una vista specifica all'interno di quell'elemento e non sull'elemento stesso.

Grazie in anticipo.

-- modificare --

Per essere più precisi: ho un RecyclerView ( R.id.recycler_view) i cui elementi sono CardView ( R.id.card_view). All'interno di ogni CardView ho quattro pulsanti (tra le altre cose) e voglio fare clic su un pulsante specifico ( R.id.bt_deliver).

Vorrei utilizzare le nuove funzionalità di Espresso 2.0, ma non sono sicuro che sia possibile.

Se non è possibile, voglio usare qualcosa di simile (usando il codice Thomas Keller):

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

ma non so cosa mettere sui punti interrogativi.


Controlla questo
Skizo-ozᴉʞS

1
Ciao, carri armati per la risposta rapida! :-) Ho visto questa domanda, ma non riesco a trovare alcun aiuto su come utilizzare RecyclerViewActions per fare quello che voglio. Devo usare la vecchia risposta ? Grazie.
Filipe Ramos

Hai trovato una soluzione al tuo problema?
HowieH

1
@ HowieH: No. Mi sono arreso e ora sto usando Robotium ...
Filipe Ramos

Risposte:


136

Puoi farlo con l'azione di personalizzazione della visualizzazione.

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

}

Quindi puoi fare clic con

onView(withId(R.id.rv_conference_list)).perform(
            RecyclerViewActions.actionOnItemAtPosition(0, MyViewAction.clickChildViewWithId(R.id. bt_deliver)));

2
Rimuoverei il controllo nullo, in modo che se la visualizzazione figlio non viene trovata viene generata un'eccezione anziché fare nulla in silenzio.
Alvaro Gutierrez Perez

Grazie per la risposta. Ma ha colpito in un problema. Nella funzione onPerform (), se ho usato la funzione onView (withId ()), la funzione viene colpita. Qual è la ragione di questo comportamento?
thedarkpassenger

3
funziona con espresso: espresso-contrib: 2.2.2 ma non con 2.2.1 qualche motivo? Ho alcune dipendenze perché non posso usare 2.2.2.
RosAng

3
Inizialmente stavo ottenendo una NullPointerException con questo, quindi ho dovuto implementare getConstraints () per evitarlo. Quanto segue ha funzionato per me: public Matcher <View> getConstraints () {return isAssignableFrom (View.class); }
dfinn

La soluzione sopra non funziona per me. Ricevo il seguente errore: android.support.test.espresso.PerformException: errore durante l'esecuzione di "android.support.test.espresso.contrib.RecyclerViewActions$ActionOnItemAtPositionViewAction@fad9df" in vista "con ID: adamhurwitz.github.io.doordashlite : id / recyclerView '.
Adam Hurwitz

67

Ora con android.support.test.espresso.contrib è diventato più semplice:

1) Aggiungi dipendenza di prova

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

* escludi 3 moduli, perché molto probabilmente lo hai già

2) Quindi fai qualcosa di simile

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

O

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

O

onView(withId(R.id.recycler_linear))
            .check(matches(hasDescendant(withText("whatever"))));

1
Grazie, non escludere quei moduli risulta in un java.lang.IncompatibleClassChangeError per me.
hboy

8
e cosa fare per fare clic diciamo nel 5 ° elemento della lista una vista specifica? negli esempi forniti vedo solo come fare clic sul 5 ° elemento o fare clic su un elemento con vista discendente, ma non entrambi insieme, o mi sono perso qualcosa?
donfuxx

1
exclude group: 'com.android.support' exclude module: 'recyclerview-v7'
dovevo

1
È inutile quando si desidera fare clic sulla casella di controllo all'interno di RecyclerView.
Farid

2
In kotlin actionOnItemAtPositionrichiede un parametro di tipo. Non sono sicuro di cosa mettere lì.
ZeroDivide

7

Ecco come ho risolto il problema in 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))
}

e poi

onView(withId(R.id.recyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(position, clickOnViewChild(R.id.viewToClickInTheRow)))

Ho incontrato E / TestRunner: java.lang.NullPointerException: tentativo di richiamare il metodo virtuale 'void android.view.View.getLocationOnScreen (int [])' su un riferimento a un oggetto nullo; qualche idea del perché?
sayvortana il

6

Prova il prossimo approccio:

    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

Puoi fare clic sul terzo elemento di recyclerViewCome questo:

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

Non dimenticare di fornire il ViewHoldertipo in modo che l'inferenza non fallisca.


1
Mi mancava <RecyclerView.ViewHolder>grazie signore;)
Skizo-ozᴉʞS

0

Tutte le risposte sopra non hanno funzionato per me, quindi ho creato un nuovo metodo che cerca tutte le visualizzazioni all'interno di una cella per restituire la visualizzazione con l'ID richiesto. Richiede due metodi (potrebbe essere combinato in uno):

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

Infine, puoi utilizzarlo su Recycler View facendo quanto segue:

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

È possibile utilizzare una richiamata per restituire la visualizzazione se si desidera fare qualcos'altro con essa, o semplicemente creare 3-4 versioni diverse di questo per eseguire altre attività.


0

Ho continuato a provare vari metodi per scoprire perché la risposta di @ blade non funzionava per me, solo per rendermi conto che ne avevo una OnTouchListener(), ho modificato ViewAction di conseguenza:

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

Puoi anche generalizzare questo approccio per supportare più azioni non solo click. Ecco la mia soluzione per questo:

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

E poi per EditTextte puoi fare qualcosa del genere:

onView(withId(R.id.yourRecyclerView))
        .perform(
            actionOnItemAtPosition<YourViewHolder>(
                0,
                recyclerChildAction<EditText>(R.id.editTextId) { setText("1000") }
            )
        )

-5

Per prima cosa dai ai tuoi pulsanti contentDescriptions univoci, ad esempio "riga 5 del pulsante di consegna".

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

Quindi scorri fino alla riga:

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

Quindi seleziona la visualizzazione in base a contentDescription.

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

La descrizione del contenuto è un ottimo modo per utilizzare onView di Espresso e rendere la tua app più accessibile.


5
La descrizione del contenuto non deve essere utilizzata in questo modo - non rende la tua app più accessibile per le visualizzazioni per fornire informazioni tecniche o di debug per gli utenti con esigenze a11y - il CD per questo dovrebbe probabilmente essere qualcosa come "Order <item summary> pulsante". withText("Order")funzionerebbe anche e non ti richiederebbe di sapere in fase di prova cosa stai ordinando.
ataulm
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.