Espresso - Check RecyclerView items are ordered co

2019-02-27 14:55发布

问题:

How to go about checking whether RecyclerView items are displayed in the correct order using Espresso? I'm trying to test it checking it by the text for the title of each element.

When I try this piece of code it works to click the element but can't go on to instead of performing a click trying to Assert the text for the element

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

When I try to use a custom matcher instead I keep getting the error

Error performing 'load adapter data' on view 'with id: mypackage_name:id/rv_metrics'

I know now onData doesn't work for RecyclerView but before that I was trying to use a custom matcher for this task.

 public static Matcher<Object> hasTitle(final String inputString) {
    return new BoundedMatcher<Object, Metric>(Metric.class) {
        @Override
        protected boolean matchesSafely(Metric metric) {

            return inputString.equals(metric.getMetric());

        }

        @Override
        public void describeTo(org.hamcrest.Description description) {
            description.appendText("with title: ");
        }
    };
}

I also tried something like this but it obviously doesn't work due to the type given as parameter to the actionOnItemAtPosition method but would we have something similar to it that could maybe work?

onView(withId(R.id.rv_metrics)).check(actionOnItemAtPosition(0, ViewAssertions.matches(withText("Weight"))));

What am I missing here please? Thanks a lot.

回答1:

As it's been mentioned here, RecyclerView objects work differently than AdapterView objects, so onData() cannot be used to interact with them.

In order to find a view at specific position of a RecyclerView you need to implement a custom RecyclerViewMatcher like below:

public class RecyclerViewMatcher {
    private final int recyclerViewId;

    public RecyclerViewMatcher(int recyclerViewId) {
        this.recyclerViewId = 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) {
                String idDescription = Integer.toString(recyclerViewId);
                if (this.resources != null) {
                    try {
                        idDescription = this.resources.getResourceName(recyclerViewId);
                    } catch (Resources.NotFoundException var4) {
                        idDescription = String.format("%s (resource name not found)",
                                                      new Object[] { Integer.valueOf
                                                          (recyclerViewId) });
                    }
                }

                description.appendText("with id: " + idDescription);
            }

            public boolean matchesSafely(View view) {

                this.resources = view.getResources();

                if (childView == null) {
                    RecyclerView recyclerView =
                        (RecyclerView) view.getRootView().findViewById(recyclerViewId);
                    if (recyclerView != null && recyclerView.getId() == recyclerViewId) {
                        childView = recyclerView.findViewHolderForAdapterPosition(position).itemView;
                    }
                    else {
                        return false;
                    }
                }

                if (targetViewId == -1) {
                    return view == childView;
                } else {
                    View targetView = childView.findViewById(targetViewId);
                    return view == targetView;
                }

            }
        };
    }
}

And then use it in your test case in this way:

@Test
void testCase() {
    onView(new RecyclerViewMatcher(R.id.rv_metrics)
        .atPositionOnView(0, R.id.txt_title))
        .check(matches(withText("Weight")))
        .perform(click());

    onView(new RecyclerViewMatcher(R.id.rv_metrics)
        .atPositionOnView(1, R.id.txt_title))
        .check(matches(withText("Height")))
        .perform(click());
}


回答2:

If you want to match a matcher on a position in RecyclerView, then you can try to create a custom Matcher<View>:

public static Matcher<View> hasItemAtPosition(int position, Matcher<View> matcher) {
    return new BoundedMatcher<View, RecyclerView>(RecyclerView.class) {

        @Override public void describeTo(Description description) {
            description.appendText("has item: ");
            matcher.describeTo(description);
            description.appendText(" at position: " + position);
        }

        @Override protected boolean matchesSafely(RecyclerView view) {
            RecyclerView.Adapter adapter = view.getAdapter();
            int type = adapter.getItemViewType(position);
            RecyclerView.ViewHolder holder = adapter.createViewHolder(view, type);
            adapter.onBindViewHolder(holder, position);
            return matcher.matches(holder.itemView);
        }
    };
}

And you can use it for example:

onView(withId(R.id.rv_metrics)).check(matches(0, hasDescendant(withText("Weight")))))


回答3:

I simplified a bit Mosius answer:

public static Matcher<View> hasItemAtPosition(final Matcher<View> matcher, final int position) {
    return new BoundedMatcher<View, RecyclerView>(RecyclerView.class) {
        @Override
        public void describeTo(Description description) {
            description.appendText("has item at position " + position + ": ");
            matcher.describeTo(description);
        }
        @Override
        protected boolean matchesSafely(RecyclerView recyclerView) {
            RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(position);
            return matcher.matches(viewHolder.itemView);
        }
    };
}

We pass Matcher to the function so we can provide further conditions. Example usage:

onView(hasItemAtPosition(hasDescendant(withText("Item 1")), 0)).check(matches(isDisplayed()));
onView(hasItemAtPosition(hasDescendant(withText("Item 2")), 1)).check(matches(isDisplayed()));


回答4:

The original problem has been solved but am posting an answer here as found the Barista library solves this problem in one single line of code.

assertDisplayedAtPosition(R.id.rv_metrics, 0, R.id.tv_title, "weight");

It's made on top of Espresso and the documentation for it can be found here

Hope this may be helpful to someone. :)