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