Usando Espresso para hacer clic en la vista dentro del elemento RecyclerView

100

¿Cómo puedo usar Espresso para hacer clic en una vista específica dentro de un elemento RecyclerView ? Sé que puedo hacer clic en el elemento en la posición 0 usando:

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

Pero necesito hacer clic en una vista específica dentro de ese elemento y no en el elemento en sí.

Gracias por adelantado.

- editar -

Para ser más precisos: tengo un RecyclerView ( R.id.recycler_view) cuyos elementos son CardView ( R.id.card_view). Dentro de cada CardView tengo cuatro botones (entre otras cosas) y quiero hacer clic en un botón específico ( R.id.bt_deliver).

Me gustaría utilizar las nuevas funciones de Espresso 2.0, pero no estoy seguro de que sea posible.

Si no es posible, quiero usar algo como esto (usando el código de Thomas Keller):

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

pero no sé qué poner en los signos de interrogación.

Filipe Ramos
fuente
Mira esto
Skizo-ozᴉʞS
1
¡Hola, tanques para una respuesta rápida! :-) He visto esa pregunta, pero no puedo encontrar ninguna ayuda sobre cómo usar RecyclerViewActions para hacer lo que quiero. ¿Debería usar la respuesta anterior ? Gracias.
Filipe Ramos
¿Ha encontrado alguna solución a su problema?
HowieH
1
@HowieH: No. Me di por vencido, y ahora estoy usando Robotium ...
Filipe Ramos

Respuestas:

136

Puede hacerlo con la acción de vista personalizada.

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

}

Entonces puedes hacer clic en él con

onView(withId(R.id.rv_conference_list)).perform(
            RecyclerViewActions.actionOnItemAtPosition(0, MyViewAction.clickChildViewWithId(R.id. bt_deliver)));
espada
fuente
2
Quitaría la verificación nula, de modo que si no se encuentra la vista secundaria, se lanza una excepción en lugar de no hacer nada en silencio.
Alvaro Gutierrez Perez
Gracias por la respuesta. Pero golpeó en un problema. En la función onPerform (), si utilicé la función onView (withId ()), la función se activa. ¿Cuál es la razón de este comportamiento?
thedarkpassenger
3
funciona con espresso: espresso-contrib: 2.2.2 pero no con 2.2.1. ¿Por qué? Tengo algunas dependencias porque no puedo usar 2.2.2.
RosAng
3
Estaba obteniendo una NullPointerException con esto originalmente, así que tuve que implementar getConstraints () para evitar eso. Lo siguiente funcionó para mí: public Matcher <View> getConstraints () {return isAssignableFrom (View.class); }
dfinn
La solución anterior no me funciona. Recibo el siguiente error: android.support.test.espresso.PerformException: Error al realizar 'android.support.test.espresso.contrib.RecyclerViewActions$ActionOnItemAtPositionViewAction@fad9df' en vista 'con id: adamhurwitz.github.io.doordashlite : id / recyclingView '.
Adam Hurwitz
67

Ahora con android.support.test.espresso.contrib se ha vuelto más fácil:

1) Agregar dependencia de prueba

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

* excluye 3 módulos, porque es muy probable que ya lo tengas

2) Entonces haz algo como

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"))));
Andrey
fuente
1
Gracias, no excluir esos módulos da como resultado un java.lang.IncompatibleClassChangeError para mí.
hboy
8
y qué hacer para hacer clic, digamos en el quinto elemento de la lista una vista específica? en los ejemplos proporcionados, solo veo cómo hacer clic en el quinto elemento o hacer clic en un elemento con vista descendiente, pero no ambos juntos, ¿o me perdí algo?
donfuxx
1
tuve que hacerexclude group: 'com.android.support' exclude module: 'recyclerview-v7'
Jemshit Iskenderov
1
Es inútil cuando desea hacer clic en la casilla de verificación dentro de RecyclerView.
Farid
2
En kotlin se actionOnItemAtPositionrequiere un tipo param. No estoy seguro de qué poner allí.
ZeroDivide
7

Así es como resolví el problema en 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))
}

y entonces

onView(withId(R.id.recyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(position, clickOnViewChild(R.id.viewToClickInTheRow)))
usuario2768856
fuente
Encontré E / TestRunner: java.lang.NullPointerException: intento de invocar el método virtual 'void android.view.View.getLocationOnScreen (int [])' en una referencia de objeto nulo; alguna idea de por qué?
sayvortana
6

Prueba el siguiente enfoque:

    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
fuente
3

Puede hacer clic en el tercer elemento de recyclerViewMe gusta:

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

No olvide proporcionar el ViewHoldertipo para que la inferencia no falle.

erluxman
fuente
1
Me faltaba <RecyclerView.ViewHolder>gracias señor;)
Skizo-ozᴉʞS
0

Todas las respuestas anteriores no funcionaron para mí, así que he creado un nuevo método que busca todas las vistas dentro de una celda para devolver la vista con la ID solicitada. Requiere dos métodos (se pueden combinar en 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;
}

Luego, finalmente, puede usar esto en Recycler View haciendo lo siguiente:

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

Puede usar una devolución de llamada para devolver la vista si desea hacer algo más con ella, o simplemente crear 3-4 versiones diferentes de esto para realizar otras tareas.

paul_f
fuente
0

Seguí probando varios métodos para encontrar por qué la respuesta de @ blade no funcionaba para mí, solo para darme cuenta de que tengo un OnTouchListener(), modifiqué ViewAction en consecuencia:

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
fuente
0

Incluso puede generalizar este enfoque para respaldar más acciones, no solo click. Aquí está mi solución para esto:

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

Y luego EditTextpuedes hacer algo como esto:

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

En primer lugar, asigne descripciones de contenido únicas a sus botones, es decir, "fila 5 del botón de entrega".

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

Luego, desplácese a la fila:

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

Luego seleccione la vista basada en contentDescription.

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

La descripción del contenido es una excelente manera de usar onView de Espresso y hacer que su aplicación sea más accesible.

RPGObjects
fuente
5
La descripción del contenido no debe usarse de tal manera; no hace que su aplicación sea más accesible para que las vistas brinden información técnica o de depuración para los usuarios con todas las necesidades; el CD para esto probablemente debería ser algo así como "Pedido <resumen del artículo> botón". withText("Order")también funcionaría y no requeriría que sepas en el momento de la prueba lo que estás pidiendo.
ataulm