web-dev-qa-db-fra.com

Espresso, le défilement ne fonctionne pas lorsque NestedScrollView ou RecyclerView est dans CoordinatorLayout

Il semble que CoordinatorLayout casse le comportement des actions Espresso telles que scrollTo() ou RecyclerViewActions.scrollToPosition().

Problème avec NestedScrollView

Pour une mise en page comme celle-ci:

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

Si j'essaie de faire défiler vers n'importe quelle vue à l'intérieur du NestedScrollView en utilisant ViewActions.scrollTo() le premier problème que je trouve est que j'obtiens un PerformException. En effet, cette action prend uniquement en charge ScrollView et NestedScrollView ne l'étend pas. Une solution de contournement pour ce problème est expliquée ici , fondamentalement, nous pouvons copier le code dans scrollTo() et changer les contraintes pour prendre en charge NestedScrollView. Cela semble fonctionner si le NestedScrollView n'est pas dans un CoordinatorLayout mais dès que vous le placez dans un CoordinatorLayout l'action de défilement échoue.

Problème avec RecyclerView

Pour la même mise en page, si je remplace le NestedScrollView par un RecyclerView il y a aussi des problèmes de défilement.

Dans ce cas, j'utilise RecyclerViewAction.scrollToPosition(position). Contrairement au NestedScrollView, ici je peux voir un certain défilement. Cependant, il semble qu'il défile dans la mauvaise position. Par exemple, si je défile jusqu'à la dernière position, cela rend visible l'avant-dernier mais pas le dernier. Lorsque je déplace le RecyclerView hors du CoordinatorLayout le défilement fonctionne comme il se doit.

Pour le moment, nous ne pouvons pas écrire de test Espresso pour les écrans qui utilisent CoordinatorLayout en raison de ces problèmes. Quelqu'un connaît les mêmes problèmes ou connaît une solution?

36
ivacf

Cela se produit car la méthode Espresso scrollTo () vérifie explicitement la classe de disposition et ne fonctionne que pour ScrollView et HorizontalScrollView. En interne, il utilise View.requestRectangleOnScreen (...), je m'attends donc à ce que cela fonctionne réellement pour de nombreuses mises en page.

Ma solution de contournement pour NestedScrollView était de prendre ScrollToAction et de modifier cette contrainte. L'action modifiée a bien fonctionné pour NestedScrollView avec ce changement.

Méthode modifiée dans la classe ScrollToAction:

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

Méthode pratique:

public static ViewAction betterScrollTo() {
    return ViewActions.actionWithAssertions(new NestedScrollToAction());
}
26
Turnsole

Voici comment j'ai fait la même chose que @miszmaniac à Kotlin. Avec délégation dans Kotlin , c'est beaucoup plus propre et plus facile car je n'ai pas à remplacer les méthodes dont je n'ai pas besoin.

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
  )
}
16
tasomaniac

J'ai eu ce problème avec CoordinatorLayout-> ViewPager-> NestedScrollView un travail facile autour de moi pour obtenir le même comportement scrollTo () consistait à simplement balayer l'écran:

onView(withId(Android.R.id.content)).perform(ViewActions.swipeUp());
11
MR Mido

Ce problème a été signalé (peut-être par l'OP?), Voir problème 203684

L'un des commentaires sur ce problème suggère une solution de contournement au problème lorsque le NestedScrollView est à l'intérieur d'un CoordinatorLayout:

vous devez supprimer le @string/appbar_scrolling_view_behavior comportement de mise en page du ScrollingView ou de toute vue parente dans laquelle ce ScrollingView est inclus

Voici une implémentation de cette solution de contournement:

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

J'ai pu faire fonctionner mes tests par:

  1. Faire une action scrollTo () personnalisée (comme référencé par l'OP et Turnsole)
  2. Suppression des paramètres de disposition de NestedScrollView comme indiqué ici
4
Clo Knibbe

La scrollTo(R.id.button) de Barista fonctionne sur toutes sortes de vues déroulantes, également sur NestedScrollView.

Il est utile de résoudre ce type de problèmes avec Espresso. Nous le développons et l'utilisons juste pour écrire des tests Espresso de manière rapide et fiable. Et voici un lien: https://github.com/SchibstedSpain/Barista

3
Roc Boronat

La solution de M. Mido peut fonctionner dans certaines situations, mais pas toujours. Si vous avez une vue en bas de l'écran, le défilement de votre RecyclerView ne se produira pas car le clic commencera en dehors de RecyclerView.

Une façon de contourner ce problème consiste à écrire un SwipeAction personnalisé. Comme ça:

1 - Créez le 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 - Créez la méthode pour retourner le 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 - Ensuite, utilisez-le pour faire défiler l'écran:

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

Et c'est tout! De cette façon, vous pouvez contrôler la façon dont le défilement se produira sur votre écran.

J'ai créé une classe NestedScrollViewScrollToAction.

Je pense que c'est un meilleur endroit pour y faire des choses spécifiques à l'activité.

La seule chose à noter est que le code recherche le parent nestedScrollView et supprime son comportement CoordinatorLayout.

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

2
miszmaniac