
org.robolectric.android.AccessibilityUtil Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of framework Show documentation
Show all versions of framework Show documentation
An alternative Android testing framework.
The newest version!
package org.robolectric.android;
import android.view.View;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult;
import com.google.android.apps.common.testing.accessibility.framework.DuplicateClickableBoundsViewCheck;
import com.google.android.apps.common.testing.accessibility.framework.TouchTargetSizeViewCheck;
import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.robolectric.annotation.AccessibilityChecks;
import org.robolectric.annotation.AccessibilityChecks.ForRobolectricVersion;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
/**
* Utility class for checking Views for accessibility.
*
* This class is used by {@code ShadowView.checkedPerformClick} to check for accessibility problems.
* There is some subtlety to checking a UI for accessibility when it hasn't been rendered. The
* better initialized the View, the more accurate the checking will be. At a minimum, the view
* should be attached to a proper view hierarchy similar to what's checked for in:q
* {@code ShadowView.checkedPerformClick}.
*/
public class AccessibilityUtil {
private static final String COMPAT_V4_CLASS_NAME = "android.support.v4.view.ViewCompat";
/* The validator that this class configures and uses to run the checks */
private static AccessibilityValidator validator;
/*
* Slightly hacky way to deal with the legacy of allowing the annotation to configure the
* subset of checks to run from the annotation. {@code true} when a version set is
* specified by setRunChecksForRobolectricVersion.
*/
private static boolean forVersionSet = false;
/* Flag indicating if the support library's presence has been verified */
private static boolean v4SupportPresenceVerified = false;
protected AccessibilityUtil() {}
/**
* Check a hierarchy of {@code View}s for accessibility. Only performs checks if (in decreasing
* priority order) accessibility checking is enabled using an {@link AccessibilityChecks}
* annotation, if the system property {@code robolectric.accessibility.enablechecks} is set to
* {@code true}, or if the environment variable {@code robolectric.accessibility.enablechecks}
* is set to {@code true}.
*
* @param view The {@code View} to examine
*
* @return A list of results from the check. If there are no results or checking is disabled,
* the list is empty.
*/
public static List checkViewIfCheckingEnabled(View view) {
AccessibilityChecks classChecksAnnotation = getAnnotation();
if (!isAccessibilityCheckingEnabled(classChecksAnnotation)) {
return Collections.emptyList();
}
return checkView(view);
}
/**
* Check a hierarchy of {@code View}s for accessibility, based on currently set options.
*
* @param view The {@code View} to examine
*
* @return A list of results from the check. If there are no results, the list is empty.
*/
public static List checkView(View view) {
return checkView(view, getAnnotation());
}
/**
* Check a hierarchy of {@code View}s for accessibility. Only performs checks if (in decreasing
* priority order) accessibility checking is enabled using an {@link AccessibilityChecks}
* annotation, if the system property {@code robolectric.accessibility.enablechecks} is set to
* {@code true}, or if the environment variable {@code robolectric.accessibility.enablechecks}
* is set to {@code true}.
*
* Implicitly calls {code setThrowExceptionForErrors(false)} to disable exception throwing. This
* method is deprecated, both because of this side effect and because the other methods offer
* more control over execution.
*
* @param view The {@code View} to examine
*
* @return A list of results from the check. If there are no results or checking is disabled,
* the list is empty.
*/
@Deprecated
public static boolean passesAccessibilityChecksIfEnabled(View view) {
setThrowExceptionForErrors(false);
List results = checkViewIfCheckingEnabled(view);
List errors = AccessibilityCheckResultUtils.getResultsForType(
results, AccessibilityCheckResultType.ERROR);
return (errors.size() == 0);
}
/**
* Specify that a specific subset of accessibility checks be run. The subsets are specified based
* on which Robolectric version particular checks were released with. By default, all checks are
* run {@link ForRobolectricVersion}.
*
* If you call this method, the value you pass will take precedence over any value in any
* annotations.
*
* @param forVersion The version of checks to run for. If {@code null}, throws away the current
* value and falls back on the annotation or default.
*/
public static void setRunChecksForRobolectricVersion(ForRobolectricVersion forVersion) {
initializeValidator();
if (forVersion != null) {
validator.setCheckPreset(convertRoboVersionToA11yTestVersion(forVersion));
forVersionSet = true;
} else {
forVersionSet = false;
}
}
/**
* Specify that accessibility checks should be run for all views in the hierarchy whenever a
* single view's accessibility is asserted.
*
* @param runChecksFromRootView {@code true} if all views in the hierarchy should be checked.
*/
public static void setRunChecksFromRootView(boolean runChecksFromRootView) {
initializeValidator();
validator.setRunChecksFromRootView(runChecksFromRootView);
}
/**
* Suppress all results that match the given matcher. Suppressed results will not be included
* in any logs or cause any {@code Exception} to be thrown. This capability is useful if there
* are known issues, but checks should still look for regressions.
*
* @param matcher A matcher to match a {@link AccessibilityViewCheckResult}. {@code null}
* disables suppression and is the default.
*/
@SuppressWarnings("unchecked") // The generic passed to anyOf
public static void setSuppressingResultMatcher(
final Matcher super AccessibilityViewCheckResult> matcher) {
initializeValidator();
/* Suppress all touch target results, since views all report size as 0x0 */
Matcher touchTargetResultMatcher =
AccessibilityCheckResultUtils.matchesChecks(
Matchers.equalTo(TouchTargetSizeViewCheck.class));
Matcher duplicateBoundsResultMatcher =
AccessibilityCheckResultUtils.matchesChecks(
Matchers.equalTo(DuplicateClickableBoundsViewCheck.class));
if (matcher == null) {
validator.setSuppressingResultMatcher(
Matchers.anyOf(touchTargetResultMatcher, duplicateBoundsResultMatcher));
} else {
validator.setSuppressingResultMatcher(
Matchers.anyOf(matcher, touchTargetResultMatcher, duplicateBoundsResultMatcher));
}
}
/**
* Control whether or not to throw exceptions when accessibility errors are found.
*
* @param throwExceptionForErrors {@code true} to throw an {@code AccessibilityViewCheckException}
* when there is at least one error result. Default: {@code true}.
*/
public static void setThrowExceptionForErrors(boolean throwExceptionForErrors) {
initializeValidator();
validator.setThrowExceptionForErrors(throwExceptionForErrors);
}
private static List checkView(View view,
AccessibilityChecks classChecksAnnotation) {
/*
* Accessibility Checking requires the v4 support library. If the support library isn't present,
* throw a descriptive exception now.
*/
if (!v4SupportPresenceVerified) {
try {
View.class.getClassLoader().loadClass(COMPAT_V4_CLASS_NAME);
} catch (ClassNotFoundException e) {
throw new RuntimeException(
"Accessibility Checking requires the Android support library (v4).\n"
+ "Either include it in the project or disable accessibility checking.");
}
v4SupportPresenceVerified = true;
}
initializeValidator();
if (!forVersionSet) {
if (classChecksAnnotation != null) {
validator.setCheckPreset(
convertRoboVersionToA11yTestVersion(classChecksAnnotation.forRobolectricVersion()));
} else {
validator.setCheckPreset(AccessibilityCheckPreset.LATEST);
}
}
return validator.checkAndReturnResults(view);
}
private static boolean isAccessibilityCheckingEnabled(AccessibilityChecks classChecksAnnotation) {
boolean checksEnabled = false;
String checksEnabledString = System.getenv("robolectric.accessibility.enablechecks");
if (checksEnabledString != null) {
checksEnabled = checksEnabledString.equals("true");
}
/* Allow test arg to enable checking (and override environment variables) */
checksEnabledString = System.getProperty("robolectric.accessibility.enablechecks");
if (checksEnabledString != null) {
checksEnabled = checksEnabledString.equals("true");
}
if (classChecksAnnotation != null) {
checksEnabled = classChecksAnnotation.enabled();
}
return checksEnabled;
}
private static AccessibilityChecks getAnnotation() {
AccessibilityChecks classChecksAnnotation = null;
StackTraceElement[] stack = new Throwable().fillInStackTrace().getStackTrace();
for (StackTraceElement element : stack) {
/* Look for annotations on the method or the class */
Class> clazz;
try {
clazz = Class.forName(element.getClassName());
Method method;
method = clazz.getMethod(element.getMethodName());
/* Assume the method is void, as that is the case for tests */
classChecksAnnotation = method.getAnnotation(AccessibilityChecks.class);
if (classChecksAnnotation == null) {
classChecksAnnotation = clazz.getAnnotation(AccessibilityChecks.class);
}
/* Stop looking when we find an annotation */
if (classChecksAnnotation != null) {
break;
}
/* If we've crawled up the stack far enough to find the test, stop looking */
for (Annotation annotation : clazz.getAnnotations()) {
if (annotation.annotationType().getName().equals("org.junit.Test")) {
break;
}
}
}
/*
* The reflective calls may throw exceptions if the stack trace elements
* don't look like junit test methods. In that case we simply go on
* to the next element
*/
catch (ClassNotFoundException | SecurityException | NoSuchMethodException e) {}
}
return classChecksAnnotation;
}
private static void initializeValidator() {
if (validator == null) {
validator = new AccessibilityValidator();
setSuppressingResultMatcher(null);
}
}
private static AccessibilityCheckPreset convertRoboVersionToA11yTestVersion(
ForRobolectricVersion robolectricVersion) {
if (robolectricVersion == ForRobolectricVersion.LATEST) {
return AccessibilityCheckPreset.LATEST;
}
AccessibilityCheckPreset preset = AccessibilityCheckPreset.VERSION_1_0_CHECKS;
if (robolectricVersion.ordinal() >= ForRobolectricVersion.VERSION_3_1.ordinal()) {
preset = AccessibilityCheckPreset.VERSION_2_0_CHECKS;
}
return preset;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy