41

看起来像是CoordinatorLayout破坏了 Espresso 操作的行为,例如scrollTo()or RecyclerViewActions.scrollToPosition()

NestedScrollView 的问题

对于这样的布局:

<android.support.design.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        ...

    </android.support.v4.widget.NestedScrollView>

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        ...

    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

如果我尝试滚动到NestedScrollView使用里面的任何视图ViewActions.scrollTo(),我发现的第一个问题是我得到一个PerformException. 这是因为这个动作只支持ScrollViewNestedScrollView不是扩展它。这里解释了这个问题的解决方法,基本上我们可以复制代码scrollTo()并更改约束以支持NestedScrollViewNestedScrollView如果不在 a 中,这似乎可行,CoordinatorLayout但只要将其放入 a 中CoordinatorLayout,滚动操作就会失败。

RecyclerView 的问题

对于相同的布局,如果我NestedScrollView用 a替换RecyclerView,滚动也会出现问题。

在这种情况下,我使用RecyclerViewAction.scrollToPosition(position). 与 不同NestedScrollView,在这里我可以看到一些滚动发生。但是,它看起来像滚动到错误的位置。例如,如果我滚动到最后一个位置,它会显示倒数第二个而不是最后一个。当我将滚动作品RecyclerView移出时,它应该是。CoordinatorLayout

CoordinatorLayout由于这个问题,目前我们无法为使用的屏幕编写任何 Espresso 测试。有人遇到同样的问题或知道解决方法吗?

4

8 回答 8

31

发生这种情况是因为 Espresso scrollTo() 方法显式检查布局类并且仅适用于 ScrollView 和 Horizo​​ntalScrollView。在内部它使用 View.requestRectangleOnScreen(...) 所以我希望它实际上适用于许多布局。

我对 NestedScrollView 的解决方法是采用 ScrollToAction 并修改该约束。修改后的操作对 NestedScrollView 的更改效果很好。

ScrollToAction 类中的更改方法:

public Matcher<View> getConstraints() {
    return allOf(withEffectiveVisibility(Visibility.VISIBLE), isDescendantOfA(anyOf(
            isAssignableFrom(ScrollView.class), isAssignableFrom(HorizontalScrollView.class), isAssignableFrom(NestedScrollView.class))));
}

便捷方法:

public static ViewAction betterScrollTo() {
    return ViewActions.actionWithAssertions(new NestedScrollToAction());
}
于 2016-03-09T01:10:39.197 回答
26

以下是我在 Kotlin 中所做的与 @miszmaniac 相同的操作。使用Kotlin 中的委托,它更加简洁和容易,因为我不必重写我不需要的方法。

class ScrollToAction(
    private val original: android.support.test.espresso.action.ScrollToAction = android.support.test.espresso.action.ScrollToAction()
) : ViewAction by original {

  override fun getConstraints(): Matcher<View> = anyOf(
      allOf(
          withEffectiveVisibility(Visibility.VISIBLE),
          isDescendantOfA(isAssignableFrom(NestedScrollView::class.java))),
      original.constraints
  )
}
于 2017-11-23T09:00:54.350 回答
15

我在 CoordinatorLayout->ViewPager->NestedScrollView 遇到了这个问题,我很容易获得相同的 scrollTo() 行为,只需在屏幕上向上滑动即可:

onView(withId(android.R.id.content)).perform(ViewActions.swipeUp());
于 2016-12-14T21:26:33.787 回答
5

Mido 先生的解决方案可能在某些情况下有效,但并非总是如此。如果您在屏幕底部有一些视图,则 RecyclerView 的滚动将不会发生,因为单击将在 RecyclerView 之外开始。

解决此问题的一种方法是编写自定义 SwipeAction。像这样:

1 - 创建 CenterSwipeAction

public class CenterSwipeAction implements ViewAction {

    private final Swiper swiper;
    private final CoordinatesProvider startCoordProvide;
    private final CoordinatesProvider endCoordProvide;
    private final PrecisionDescriber precDesc;

    public CenterSwipeAction(Swiper swiper, CoordinatesProvider startCoordProvide,
                             CoordinatesProvider endCoordProvide, PrecisionDescriber precDesc) {
        this.swiper = swiper;
        this.startCoordProvide = startCoordProvide;
        this.endCoordProvide = endCoordProvide;
        this.precDesc = precDesc;
    }

    @Override public Matcher<View> getConstraints() {
        return withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE);
    }

    @Override public String getDescription() {
        return "swipe from middle of screen";
    }

    @Override
    public void perform(UiController uiController, View view) {
        float[] startCoord = startCoordProvide.calculateCoordinates(view);
        float[] finalCoord = endCoordProvide.calculateCoordinates(view);
        float[] precision =  precDesc.describePrecision();

        // you could try this for several times until Swiper.Status is achieved or try count is reached
        try {
            swiper.sendSwipe(uiController, startCoord, finalCoord, precision);
        } catch (RuntimeException re) {
            throw new PerformException.Builder()
                    .withActionDescription(this.getDescription())
                    .withViewDescription(HumanReadables.describe(view))
                    .withCause(re)
                    .build();
        }

        // ensures that the swipe has been run.
        uiController.loopMainThreadForAtLeast(ViewConfiguration.getPressedStateDuration());
    }
}

