Home | History | Annotate | Download | only in scroll
      1 /*
      2  * Copyright (C) 2013 DroidDriver committers
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.google.android.droiddriver.scroll;
     17 
     18 import android.util.Log;
     19 
     20 import com.google.android.droiddriver.DroidDriver;
     21 import com.google.android.droiddriver.UiElement;
     22 import com.google.android.droiddriver.actions.ScrollDirection;
     23 import com.google.android.droiddriver.exceptions.ElementNotFoundException;
     24 import com.google.android.droiddriver.finders.Finder;
     25 import com.google.android.droiddriver.scroll.Direction.PhysicalToLogicalConverter;
     26 import com.google.android.droiddriver.util.Logs;
     27 import com.google.common.base.Objects;
     28 
     29 /**
     30  * Determines whether scrolling is possible by checking whether the sentinel
     31  * child is updated after scrolling. Use this when
     32  * {@link UiElement#getChildCount()} is not reliable. This can happen, for
     33  * instance, when UiAutomationDriver is used, which skips invisible children, or
     34  * in the case of dynamic list, which shows more items when scrolling beyond the
     35  * end.
     36  */
     37 public class DynamicSentinelStrategy extends AbstractSentinelStrategy {
     38 
     39   /**
     40    * Interface for determining whether sentinel is updated.
     41    */
     42   public static interface IsUpdatedStrategy {
     43     /**
     44      * Returns whether {@code newSentinel} is updated from {@code oldSentinel}.
     45      */
     46     boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel);
     47 
     48     /**
     49      * {@inheritDoc}
     50      *
     51      * <p>
     52      * It is recommended that this method return a description to help
     53      * debugging.
     54      */
     55     @Override
     56     String toString();
     57   }
     58 
     59   /**
     60    * Determines whether the sentinel is updated by checking a single unique
     61    * String attribute of a child element of the sentinel (or itself).
     62    */
     63   public static abstract class SingleStringUpdated implements IsUpdatedStrategy {
     64     private final Finder uniqueStringFinder;
     65 
     66     /**
     67      * @param uniqueStringFinder a Finder relative to the sentinel that finds
     68      *        its child element which contains a unique String.
     69      */
     70     public SingleStringUpdated(Finder uniqueStringFinder) {
     71       this.uniqueStringFinder = uniqueStringFinder;
     72     }
     73 
     74     /**
     75      * @param uniqueStringChild the child of sentinel (or itself) that contains
     76      *        the unique String
     77      * @return the unique String
     78      */
     79     protected abstract String getUniqueString(UiElement uniqueStringChild);
     80 
     81     private String getUniqueStringFromSentinel(UiElement sentinel) {
     82       try {
     83         return getUniqueString(uniqueStringFinder.find(sentinel));
     84       } catch (ElementNotFoundException e) {
     85         return null;
     86       }
     87     }
     88 
     89     @Override
     90     public boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel) {
     91       String newString = getUniqueStringFromSentinel(newSentinel);
     92       // If newString is null, newSentinel must be partially shown. In this case
     93       // we return true to allow further scrolling. But program error could also
     94       // cause this, e.g. a bad choice of GetStrategy. log for debugging.
     95       if (newString == null) {
     96         Logs.logfmt(Log.WARN, "Unique String under sentinel %s is null", newSentinel);
     97         return true;
     98       }
     99       if (newString.equals(getUniqueStringFromSentinel(oldSentinel))) {
    100         Logs.log(Log.INFO, "Unique String is not updated: " + newString);
    101         return false;
    102       }
    103       return true;
    104     }
    105 
    106     @Override
    107     public String toString() {
    108       return Objects.toStringHelper(this).addValue(uniqueStringFinder).toString();
    109     }
    110   }
    111 
    112   /**
    113    * Determines whether the sentinel is updated by checking the text of a child
    114    * element of the sentinel (or itself).
    115    */
    116   public static class TextUpdated extends SingleStringUpdated {
    117     public TextUpdated(Finder uniqueStringFinder) {
    118       super(uniqueStringFinder);
    119     }
    120 
    121     @Override
    122     protected String getUniqueString(UiElement uniqueStringChild) {
    123       return uniqueStringChild.getText();
    124     }
    125   }
    126 
    127   /**
    128    * Determines whether the sentinel is updated by checking the content
    129    * description of a child element of the sentinel (or itself).
    130    */
    131   public static class ContentDescriptionUpdated extends SingleStringUpdated {
    132     public ContentDescriptionUpdated(Finder uniqueStringFinder) {
    133       super(uniqueStringFinder);
    134     }
    135 
    136     @Override
    137     protected String getUniqueString(UiElement uniqueStringChild) {
    138       return uniqueStringChild.getContentDescription();
    139     }
    140   }
    141 
    142   private final IsUpdatedStrategy isUpdatedStrategy;
    143 
    144   /**
    145    * Constructs with {@code GetStrategy}s that decorate the given
    146    * {@code GetStrategy}s with {@link UiElement#VISIBLE}, and the given
    147    * {@code isUpdatedStrategy} and {@code physicalToLogicalConverter}. Be
    148    * careful with {@code GetStrategy}s: the sentinel after each scroll should be
    149    * unique.
    150    */
    151   public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy,
    152       GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy,
    153       PhysicalToLogicalConverter physicalToLogicalConverter) {
    154     super(new MorePredicateGetStrategy(backwardGetStrategy, UiElement.VISIBLE, "VISIBLE_"),
    155         new MorePredicateGetStrategy(forwardGetStrategy, UiElement.VISIBLE, "VISIBLE_"),
    156         physicalToLogicalConverter);
    157     this.isUpdatedStrategy = isUpdatedStrategy;
    158   }
    159 
    160   /**
    161    * Defaults to the standard {@link PhysicalToLogicalConverter}.
    162    */
    163   public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy,
    164       GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy) {
    165     this(isUpdatedStrategy, backwardGetStrategy, forwardGetStrategy,
    166         PhysicalToLogicalConverter.STANDARD_CONVERTER);
    167   }
    168 
    169   /**
    170    * Defaults to LAST_CHILD_GETTER for forward scrolling, and the standard
    171    * {@link PhysicalToLogicalConverter}.
    172    */
    173   public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy,
    174       GetStrategy backwardGetStrategy) {
    175     this(isUpdatedStrategy, backwardGetStrategy, LAST_CHILD_GETTER,
    176         PhysicalToLogicalConverter.STANDARD_CONVERTER);
    177   }
    178 
    179   @Override
    180   public boolean scroll(DroidDriver driver, Finder parentFinder, ScrollDirection direction) {
    181     UiElement parent = driver.on(parentFinder);
    182     UiElement oldSentinel = getSentinel(parent, direction);
    183     parent.scroll(direction);
    184     UiElement newSentinel = getSentinel(driver.on(parentFinder), direction);
    185     return isUpdatedStrategy.isSentinelUpdated(newSentinel, oldSentinel);
    186   }
    187 
    188   @Override
    189   public String toString() {
    190     return String.format("DynamicSentinelStrategy{%s, isUpdatedStrategy=%s}", super.toString(),
    191         isUpdatedStrategy);
    192   }
    193 }
    194