Usando o Espresso para clicar em visualizar dentro do item RecyclerView

100

Como posso usar o Espresso para clicar em uma visualização específica dentro de um item RecyclerView ? Sei que posso clicar no item na posição 0 usando:

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

Mas preciso clicar em uma visualização específica dentro desse item e não no próprio item.

Desde já, obrigado.

- editar -

Para ser mais preciso: eu tenho um RecyclerView ( R.id.recycler_view) cujos itens são CardView ( R.id.card_view). Dentro de cada CardView tenho quatro botões (entre outras coisas) e quero clicar em um botão específico ( R.id.bt_deliver).

Gostaria de usar os novos recursos do Espresso 2.0, mas não tenho certeza se isso é possível.

Se não for possível, quero usar algo assim (usando o código de Thomas Keller):

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

mas não sei o que colocar nos pontos de interrogação.

Filipe Ramos
fonte
Verifique isto
Skizo-ozᴉʞS
1
Olá, tanques pela resposta rápida! :-) Eu já vi essa pergunta, mas não consigo encontrar nenhuma ajuda sobre como usar o RecyclerViewActions para fazer o que quero. Devo usar a resposta antiga ? Obrigado.
Filipe Ramos
Você encontrou alguma solução para o seu problema?
HowieH
1
@HowieH: Não. Desisti e agora estou a usar o Robotium ...
Filipe Ramos

Respostas:

136

Você pode fazer isso com a ação de visualização 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();
            }
        };
    }

}

Então você pode clicar nele com

onView(withId(R.id.rv_conference_list)).perform(
            RecyclerViewActions.actionOnItemAtPosition(0, MyViewAction.clickChildViewWithId(R.id. bt_deliver)));
lâmina
fonte
2
Eu removeria a verificação de nulo, de modo que, se a visão filha não fosse encontrada, uma exceção fosse lançada em vez de não fazer nada silenciosamente.
Alvaro Gutierrez Perez
Obrigado pela resposta. Mas atingiu um problema. Na função onPerform (), se eu usar a função onView (withId ()), a função será atingida. Qual é o motivo desse comportamento?
thedarkpassenger de
3
funciona com espresso: espresso-contrib: 2.2.2 mas não com 2.2.1 alguma razão por quê? Tenho algumas dependências porque não consigo usar 2.2.2.
RosAng
3
Eu estava recebendo uma NullPointerException com isso originalmente, então tive que implementar getConstraints () para evitar isso. O seguinte funcionou para mim: public Matcher <View> getConstraints () {return isAssignableFrom (View.class); }
dfinn
A solução acima não está funcionando para mim. Estou recebendo o seguinte erro: android.support.test.espresso.PerformException: Erro ao executar 'android.support.test.espresso.contrib.RecyclerViewActions$ActionOnItemAtPositionViewAction@fad9df' on view 'com id: adamhurwitz.github.io.doordashlite : id / recyclerView '.
Adam Hurwitz
67

Agora, com android.support.test.espresso.contrib, ficou mais fácil:

1) Adicionar dependência de teste

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

* exclua 3 módulos, porque muito provavelmente você já os tem

2) Em seguida, faça algo como

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"))));
Andrey
fonte
1
Obrigado, não excluir esses módulos resulta em um java.lang.IncompatibleClassChangeError para mim.
hboy
8
e o que fazer para clicar digamos no 5º item da lista uma vista específica? nos exemplos fornecidos, vejo apenas como clicar no quinto item ou clicar em um item com visualização descendente, mas não ambos juntos, ou perdi algo?
donfuxx
1
eu tive que fazerexclude group: 'com.android.support' exclude module: 'recyclerview-v7'
Jemshit Iskenderov
1
É inútil quando você deseja clicar na caixa de seleção dentro do RecyclerView.
Farid
2
Em kotlin actionOnItemAtPositionrequer um parâmetro de tipo. Não tenho certeza do que colocar lá.
ZeroDivide
7

Aqui está como resolvi o problema no 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 depois

onView(withId(R.id.recyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(position, clickOnViewChild(R.id.viewToClickInTheRow)))
user2768856
fonte
Encontrei E / TestRunner: java.lang.NullPointerException: Tentativa de invocar o método virtual 'void android.view.View.getLocationOnScreen (int [])' em uma referência de objeto nulo; alguma ideia do porquê?
sayvortana
6

Tente a próxima abordagem:

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

Você pode clicar no terceiro item de recyclerViewcomo este:

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

Não se esqueça de fornecer o ViewHoldertipo para que a inferência não falhe.

Erluxman
fonte
1
Eu estava faltando, <RecyclerView.ViewHolder>obrigado senhor;)
Skizo-ozᴉʞS
0

Todas as respostas acima não funcionaram para mim, então eu construí um novo método que pesquisa todas as visualizações dentro de uma célula para retornar a visualização com o ID solicitado. Requer dois métodos (podem ser combinados em um):

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

Por fim, você pode usar isso no Recycler View fazendo o seguinte:

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

Você pode usar um retorno de chamada para retornar a visualização se quiser fazer algo mais com ela, ou apenas construir 3-4 versões diferentes disso para fazer qualquer outra tarefa.

paul_f
fonte
0

Continuei tentando vários métodos para descobrir por que a resposta de OnTouchListener()@blade não estava funcionando para mim, apenas para perceber que eu tinha um , modifiquei ViewAction de acordo:

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

Você pode até mesmo generalizar essa abordagem para apoiar mais ações não só click. Aqui está minha solução para isso:

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 então EditTextvocê pode fazer algo assim:

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

Primeiro, dê a seus botões contentDescriptions exclusivos, ou seja, "linha 5 do botão de entrega".

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

Em seguida, role para a linha:

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

Em seguida, selecione a exibição com base em contentDescription.

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

A descrição do conteúdo é uma ótima maneira de usar o onView do Espresso e tornar seu aplicativo mais acessível.

RPGObjects
fonte
5
A descrição do conteúdo não deve ser usada dessa forma - ela não torna seu aplicativo mais acessível para visualizações para fornecer informações técnicas ou de depuração para usuários com todas as necessidades - o CD para isso provavelmente deve ser algo como "Pedido <resumo do item> botão". withText("Order")funcionaria também e não exigiria que você soubesse na hora do teste o que está pedindo.
ataulm