1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tools.idea.validator.accessibility; 18 19 import com.android.ide.common.rendering.api.RenderSession; 20 import com.android.ide.common.rendering.api.SessionParams; 21 import com.android.layoutlib.bridge.intensive.RenderTestBase; 22 import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator; 23 import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback; 24 import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser; 25 import com.android.layoutlib.bridge.intensive.util.SessionParamsBuilder; 26 import com.android.tools.idea.validator.LayoutValidator; 27 import com.android.tools.idea.validator.ValidatorData; 28 import com.android.tools.idea.validator.ValidatorData.Issue; 29 import com.android.tools.idea.validator.ValidatorData.Level; 30 import com.android.tools.idea.validator.ValidatorData.Policy; 31 import com.android.tools.idea.validator.ValidatorData.Type; 32 import com.android.tools.idea.validator.ValidatorResult; 33 34 import org.junit.Test; 35 36 import java.util.EnumSet; 37 import java.util.List; 38 import java.util.stream.Collectors; 39 40 import static org.junit.Assert.assertEquals; 41 import static org.junit.Assert.assertNotNull; 42 import static org.junit.Assert.assertTrue; 43 44 /** 45 * Sanity check for a11y checks. For now it lacks checking the following: 46 * - ClassNameCheck 47 * - ClickableSpanCheck 48 * - EditableContentDescCheck 49 * - LinkPurposeUnclearCheck 50 * As these require more complex UI for testing. 51 * 52 * It's also missing: 53 * - TraversalOrderCheck 54 * Because in Layoutlib test env, traversalBefore/after attributes seems to be lost. Tested on 55 * studio and it seems to work ok. 56 */ 57 public class AccessibilityValidatorTests extends RenderTestBase { 58 59 @Test testDuplicateClickableBoundsCheck()60 public void testDuplicateClickableBoundsCheck() throws Exception { 61 render("a11y_test_dup_clickable_bounds.xml", session -> { 62 ValidatorResult result = getRenderResult(session); 63 List<Issue> dupBounds = filter(result.getIssues(), "DuplicateClickableBoundsCheck"); 64 65 ExpectedLevels expectedLevels = new ExpectedLevels(); 66 expectedLevels.expectedErrors = 1; 67 expectedLevels.check(dupBounds); 68 }); 69 } 70 71 @Test testDuplicateSpeakableTextsCheck()72 public void testDuplicateSpeakableTextsCheck() throws Exception { 73 render("a11y_test_duplicate_speakable.xml", session -> { 74 ValidatorResult result = getRenderResult(session); 75 List<Issue> duplicateSpeakableTexts = filter(result.getIssues(), 76 "DuplicateSpeakableTextCheck"); 77 78 ExpectedLevels expectedLevels = new ExpectedLevels(); 79 expectedLevels.expectedInfos = 1; 80 expectedLevels.expectedWarnings = 1; 81 expectedLevels.check(duplicateSpeakableTexts); 82 }); 83 } 84 85 @Test testRedundantDescriptionCheck()86 public void testRedundantDescriptionCheck() throws Exception { 87 render("a11y_test_redundant_desc.xml", session -> { 88 ValidatorResult result = getRenderResult(session); 89 List<Issue> redundant = filter(result.getIssues(), "RedundantDescriptionCheck"); 90 91 ExpectedLevels expectedLevels = new ExpectedLevels(); 92 expectedLevels.expectedVerboses = 3; 93 expectedLevels.expectedWarnings = 1; 94 expectedLevels.check(redundant); 95 }); 96 } 97 98 @Test testLabelFor()99 public void testLabelFor() throws Exception { 100 render("a11y_test_speakable_text_present.xml", session -> { 101 ValidatorResult result = getRenderResult(session); 102 List<Issue> speakableCheck = filter(result.getIssues(), "SpeakableTextPresentCheck"); 103 104 // Post-JB MR2 support labelFor, so SpeakableTextPresentCheck does not need to find any 105 // speakable text. Expected 1 verbose result saying something along the line of 106 // didn't run or not important for a11y. 107 ExpectedLevels expectedLevels = new ExpectedLevels(); 108 expectedLevels.expectedVerboses = 1; 109 expectedLevels.check(speakableCheck); 110 }); 111 } 112 113 @Test testImportantForAccessibility()114 public void testImportantForAccessibility() throws Exception { 115 render("a11y_test_speakable_text_present2.xml", session -> { 116 ValidatorResult result = getRenderResult(session); 117 List<Issue> speakableCheck = filter(result.getIssues(), "SpeakableTextPresentCheck"); 118 119 // Post-JB MR2 support importantForAccessibility, so SpeakableTextPresentCheck 120 // does not need to find any speakable text. Expected 2 verbose results. 121 ExpectedLevels expectedLevels = new ExpectedLevels(); 122 expectedLevels.expectedVerboses = 2; 123 expectedLevels.check(speakableCheck); 124 }); 125 } 126 127 @Test testSpeakableTextPresentCheck()128 public void testSpeakableTextPresentCheck() throws Exception { 129 render("a11y_test_speakable_text_present3.xml", session -> { 130 ValidatorResult result = getRenderResult(session); 131 List<Issue> speakableCheck = filter(result.getIssues(), "SpeakableTextPresentCheck"); 132 133 ExpectedLevels expectedLevels = new ExpectedLevels(); 134 expectedLevels.expectedVerboses = 1; 135 expectedLevels.expectedErrors = 1; 136 expectedLevels.check(speakableCheck); 137 138 // Make sure no other errors in the system. 139 speakableCheck = filter(speakableCheck, EnumSet.of(Level.ERROR)); 140 assertEquals(1, speakableCheck.size()); 141 List<Issue> allErrors = filter( 142 result.getIssues(), EnumSet.of(Level.ERROR, Level.WARNING, Level.INFO)); 143 checkEquals(speakableCheck, allErrors); 144 }); 145 } 146 147 @Test testTextContrastCheck()148 public void testTextContrastCheck() throws Exception { 149 render("a11y_test_text_contrast.xml", session -> { 150 ValidatorResult result = getRenderResult(session); 151 List<Issue> textContrast = filter(result.getIssues(), "TextContrastCheck"); 152 153 // ATF doesn't count alpha values unless image is passed. 154 ExpectedLevels expectedLevels = new ExpectedLevels(); 155 expectedLevels.expectedErrors = 3; 156 expectedLevels.expectedWarnings = 1; // This is true only if image is passed. 157 expectedLevels.expectedVerboses = 2; 158 expectedLevels.check(textContrast); 159 160 // Make sure no other errors in the system. 161 textContrast = filter(textContrast, EnumSet.of(Level.ERROR)); 162 List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR)); 163 checkEquals(filtered, textContrast); 164 }); 165 } 166 167 @Test testTextContrastCheckNoImage()168 public void testTextContrastCheckNoImage() throws Exception { 169 render("a11y_test_text_contrast.xml", session -> { 170 ValidatorResult result = getRenderResult(session); 171 List<Issue> textContrast = filter(result.getIssues(), "TextContrastCheck"); 172 173 // ATF doesn't count alpha values unless image is passed. 174 ExpectedLevels expectedLevels = new ExpectedLevels(); 175 expectedLevels.expectedErrors = 3; 176 expectedLevels.expectedVerboses = 3; 177 expectedLevels.check(textContrast); 178 179 // Make sure no other errors in the system. 180 textContrast = filter(textContrast, EnumSet.of(Level.ERROR)); 181 List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR)); 182 checkEquals(filtered, textContrast); 183 }, false); 184 } 185 186 @Test testImageContrastCheck()187 public void testImageContrastCheck() throws Exception { 188 render("a11y_test_image_contrast.xml", session -> { 189 ValidatorResult result = getRenderResult(session); 190 List<Issue> imageContrast = filter(result.getIssues(), "ImageContrastCheck"); 191 192 ExpectedLevels expectedLevels = new ExpectedLevels(); 193 expectedLevels.expectedWarnings = 1; 194 expectedLevels.expectedVerboses = 1; 195 expectedLevels.check(imageContrast); 196 197 // Make sure no other errors in the system. 198 imageContrast = filter(imageContrast, EnumSet.of(Level.ERROR, Level.WARNING)); 199 List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR, Level.WARNING)); 200 checkEquals(filtered, imageContrast); 201 }); 202 } 203 204 @Test testImageContrastCheckNoImage()205 public void testImageContrastCheckNoImage() throws Exception { 206 render("a11y_test_image_contrast.xml", session -> { 207 ValidatorResult result = getRenderResult(session); 208 List<Issue> imageContrast = filter(result.getIssues(), "ImageContrastCheck"); 209 210 ExpectedLevels expectedLevels = new ExpectedLevels(); 211 expectedLevels.expectedVerboses = 3; 212 expectedLevels.check(imageContrast); 213 214 // Make sure no other errors in the system. 215 imageContrast = filter(imageContrast, EnumSet.of(Level.ERROR, Level.WARNING)); 216 List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR, Level.WARNING)); 217 checkEquals(filtered, imageContrast); 218 }, false); 219 } 220 221 @Test testTouchTargetSizeCheck()222 public void testTouchTargetSizeCheck() throws Exception { 223 render("a11y_test_touch_target_size.xml", session -> { 224 ValidatorResult result = getRenderResult(session); 225 List<Issue> targetSizes = filter(result.getIssues(), "TouchTargetSizeCheck"); 226 227 ExpectedLevels expectedLevels = new ExpectedLevels(); 228 expectedLevels.expectedErrors = 5; 229 expectedLevels.expectedVerboses = 1; 230 expectedLevels.check(targetSizes); 231 232 // Make sure no other errors in the system. 233 targetSizes = filter(targetSizes, EnumSet.of(Level.ERROR)); 234 List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR)); 235 checkEquals(filtered, targetSizes); 236 }); 237 } 238 checkEquals(List<Issue> list1, List<Issue> list2)239 private void checkEquals(List<Issue> list1, List<Issue> list2) { 240 assertEquals(list1.size(), list2.size()); 241 for (int i = 0; i < list1.size(); i++) { 242 assertEquals(list1.get(i), list2.get(i)); 243 } 244 } 245 filter(List<ValidatorData.Issue> results, EnumSet<Level> errors)246 private List<Issue> filter(List<ValidatorData.Issue> results, EnumSet<Level> errors) { 247 return results.stream().filter( 248 issue -> errors.contains(issue.mLevel)).collect(Collectors.toList()); 249 } 250 filter( List<ValidatorData.Issue> results, String sourceClass)251 private List<Issue> filter( 252 List<ValidatorData.Issue> results, String sourceClass) { 253 return results.stream().filter( 254 issue -> sourceClass.equals(issue.mSourceClass)).collect(Collectors.toList()); 255 } 256 getRenderResult(RenderSession session)257 private ValidatorResult getRenderResult(RenderSession session) { 258 Object validationData = session.getValidationData(); 259 assertNotNull(validationData); 260 assertTrue(validationData instanceof ValidatorResult); 261 return (ValidatorResult) validationData; 262 } render(String fileName, RenderSessionListener verifier)263 private void render(String fileName, RenderSessionListener verifier) throws Exception { 264 render(fileName, verifier, true); 265 } 266 render( String fileName, RenderSessionListener verifier, boolean enableImageCheck)267 private void render( 268 String fileName, 269 RenderSessionListener verifier, 270 boolean enableImageCheck) throws Exception { 271 LayoutValidator.updatePolicy(new Policy( 272 EnumSet.of(Type.ACCESSIBILITY, Type.RENDER), 273 EnumSet.of(Level.ERROR, Level.WARNING, Level.INFO, Level.VERBOSE))); 274 275 LayoutPullParser parser = createParserFromPath(fileName); 276 LayoutLibTestCallback layoutLibCallback = 277 new LayoutLibTestCallback(getLogger(), mDefaultClassLoader); 278 layoutLibCallback.initResources(); 279 SessionParamsBuilder params = getSessionParamsBuilder() 280 .setParser(parser) 281 .setConfigGenerator(ConfigGenerator.NEXUS_5) 282 .setCallback(layoutLibCallback) 283 .disableDecoration() 284 .enableLayoutValidation(); 285 286 if (enableImageCheck) { 287 params.enableLayoutValidationImageCheck(); 288 } 289 290 render(sBridge, params.build(), -1, verifier); 291 } 292 293 /** 294 * Helper class that checks the list of issues.. 295 */ 296 private static class ExpectedLevels { 297 // Number of errors expected 298 public int expectedErrors = 0; 299 // Number of warnings expected 300 public int expectedWarnings = 0; 301 // Number of infos expected 302 public int expectedInfos = 0; 303 // Number of verboses expected 304 public int expectedVerboses = 0; 305 check(List<Issue> issues)306 public void check(List<Issue> issues) { 307 int errors = 0; 308 int warnings = 0; 309 int infos = 0; 310 int verboses = 0; 311 312 for (Issue issue : issues) { 313 switch (issue.mLevel) { 314 case ERROR: 315 errors++; 316 break; 317 case WARNING: 318 warnings++; 319 break; 320 case INFO: 321 infos++; 322 break; 323 case VERBOSE: 324 verboses++; 325 break; 326 } 327 } 328 329 assertEquals("Number of expected errors", expectedErrors, errors); 330 assertEquals("Number of expected warnings",expectedWarnings, warnings); 331 assertEquals("Number of expected infos", expectedInfos, infos); 332 assertEquals("Number of expected verboses", expectedVerboses, verboses); 333 334 int size = expectedErrors + expectedWarnings + expectedInfos + expectedVerboses; 335 assertEquals("expected size", size, issues.size()); 336 } 337 }; 338 } 339