ใช้ Espresso เพื่อคลิกดูภายในรายการ RecyclerView


102

ฉันจะใช้ 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 แต่ไม่แน่ใจว่าเป็นไปได้

ถ้าเป็นไปไม่ได้ฉันต้องการใช้สิ่งนี้ (โดยใช้รหัส Thomas Keller):

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

แต่ไม่รู้จะใส่เครื่องหมายคำถามอะไรดี


ตรวจสอบสิ่งนี้
Skizo-ozᴉʞS

1
สวัสดีรถถังสำหรับคำตอบด่วน! :-) ฉันได้เห็นคำถามนั้นแล้ว แต่ไม่พบความช่วยเหลือใด ๆ เกี่ยวกับวิธีใช้RecyclerViewActionsเพื่อทำสิ่งที่ฉันต้องการ ฉันควรใช้คำตอบเก่าหรือไม่ ขอบคุณ.
Filipe Ramos

คุณพบวิธีแก้ปัญหาของคุณหรือไม่?
HowieH

1
@HowieH: ไม่ฉันยอมแพ้และตอนนี้ฉันใช้ Robotium ...
Filipe Ramos

คำตอบ:


139

คุณสามารถทำได้โดยปรับแต่งมุมมองการกระทำ

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
ฉันจะลบการตรวจสอบค่าว่างดังนั้นหากไม่พบมุมมองลูกจะมีการโยนข้อยกเว้นแทนที่จะทำอะไรเงียบ ๆ
Alvaro Gutierrez Perez

ขอบคุณสำหรับคำตอบ. แต่หลงในปัญหาเดียว. ในฟังก์ชัน onPerform () ถ้าฉันใช้ฟังก์ชัน onView (withId ()) ฟังก์ชันจะถูกขีดฆ่า สาเหตุของพฤติกรรมนี้คืออะไร?
thedarkpassenger

3
ใช้ได้กับเอสเปรสโซ: เอสเพรสโซ - ส่วน: 2.2.2 แต่ไม่ใช่กับ 2.2.1 สาเหตุใด ฉันมีการอ้างอิงบางอย่างเนื่องจากฉันไม่สามารถใช้ 2.2.2 ได้
RosAng

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' ในมุมมอง 'พร้อม id: adamhurwitz.github.io.doordashlite : id / RecyclerView '
Adam Hurwitz

68

ตอนนี้มี 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'
Jemshit Iskenderov

1
ไม่มีประโยชน์เมื่อคุณต้องการคลิกที่ช่องทำเครื่องหมายใน RecyclerView
Farid

2
ใน kotlin actionOnItemAtPositionต้องการพารามิเตอร์ประเภท ไม่แน่ใจว่าจะใส่อะไรที่นั่น
ZeroDivide

7

นี่คือวิธีที่ฉันแก้ไขปัญหาใน 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))
}

แล้ว

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ประเภทเพื่อให้การอนุมานไม่ล้มเหลว


1
ฉันหายไป<RecyclerView.ViewHolder>ขอบคุณครับ;)
Skizo-ozᴉʞS

0

คำตอบทั้งหมดข้างต้นไม่ได้ผลสำหรับฉันดังนั้นฉันจึงได้สร้างวิธีการใหม่ที่ค้นหามุมมองทั้งหมดภายในเซลล์เพื่อส่งคืนมุมมองพร้อมกับ ID ที่ร้องขอ ต้องใช้สองวิธี (อาจรวมกันเป็นหนึ่ง):

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 เวอร์ชันเพื่อทำงานอื่น ๆ


0

ฉันพยายามหาวิธีการต่างๆOnTouchListener()อยู่เรื่อยๆ เพื่อค้นหาว่าทำไมคำตอบของ @ blade ไม่ได้ผลสำหรับฉันเพียงแค่รู้ว่าฉันมีฉันก็แก้ไข 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));

จากนั้นเลือกมุมมองตาม contentDescription

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

คำอธิบายเนื้อหาเป็นวิธีที่ยอดเยี่ยมในการใช้ onView ของ Espresso และทำให้แอปของคุณสามารถเข้าถึงได้มากขึ้น


5
ไม่ควรใช้คำอธิบายเนื้อหาในลักษณะนี้ - จะไม่ทำให้แอปของคุณสามารถเข้าถึงได้มากขึ้นสำหรับมุมมองเพื่อเผยแพร่ข้อมูลทางเทคนิคหรือการแก้ไขข้อบกพร่องสำหรับผู้ใช้ที่มีความต้องการ a11y ซีดีสำหรับสิ่งนี้น่าจะเป็น "Order <item summary> ปุ่ม". withText("Order")ก็ใช้ได้ผลเช่นกันและไม่ต้องการให้คุณทราบในเวลาทดสอบว่าคุณสั่งอะไร
ataulm
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.