Home | History | Annotate | Download | only in android
      1 package org.robolectric.android;
      2 
      3 import android.view.View;
      4 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset;
      5 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult;
      6 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
      7 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils;
      8 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult;
      9 import com.google.android.apps.common.testing.accessibility.framework.DuplicateClickableBoundsViewCheck;
     10 import com.google.android.apps.common.testing.accessibility.framework.TouchTargetSizeViewCheck;
     11 import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator;
     12 import java.lang.annotation.Annotation;
     13 import java.lang.reflect.Method;
     14 import java.util.Collections;
     15 import java.util.List;
     16 import org.hamcrest.Matcher;
     17 import org.hamcrest.Matchers;
     18 import org.robolectric.annotation.AccessibilityChecks;
     19 import org.robolectric.annotation.AccessibilityChecks.ForRobolectricVersion;
     20 
     21 /**
     22  * Utility class for checking Views for accessibility.
     23  *
     24  * This class is used by {@code ShadowView.checkedPerformClick} to check for accessibility problems.
     25  * There is some subtlety to checking a UI for accessibility when it hasn't been rendered. The
     26  * better initialized the View, the more accurate the checking will be. At a minimum, the view
     27  * should be attached to a proper view hierarchy similar to what's checked for in:q
     28  * {@code ShadowView.checkedPerformClick}.
     29  */
     30 public class AccessibilityUtil {
     31   private static final String COMPAT_V4_CLASS_NAME = "android.support.v4.view.ViewCompat";
     32   /* The validator that this class configures and uses to run the checks */
     33   private static AccessibilityValidator validator;
     34 
     35   /*
     36    * Slightly hacky way to deal with the legacy of allowing the annotation to configure the
     37    * subset of checks to run from the annotation. {@code true} when a version set is
     38    * specified by setRunChecksForRobolectricVersion.
     39    */
     40   private static boolean forVersionSet = false;
     41 
     42   /* Flag indicating if the support library's presence has been verified */
     43   private static boolean v4SupportPresenceVerified = false;
     44 
     45   protected AccessibilityUtil() {}
     46 
     47   /**
     48    * Check a hierarchy of {@code View}s for accessibility. Only performs checks if (in decreasing
     49    * priority order) accessibility checking is enabled using an {@link AccessibilityChecks}
     50    * annotation, if the system property {@code robolectric.accessibility.enablechecks} is set to
     51    * {@code true}, or if the environment variable {@code robolectric.accessibility.enablechecks}
     52    * is set to {@code true}.
     53    *
     54    * @param view The {@code View} to examine
     55    *
     56    * @return A list of results from the check. If there are no results or checking is disabled,
     57    * the list is empty.
     58    */
     59   public static List<AccessibilityViewCheckResult> checkViewIfCheckingEnabled(View view) {
     60     AccessibilityChecks classChecksAnnotation = getAnnotation();
     61     if (!isAccessibilityCheckingEnabled(classChecksAnnotation)) {
     62       return Collections.emptyList();
     63     }
     64 
     65     return checkView(view);
     66   }
     67 
     68   /**
     69    * Check a hierarchy of {@code View}s for accessibility, based on currently set options.
     70    *
     71    * @param view The {@code View} to examine
     72    *
     73    * @return A list of results from the check. If there are no results, the list is empty.
     74    */
     75   public static List<AccessibilityViewCheckResult> checkView(View view) {
     76     return checkView(view, getAnnotation());
     77   }
     78 
     79   /**
     80    * Check a hierarchy of {@code View}s for accessibility. Only performs checks if (in decreasing
     81    * priority order) accessibility checking is enabled using an {@link AccessibilityChecks}
     82    * annotation, if the system property {@code robolectric.accessibility.enablechecks} is set to
     83    * {@code true}, or if the environment variable {@code robolectric.accessibility.enablechecks}
     84    * is set to {@code true}.
     85    *
     86    * Implicitly calls {code setThrowExceptionForErrors(false)} to disable exception throwing. This
     87    * method is deprecated, both because of this side effect and because the other methods offer
     88    * more control over execution.
     89    *
     90    * @param view The {@code View} to examine
     91    *
     92    * @return A list of results from the check. If there are no results or checking is disabled,
     93    * the list is empty.
     94    */
     95   @Deprecated
     96   public static boolean passesAccessibilityChecksIfEnabled(View view) {
     97     setThrowExceptionForErrors(false);
     98     List<AccessibilityViewCheckResult> results = checkViewIfCheckingEnabled(view);
     99     List<AccessibilityViewCheckResult> errors = AccessibilityCheckResultUtils.getResultsForType(
    100         results, AccessibilityCheckResultType.ERROR);
    101     return (errors.size() == 0);
    102   }
    103 
    104   /**
    105    * Specify that a specific subset of accessibility checks be run. The subsets are specified based
    106    * on which Robolectric version particular checks were released with. By default, all checks are
    107    * run {@link ForRobolectricVersion}.
    108    *
    109    * If you call this method, the value you pass will take precedence over any value in any
    110    * annotations.
    111    *
    112    * @param forVersion The version of checks to run for. If {@code null}, throws away the current
    113    * value and falls back on the annotation or default.
    114    */
    115   public static void setRunChecksForRobolectricVersion(ForRobolectricVersion forVersion) {
    116     initializeValidator();
    117     if (forVersion != null) {
    118       validator.setCheckPreset(convertRoboVersionToA11yTestVersion(forVersion));
    119       forVersionSet = true;
    120     } else {
    121       forVersionSet = false;
    122     }
    123   }
    124 
    125   /**
    126    * Specify that accessibility checks should be run for all views in the hierarchy whenever a
    127    * single view's accessibility is asserted.
    128    *
    129    * @param runChecksFromRootView {@code true} if all views in the hierarchy should be checked.
    130    */
    131   public static void setRunChecksFromRootView(boolean runChecksFromRootView) {
    132     initializeValidator();
    133     validator.setRunChecksFromRootView(runChecksFromRootView);
    134   }
    135 
    136   /**
    137    * Suppress all results that match the given matcher. Suppressed results will not be included
    138    * in any logs or cause any {@code Exception} to be thrown. This capability is useful if there
    139    * are known issues, but checks should still look for regressions.
    140    *
    141    * @param matcher A matcher to match a {@link AccessibilityViewCheckResult}. {@code null}
    142    * disables suppression and is the default.
    143    */
    144   @SuppressWarnings("unchecked") // The generic passed to anyOf
    145   public static void setSuppressingResultMatcher(
    146       final Matcher<? super AccessibilityViewCheckResult> matcher) {
    147     initializeValidator();
    148     /* Suppress all touch target results, since views all report size as 0x0 */
    149     Matcher<AccessibilityCheckResult> touchTargetResultMatcher =
    150         AccessibilityCheckResultUtils.matchesChecks(
    151             Matchers.equalTo(TouchTargetSizeViewCheck.class));
    152     Matcher<AccessibilityCheckResult> duplicateBoundsResultMatcher =
    153         AccessibilityCheckResultUtils.matchesChecks(
    154             Matchers.equalTo(DuplicateClickableBoundsViewCheck.class));
    155     if (matcher == null) {
    156       validator.setSuppressingResultMatcher(
    157           Matchers.anyOf(touchTargetResultMatcher, duplicateBoundsResultMatcher));
    158     } else {
    159       validator.setSuppressingResultMatcher(
    160           Matchers.anyOf(matcher, touchTargetResultMatcher, duplicateBoundsResultMatcher));
    161     }
    162   }
    163 
    164   /**
    165    * Control whether or not to throw exceptions when accessibility errors are found.
    166    *
    167    * @param throwExceptionForErrors {@code true} to throw an {@code AccessibilityViewCheckException}
    168    * when there is at least one error result. Default: {@code true}.
    169    */
    170   public static void setThrowExceptionForErrors(boolean throwExceptionForErrors) {
    171     initializeValidator();
    172     validator.setThrowExceptionForErrors(throwExceptionForErrors);
    173   }
    174 
    175   private static List<AccessibilityViewCheckResult> checkView(View view,
    176       AccessibilityChecks classChecksAnnotation) {
    177     /*
    178      * Accessibility Checking requires the v4 support library. If the support library isn't present,
    179      * throw a descriptive exception now.
    180      */
    181     if (!v4SupportPresenceVerified) {
    182       try {
    183         View.class.getClassLoader().loadClass(COMPAT_V4_CLASS_NAME);
    184       } catch (ClassNotFoundException e) {
    185         throw new RuntimeException(
    186             "Accessibility Checking requires the Android support library (v4).\n"
    187             + "Either include it in the project or disable accessibility checking.");
    188       }
    189       v4SupportPresenceVerified = true;
    190     }
    191 
    192     initializeValidator();
    193     if (!forVersionSet) {
    194       if (classChecksAnnotation != null) {
    195         validator.setCheckPreset(
    196             convertRoboVersionToA11yTestVersion(classChecksAnnotation.forRobolectricVersion()));
    197       } else {
    198         validator.setCheckPreset(AccessibilityCheckPreset.LATEST);
    199       }
    200     }
    201     return validator.checkAndReturnResults(view);
    202   }
    203 
    204   private static boolean isAccessibilityCheckingEnabled(AccessibilityChecks classChecksAnnotation) {
    205     boolean checksEnabled = false;
    206 
    207     String checksEnabledString = System.getenv("robolectric.accessibility.enablechecks");
    208     if (checksEnabledString != null) {
    209       checksEnabled = checksEnabledString.equals("true");
    210     }
    211 
    212     /* Allow test arg to enable checking (and override environment variables) */
    213     checksEnabledString = System.getProperty("robolectric.accessibility.enablechecks");
    214     if (checksEnabledString != null) {
    215       checksEnabled = checksEnabledString.equals("true");
    216     }
    217 
    218     if (classChecksAnnotation != null) {
    219       checksEnabled = classChecksAnnotation.enabled();
    220     }
    221 
    222     return checksEnabled;
    223   }
    224 
    225   private static AccessibilityChecks getAnnotation() {
    226     AccessibilityChecks classChecksAnnotation = null;
    227     StackTraceElement[] stack = new Throwable().fillInStackTrace().getStackTrace();
    228     for (StackTraceElement element : stack) {
    229       /* Look for annotations on the method or the class */
    230       Class<?> clazz;
    231       try {
    232         clazz = Class.forName(element.getClassName());
    233         Method method;
    234         method = clazz.getMethod(element.getMethodName());
    235         /* Assume the method is void, as that is the case for tests */
    236         classChecksAnnotation = method.getAnnotation(AccessibilityChecks.class);
    237         if (classChecksAnnotation == null) {
    238           classChecksAnnotation = clazz.getAnnotation(AccessibilityChecks.class);
    239         }
    240         /* Stop looking when we find an annotation */
    241         if (classChecksAnnotation != null) {
    242           break;
    243         }
    244 
    245         /* If we've crawled up the stack far enough to find the test, stop looking */
    246         for (Annotation annotation : clazz.getAnnotations()) {
    247           if (annotation.annotationType().getName().equals("org.junit.Test")) {
    248             break;
    249           }
    250         }
    251       }
    252       /*
    253        * The reflective calls may throw exceptions if the stack trace elements
    254        * don't look like junit test methods. In that case we simply go on
    255        * to the next element
    256        */
    257       catch (ClassNotFoundException | SecurityException | NoSuchMethodException e) {}
    258     }
    259     return classChecksAnnotation;
    260   }
    261 
    262   private static void initializeValidator() {
    263     if (validator == null) {
    264       validator = new AccessibilityValidator();
    265       setSuppressingResultMatcher(null);
    266     }
    267   }
    268 
    269   private static AccessibilityCheckPreset convertRoboVersionToA11yTestVersion(
    270       ForRobolectricVersion robolectricVersion) {
    271     if (robolectricVersion == ForRobolectricVersion.LATEST) {
    272       return AccessibilityCheckPreset.LATEST;
    273     }
    274     AccessibilityCheckPreset preset = AccessibilityCheckPreset.VERSION_1_0_CHECKS;
    275     if (robolectricVersion.ordinal() >= ForRobolectricVersion.VERSION_3_1.ordinal()) {
    276       preset = AccessibilityCheckPreset.VERSION_2_0_CHECKS;
    277     }
    278     return preset;
    279   }
    280 }
    281