blob: 16ad4a2061a180b7e4ad17e2523c4428eb284bf7 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.validator.accessibility;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.SessionParams;
import com.android.layoutlib.bridge.intensive.RenderTestBase;
import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator;
import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback;
import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser;
import com.android.layoutlib.bridge.intensive.util.SessionParamsBuilder;
import com.android.tools.idea.validator.LayoutValidator;
import com.android.tools.idea.validator.ValidatorData;
import com.android.tools.idea.validator.ValidatorData.Issue;
import com.android.tools.idea.validator.ValidatorData.Level;
import com.android.tools.idea.validator.ValidatorData.Policy;
import com.android.tools.idea.validator.ValidatorData.Type;
import com.android.tools.idea.validator.ValidatorResult;
import org.junit.Test;
import java.util.EnumSet;
import java.util.List;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* Sanity check for a11y checks. For now it lacks checking the following:
* - ClassNameCheck
* - ClickableSpanCheck
* - EditableContentDescCheck
* - LinkPurposeUnclearCheck
* As these require more complex UI for testing.
*
* It's also missing:
* - TraversalOrderCheck
* Because in Layoutlib test env, traversalBefore/after attributes seems to be lost. Tested on
* studio and it seems to work ok.
*/
public class AccessibilityValidatorTests extends RenderTestBase {
@Test
public void testDuplicateClickableBoundsCheck() throws Exception {
render("a11y_test_dup_clickable_bounds.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> dupBounds = filter(result.getIssues(), "DuplicateClickableBoundsCheck");
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedErrors = 1;
expectedLevels.check(dupBounds);
});
}
@Test
public void testDuplicateSpeakableTextsCheck() throws Exception {
render("a11y_test_duplicate_speakable.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> duplicateSpeakableTexts = filter(result.getIssues(),
"DuplicateSpeakableTextCheck");
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedInfos = 1;
expectedLevels.expectedWarnings = 1;
expectedLevels.check(duplicateSpeakableTexts);
});
}
@Test
public void testRedundantDescriptionCheck() throws Exception {
render("a11y_test_redundant_desc.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> redundant = filter(result.getIssues(), "RedundantDescriptionCheck");
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedVerboses = 3;
expectedLevels.expectedWarnings = 1;
expectedLevels.check(redundant);
});
}
@Test
public void testLabelFor() throws Exception {
render("a11y_test_speakable_text_present.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> speakableCheck = filter(result.getIssues(), "SpeakableTextPresentCheck");
// Post-JB MR2 support labelFor, so SpeakableTextPresentCheck does not need to find any
// speakable text. Expected 1 verbose result saying something along the line of
// didn't run or not important for a11y.
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedVerboses = 1;
expectedLevels.check(speakableCheck);
});
}
@Test
public void testImportantForAccessibility() throws Exception {
render("a11y_test_speakable_text_present2.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> speakableCheck = filter(result.getIssues(), "SpeakableTextPresentCheck");
// Post-JB MR2 support importantForAccessibility, so SpeakableTextPresentCheck
// does not need to find any speakable text. Expected 2 verbose results.
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedVerboses = 2;
expectedLevels.check(speakableCheck);
});
}
@Test
public void testSpeakableTextPresentCheck() throws Exception {
render("a11y_test_speakable_text_present3.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> speakableCheck = filter(result.getIssues(), "SpeakableTextPresentCheck");
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedVerboses = 1;
expectedLevels.expectedErrors = 1;
expectedLevels.check(speakableCheck);
// Make sure no other errors in the system.
speakableCheck = filter(speakableCheck, EnumSet.of(Level.ERROR));
assertEquals(1, speakableCheck.size());
List<Issue> allErrors = filter(
result.getIssues(), EnumSet.of(Level.ERROR, Level.WARNING, Level.INFO));
checkEquals(speakableCheck, allErrors);
});
}
@Test
public void testTextContrastCheck() throws Exception {
render("a11y_test_text_contrast.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> textContrast = filter(result.getIssues(), "TextContrastCheck");
// ATF doesn't count alpha values unless image is passed.
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedErrors = 3;
expectedLevels.expectedWarnings = 1; // This is true only if image is passed.
expectedLevels.expectedVerboses = 2;
expectedLevels.check(textContrast);
// Make sure no other errors in the system.
textContrast = filter(textContrast, EnumSet.of(Level.ERROR));
List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR));
checkEquals(filtered, textContrast);
});
}
@Test
public void testTextContrastCheckNoImage() throws Exception {
render("a11y_test_text_contrast.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> textContrast = filter(result.getIssues(), "TextContrastCheck");
// ATF doesn't count alpha values unless image is passed.
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedErrors = 3;
expectedLevels.expectedVerboses = 3;
expectedLevels.check(textContrast);
// Make sure no other errors in the system.
textContrast = filter(textContrast, EnumSet.of(Level.ERROR));
List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR));
checkEquals(filtered, textContrast);
}, false);
}
@Test
public void testImageContrastCheck() throws Exception {
render("a11y_test_image_contrast.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> imageContrast = filter(result.getIssues(), "ImageContrastCheck");
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedWarnings = 1;
expectedLevels.expectedVerboses = 1;
expectedLevels.check(imageContrast);
// Make sure no other errors in the system.
imageContrast = filter(imageContrast, EnumSet.of(Level.ERROR, Level.WARNING));
List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR, Level.WARNING));
checkEquals(filtered, imageContrast);
});
}
@Test
public void testImageContrastCheckNoImage() throws Exception {
render("a11y_test_image_contrast.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> imageContrast = filter(result.getIssues(), "ImageContrastCheck");
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedVerboses = 3;
expectedLevels.check(imageContrast);
// Make sure no other errors in the system.
imageContrast = filter(imageContrast, EnumSet.of(Level.ERROR, Level.WARNING));
List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR, Level.WARNING));
checkEquals(filtered, imageContrast);
}, false);
}
@Test
public void testTouchTargetSizeCheck() throws Exception {
render("a11y_test_touch_target_size.xml", session -> {
ValidatorResult result = getRenderResult(session);
List<Issue> targetSizes = filter(result.getIssues(), "TouchTargetSizeCheck");
ExpectedLevels expectedLevels = new ExpectedLevels();
expectedLevels.expectedErrors = 5;
expectedLevels.expectedVerboses = 1;
expectedLevels.check(targetSizes);
// Make sure no other errors in the system.
targetSizes = filter(targetSizes, EnumSet.of(Level.ERROR));
List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR));
checkEquals(filtered, targetSizes);
});
}
private void checkEquals(List<Issue> list1, List<Issue> list2) {
assertEquals(list1.size(), list2.size());
for (int i = 0; i < list1.size(); i++) {
assertEquals(list1.get(i), list2.get(i));
}
}
private List<Issue> filter(List<ValidatorData.Issue> results, EnumSet<Level> errors) {
return results.stream().filter(
issue -> errors.contains(issue.mLevel)).collect(Collectors.toList());
}
private List<Issue> filter(
List<ValidatorData.Issue> results, String sourceClass) {
return results.stream().filter(
issue -> sourceClass.equals(issue.mSourceClass)).collect(Collectors.toList());
}
private ValidatorResult getRenderResult(RenderSession session) {
Object validationData = session.getValidationData();
assertNotNull(validationData);
assertTrue(validationData instanceof ValidatorResult);
return (ValidatorResult) validationData;
}
private void render(String fileName, RenderSessionListener verifier) throws Exception {
render(fileName, verifier, true);
}
private void render(
String fileName,
RenderSessionListener verifier,
boolean enableImageCheck) throws Exception {
LayoutValidator.updatePolicy(new Policy(
EnumSet.of(Type.ACCESSIBILITY, Type.RENDER),
EnumSet.of(Level.ERROR, Level.WARNING, Level.INFO, Level.VERBOSE)));
LayoutPullParser parser = createParserFromPath(fileName);
LayoutLibTestCallback layoutLibCallback =
new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
layoutLibCallback.initResources();
SessionParamsBuilder params = getSessionParamsBuilder()
.setParser(parser)
.setConfigGenerator(ConfigGenerator.NEXUS_5)
.setCallback(layoutLibCallback)
.disableDecoration()
.enableLayoutValidation();
if (enableImageCheck) {
params.enableLayoutValidationImageCheck();
}
render(sBridge, params.build(), -1, verifier);
}
/**
* Helper class that checks the list of issues..
*/
private static class ExpectedLevels {
// Number of errors expected
public int expectedErrors = 0;
// Number of warnings expected
public int expectedWarnings = 0;
// Number of infos expected
public int expectedInfos = 0;
// Number of verboses expected
public int expectedVerboses = 0;
public void check(List<Issue> issues) {
int errors = 0;
int warnings = 0;
int infos = 0;
int verboses = 0;
for (Issue issue : issues) {
switch (issue.mLevel) {
case ERROR:
errors++;
break;
case WARNING:
warnings++;
break;
case INFO:
infos++;
break;
case VERBOSE:
verboses++;
break;
}
}
assertEquals("Number of expected errors", expectedErrors, errors);
assertEquals("Number of expected warnings",expectedWarnings, warnings);
assertEquals("Number of expected infos", expectedInfos, infos);
assertEquals("Number of expected verboses", expectedVerboses, verboses);
int size = expectedErrors + expectedWarnings + expectedInfos + expectedVerboses;
assertEquals("expected size", size, issues.size());
}
};
}