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