Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(185)

Side by Side Diff: ios/chrome/test/earl_grey/accessibility_util.mm

Issue 2580333003: Upstream Chrome on iOS source code [10/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import <EarlGrey/EarlGrey.h>
6 #import <XCTest/XCTest.h>
7
8 #import "ios/chrome/test/earl_grey/accessibility_util.h"
9
10 namespace {
11
12 // Returns whether a UIView is hidden from the screen, based on the alpha
13 // and hidden properties of the UIView.
14 bool ViewIsHidden(UIView* view) {
15 return view.alpha == 0 || view.hidden;
16 }
17
18 // Returns whether a view should be accessible. A view should be accessible if
19 // either it is a whitelisted type or its isAccessibilityElement property is
20 // enabled.
21 bool ViewShouldBeAccessible(UIView* view) {
22 NSArray* classList = @[
23 [UILabel class], [UIControl class], [UITableView class],
24 [UICollectionViewCell class]
25 ];
26 bool viewIsAccessibleType = false;
27 for (Class accessibleClass in classList) {
28 if ([view isKindOfClass:accessibleClass]) {
29 viewIsAccessibleType = true;
30 break;
31 }
32 }
33 return (viewIsAccessibleType || view.isAccessibilityElement) &&
34 !ViewIsHidden(view);
35 }
36
37 // Returns true if |element| or descendant has property accessibilityLabel
38 // that is not an empty string. If |element| has isAccessibilityElement set to
39 // true, then the |element| itself must have a label.
40 bool ViewOrDescendantHasAccessibilityLabel(UIView* element) {
41 if ([element.accessibilityLabel length])
42 return true;
43 if (element.isAccessibilityElement)
44 return false;
45 for (UIView* view in element.subviews) {
46 if (ViewOrDescendantHasAccessibilityLabel(view))
47 return true;
48 }
49 return false;
50 }
51
52 // Returns true if |element|'s accessibilityLabel is a not a bad default value,
53 // particularly that it does not match the name of an image in the bundle, since
54 // UIKit can set the accessibilityLabel to an associated image name if no other
55 // data is available.
56 bool ViewHasNonDefaultAccessibilityLabel(UIView* element) {
57 if (element.accessibilityLabel) {
58 // Replace all spaces with underscores because when UIKit converts an
59 // image name to an accessibility label by default, it replaces underscores
60 // with spaces.
61 NSString* fileName =
62 [element.accessibilityLabel stringByReplacingOccurrencesOfString:@" "
63 withString:@"_"];
64 if ([fileName rangeOfString:@"/"].location != NSNotFound)
65 return true; // "/" is not a valid character in a file name
66 if ([UIImage imageNamed:fileName])
67 return false;
68 if ([UIImage imageNamed:[fileName stringByAppendingString:@".jpg"]])
69 return false;
70 if ([UIImage imageNamed:[fileName stringByAppendingString:@".gif"]])
71 return false;
72 }
73 return true;
74 }
75
76 // Returns an array of elements that should be accessible.
77 // Helper method for accessibilityElementsStartingFromView, so that
78 // |ancestorString|, which handles internal bookkeeping, is hidden from the top
79 // level API. |ancestorString| is the description of the most recent ancestor
80 // with isAccessibilityElement set to true, and thus when the method is first
81 // called, it should always be set to nil.
82 NSArray* AccessibilityElementsHelperStartingFromView(UIView* view,
83 NSString* ancestorString,
84 NSError** error) {
85 NSMutableArray* results = [NSMutableArray array];
86 // Add |view| to |results| if it should be accessible and is not hidden.
87 if (ViewShouldBeAccessible(view)) {
88 // UILabels have an extra check since some labels are dynamically set, and
89 // may not have text or an accessibilityLabel at a given point of
90 // execution.
91 if ([view isKindOfClass:[UILabel class]]) {
92 UILabel* label = static_cast<UILabel*>(view);
93 if ([label.text length])
94 [results addObject:label];
95 } else {
96 [results addObject:view];
97 if ([ancestorString length]) {
98 if (error != nil && !*error)
99 *error = [NSError errorWithDomain:@"Ancestor blocks VoiceOver"
100 code:1
101 userInfo:nil];
102 // The most recent ancestor with Accessibility Element set to true
103 // blocks VoiceOver for Element ancestorString
104 }
105 }
106 }
107 if (view.isAccessibilityElement && ![view isKindOfClass:[UILabel class]]) {
108 ancestorString = [view description];
109 }
110 if (![view isKindOfClass:[UITableView class]]) {
111 // Do not recurse below views which are accessible but may have children
112 // that default to being accessible. Also, do not recurse below views which
113 // implement the UIAccessibilityContainer informal protocol, as these views
114 // have taken ownership of accessibility behavior of descendents.
115 if ([view isKindOfClass:[UISwitch class]] ||
116 [view respondsToSelector:@selector(accessibilityElements)])
117 return results;
118 for (UIView* subView in [view subviews]) {
119 [results addObjectsFromArray:AccessibilityElementsHelperStartingFromView(
120 subView, ancestorString, error)];
121 }
122 }
123 return results;
124 }
125
126 // Recursively traverses the UIView tree and returns all views that should be
127 // accessible. Views should be accessible if they are of type UIControl,
128 // UILabel, UITableViewCell, or UICollectionViewCell or if their
129 // isAccessibilityElement property is set to true. Also, it prints errors when
130 // an ancestor of an element which should be accessible has
131 // isAccessibilityElement set to true. It notifies the calling method of this
132 // error by assigning |error| to a new NSError object. By passing in an NSError
133 // object set to nil, it can be used to determine if there was an error.
134 NSArray* AccessibilityElementsStartingFromView(UIView* view, NSError** error) {
135 return AccessibilityElementsHelperStartingFromView(view, nil, error);
136 }
137
138 // Starting from |view|, verifies that no view masks its descendants by having
139 // isAccessibilityElement set to true. |ancestorString| is the description of
140 // the most recent ancestor with isAccessibilityElement set to true, and thus
141 // when the method is first called, it should always be set to nil.
142 bool ViewAndDescendantsDoNotBlockVoiceOver(UIView* view,
143 NSString* ancestorString) {
144 if (![view isKindOfClass:[UILabel class]] && ViewShouldBeAccessible(view)) {
145 if ([ancestorString length]) {
146 // Ancestor String masks Descendant because the ancestor's
147 // isAccessibilityElement is set to true.
148 return false;
149 }
150 if (view.isAccessibilityElement) {
151 ancestorString = [view description];
152 }
153 }
154 // Do not recurse through hidden elements, as their descendants are also
155 // hidden.
156 if (ViewIsHidden(view))
157 return true;
158 // Do not recurse below views which are accessible but may have children
159 // that default to being accessible. Also, do not recurse below views which
160 // implement the UIAccessibilityContainer informal protocol, as these views
161 // have taken ownership of accessibility behavior of descendents.
162 if ([view isKindOfClass:[UISwitch class]] ||
163 [view respondsToSelector:@selector(accessibilityElements)])
164 return true;
165 bool ancestorMasksDescendant = true;
166 for (UIView* subView in [view subviews]) {
167 if (!ViewAndDescendantsDoNotBlockVoiceOver(subView, ancestorString)) {
168 ancestorMasksDescendant = false;
169 }
170 }
171 return ancestorMasksDescendant;
172 }
173
174 // Run accessibilityLabel tests on |view|. This method is used for cases where
175 // tests are grouped by element, instead of the typical case where elements are
176 // grouped by test.
177 bool VerifyElementAccessibilityLabel(UIView* view) {
178 if (view && !ViewIsHidden(view)) {
179 if (!ViewOrDescendantHasAccessibilityLabel(view)) {
180 // TODO: (crbug.com/650800) Add more verbose fail case logging.
181 return false;
182 }
183 if (!ViewHasNonDefaultAccessibilityLabel(view)) {
184 // TODO: (crbug.com/650800) Add more verbose fail case logging.
185 return false;
186 }
187 }
188 return true;
189 }
190
191 // Verifies |tableView|'s accessibility by scrolling through to ensure that
192 // accessibility tests are run on each cell. Cells which are offscreen may
193 // not be in the UIView hierarchy, so UITableViews must be scrolled in order to
194 // verify all of its cells. The method will scroll through the |tableView| no
195 // more than the number of times specified with the |kMaxTableViewScrolls|
196 // constant so that dynamically updated UITableViews do not scroll infinitely.
197 bool VerifyTableViewAccessibility(UITableView* tableView) {
198 // Reload |tableView| in order to update its representation in the view
199 // hierarchy, which can be stale.
200 [tableView reloadData];
201 [tableView layoutIfNeeded];
202 bool hasRows = false;
203 NSInteger numberOfSections = [tableView numberOfSections];
204 for (NSInteger section = 0; section < numberOfSections; section++) {
205 if ([tableView numberOfRowsInSection:section]) {
206 hasRows = true;
207 break;
208 }
209 }
210 if (!hasRows) {
211 return ViewAndDescendantsDoNotBlockVoiceOver(tableView, nil);
212 }
213 bool tableViewIsAccessible = true;
214 NSIndexPath* prevIndexPath = nil;
215 // Cell index path to scroll to on each iteration.
216 NSIndexPath* nextIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
217 bool hasMoreRows = true;
218 // The maximum number of times that the test will scroll through a
219 // UITableView.
220 const NSUInteger kMaxTableViewScrolls = 1000;
221 NSUInteger numberOfScrolls = 0;
222 // Iterate until the tests have run on every cell in |tableView| or max number
223 // of scrolls is reached.
224 while (hasMoreRows && numberOfScrolls < kMaxTableViewScrolls) {
225 [tableView scrollToRowAtIndexPath:nextIndexPath
226 atScrollPosition:UITableViewScrollPositionTop
227 animated:false];
228 [tableView reloadData];
229 [tableView layoutIfNeeded];
230 if (!ViewAndDescendantsDoNotBlockVoiceOver(tableView, nil)) {
231 tableViewIsAccessible = false;
232 // TODO: (crbug.com/650800) Add more verbose fail case logging.
233 }
234 for (UITableViewCell* cell in tableView.visibleCells) {
235 NSError* error = nil;
236 NSArray* accessibleElements =
237 AccessibilityElementsStartingFromView(cell, &error);
238 for (UIView* view in accessibleElements) {
239 if (!VerifyElementAccessibilityLabel(view))
240 tableViewIsAccessible = false;
241 // TODO: (crbug.com/650800) Add more verbose fail case logging.
242 }
243 }
244 nextIndexPath =
245 [tableView indexPathForCell:[tableView.visibleCells lastObject]];
246 // If nextIndexPath is nil or it is greater than or equal to the last cell
247 // in the |tableView|, end loop.
248 if (!nextIndexPath ||
249 (nextIndexPath.section >= tableView.numberOfSections - 1 &&
250 nextIndexPath.row >=
251 [tableView numberOfRowsInSection:nextIndexPath.section] - 1)) {
252 hasMoreRows = false;
253 }
254 // If nextIndexPath is the same value as prev, which can happen if the
255 // scrolling fails, set nextIndexPath to the next cell.
256 if ([nextIndexPath isEqual:prevIndexPath]) {
257 if (nextIndexPath.row ==
258 [tableView numberOfRowsInSection:nextIndexPath.section] - 1) {
259 nextIndexPath =
260 [NSIndexPath indexPathForRow:0 inSection:nextIndexPath.section + 1];
261 } else {
262 nextIndexPath = [NSIndexPath indexPathForRow:nextIndexPath.row + 1
263 inSection:nextIndexPath.section];
264 }
265 }
266 prevIndexPath = nextIndexPath;
267 numberOfScrolls++;
268 }
269 return tableViewIsAccessible;
270 }
271 }
272
273 namespace chrome_test_util {
274
275 void VerifyAccessibilityForCurrentScreen() {
276 NSMutableArray* accessibilityElements = [NSMutableArray array];
277 NSError* inaccessibleChildrenError = nil;
278 // Checking for elements that are inaccessible because
279 // they have an ancestor whose isAccessibilityElement is set to
280 // true, blocking VoiceOver from reaching them...
281 for (UIWindow* window in [[UIApplication sharedApplication] windows]) {
282 // If window is UITextEffectsWindow or UIRemoteKeyboardWindow skip
283 // accessibility check as this is likely a native keyboard.
284 if (!([NSStringFromClass([window class])
285 isEqualToString:@"UITextEffectsWindow"]) &&
286 !([NSStringFromClass([window class])
287 isEqualToString:@"UIRemoteKeyboardWindow"])) {
288 NSArray* windowElements = AccessibilityElementsStartingFromView(
289 window, &inaccessibleChildrenError);
290 [accessibilityElements addObjectsFromArray:windowElements];
291 }
292 }
293
294 // Special case UITableViews. Some elements on UITableViews are not in
295 // the view tree, so we must scroll to ensure that each row in the
296 // UITableView is visible when the accessibility tests are run. Also
297 // removes all UITableViews from accessibilityElements to stop other
298 // tests from running on the table.
299 bool tableViewError = false;
300 NSMutableIndexSet* tableViewsToBeRemoved = [NSMutableIndexSet indexSet];
301 NSUInteger accessibilityIndex = 0;
302 // Checking for TableView errors...
303 for (UIView* view in accessibilityElements) {
304 if ([view isKindOfClass:[UITableView class]]) {
305 UITableView* table_view = static_cast<UITableView*>(view);
306 if (!VerifyTableViewAccessibility(table_view)) {
307 tableViewError = true;
308 }
309 [tableViewsToBeRemoved addIndex:accessibilityIndex];
310 }
311 accessibilityIndex++;
312 }
313 [accessibilityElements removeObjectsAtIndexes:tableViewsToBeRemoved];
314 // Find all elements without labels and generate associated error
315 // messages.
316 bool noLabels = false;
317 NSString* noLabelElementDesc = @"";
318 // Checking for elements without labels...
319 for (UIView* view in accessibilityElements) {
320 if (!ViewOrDescendantHasAccessibilityLabel(view)) {
321 [noLabelElementDesc
322 stringByAppendingString:[NSString
323 stringWithFormat:@"\n'%@'",
324 [view description]]];
325 noLabels = true;
326 }
327 }
328 // Find all elements which have set their accessibility labels to the
329 // name of an associated image, and generate associated error
330 // messages.
331 bool badDefaultLabel = false;
332 NSString* badDefaultLabelDesc = @"";
333 // Checking for labels with default values...
334 for (UIView* view in accessibilityElements) {
335 if (!ViewHasNonDefaultAccessibilityLabel(view)) {
336 [badDefaultLabelDesc
337 stringByAppendingString:[NSString
338 stringWithFormat:@"\n'%@'",
339 [view description]]];
340 badDefaultLabel = true;
341 }
342 }
343
344 GREYAssert(!inaccessibleChildrenError,
345 @"The accessibility tests failed: Inaccessible children error");
346 GREYAssert(!noLabels, @"The accessibility tests failed: No labels error");
347 GREYAssert(!badDefaultLabel,
348 @"The accessibility tests failed: Bad default labels error");
349 GREYAssert(!tableViewError,
350 @"The accessibility tests failed: Table view error");
351 }
352
353 } // namespace chrome_test_util
OLDNEW
« no previous file with comments | « ios/chrome/test/earl_grey/accessibility_util.h ('k') | ios/chrome/test/earl_grey/chrome_actions.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698