• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
AccessibilityUtil()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    */
checkViewIfCheckingEnabled(View view)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    */
checkView(View view)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
passesAccessibilityChecksIfEnabled(View view)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    */
setRunChecksForRobolectricVersion(ForRobolectricVersion forVersion)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    */
setRunChecksFromRootView(boolean runChecksFromRootView)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
setSuppressingResultMatcher( final Matcher<? super AccessibilityViewCheckResult> matcher)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    */
setThrowExceptionForErrors(boolean throwExceptionForErrors)170   public static void setThrowExceptionForErrors(boolean throwExceptionForErrors) {
171     initializeValidator();
172     validator.setThrowExceptionForErrors(throwExceptionForErrors);
173   }
174 
checkView(View view, AccessibilityChecks classChecksAnnotation)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 
isAccessibilityCheckingEnabled(AccessibilityChecks classChecksAnnotation)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 
getAnnotation()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 
initializeValidator()262   private static void initializeValidator() {
263     if (validator == null) {
264       validator = new AccessibilityValidator();
265       setSuppressingResultMatcher(null);
266     }
267   }
268 
convertRoboVersionToA11yTestVersion( ForRobolectricVersion robolectricVersion)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