2 - 创建返回 ViewAction 的方法

    private static ViewAction swipeFromCenterToTop() {
        return new CenterSwipeAction(Swipe.FAST,
                GeneralLocation.CENTER,
                view -> {
                    float[] coordinates =  GeneralLocation.CENTER.calculateCoordinates(view);
                    coordinates[1] = 0;
                    return coordinates;
                },
                Press.FINGER);
    }

3 - 然后用它来滚动屏幕:

onView(withId(android.R.id.content)).perform(swipeFromCenterToTop());

就是这样!通过这种方式,您可以控制滚动在屏幕中的发生方式。

于 2017-05-01T18:44:57.813 回答
5

BaristascrollTo(R.id.button)在各种可滚动视图上的作品,也在NestedScrollView.

使用 Espresso 解决此类问题很有用。我们开发和使用它只是为了以快速可靠的方式编写 Espresso 测试。这是一个链接:https ://github.com/SchibstedSpain/Barista

于 2017-06-08T14:42:02.813 回答
4

此问题已报告(可能由 OP?),请参阅问题 203684

当 NestedScrollView 位于 CoordinatorLayout 内部时,对该问题的其中一条评论表明该问题的解决方法:

您需要删除@string/appbar_scrolling_view_behaviorScrollingView 或包含此 ScrollingView 的任何父视图的布局行为

这是该解决方法的实现:

    activity.runOnUiThread(new Runnable() {
        @Override
        public void run() {
            // remove CoordinatorLayout.LayoutParams from NestedScrollView
            NestedScrollView nestedScrollView = (NestedScrollView)activity.findViewById(scrollViewId);
            CoordinatorLayout.LayoutParams params =
                    (CoordinatorLayout.LayoutParams)nestedScrollView.getLayoutParams();
            params.setBehavior(null);
            nestedScrollView.requestLayout();
        }
    });

我能够通过以下方式进行测试:

  1. 制作自定义 scrollTo() 动作(由 OP 和 Turnsole 引用)
  2. 删除 NestedScrollView 的布局参数,如下所示
于 2016-08-09T19:34:53.250 回答
2

我制作了一个 NestedScrollViewScrollToAction 类。

我认为在那里制作特定于活动的东西是更好的地方。

唯一值得一提的是代码搜索父嵌套滚动视图并删除它的 CoordinatorLayout 行为。

https://gist.github.com/miszmaniac/12f720b7e898ece55d2464fe645e1f36

于 2016-09-12T12:52:49.980 回答
0

我必须测试 recyclerview 项目。我的 RecyclerView 位于 CoordinatorLayout 内的 NestedScrollView 中。

以下是对我有用的解决方案,我觉得它是在 NestedScrollView 中测试 RecyclerView 项目的最合适的解决方案。

第1步:复制并粘贴以下功能

以下将从我们即将测试的 recyclerView 返回所需的子视图。

fun atPositionOnView(recyclerViewId: Int, position: Int, childViewIdToTest: Int): Matcher<View?>? {
    return object : TypeSafeMatcher<View?>() {
        var resources: Resources? = null
        var childView: View? = null
        override fun describeTo(description: Description?) {
            var idDescription = Integer.toString(recyclerViewId)
            if (resources != null) {
                idDescription = try {
                    resources!!.getResourceName(recyclerViewId)
                } catch (var4: Resources.NotFoundException) {
                    String.format("%s (resource name not found)",
                            *arrayOf<Any?>(Integer.valueOf(recyclerViewId)))
                }
            }
            description?.appendText("with id: $idDescription")
        }

        override fun matchesSafely(view: View?): Boolean {
            resources = view?.getResources()
            if (childView == null) {
                val recyclerView = view?.getRootView()?.findViewById(recyclerViewId) as RecyclerView
                childView = if (recyclerView != null && recyclerView.id == recyclerViewId) {
                    recyclerView.findViewHolderForAdapterPosition(position)!!.itemView
                } else {
                    return false
                }
            }

            return if (viewId == -1) {
                view === childView
            } else {
                val targetView = childView!!.findViewById<View>(viewId)
                view === targetView
            }
        }
    }
}

第2步:现在复制并粘贴下面的功能

以下将检查您的孩子在 recyclerView 中是否正在显示。

fun ViewInteraction.isNotDisplayed(): Boolean {
    return try {
        check(matches(not(isDisplayed())))
        true
    } catch (e: Error) {
        false
    }
}

第 3 步:测试您的 recyclerView 项目并滚动它们是否不在屏幕上

以下将滚动未显示的孩子并使其出现在屏幕上。

if (onView(atPositionOnView(R.id.rv_items, pos, R.id.tv_item_name)).isNotDisplayed()) {
            val appViews = UiScrollable(UiSelector().scrollable(true))
            appViews.scrollForward() //appViews.scrollBackward()
        }

显示视图后,您可以执行测试用例。

于 2020-05-04T14:03:24.187 回答