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

Unified 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 side-by-side diff with in-line comments
Download patch
« 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 »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: ios/chrome/test/earl_grey/accessibility_util.mm
diff --git a/ios/chrome/test/earl_grey/accessibility_util.mm b/ios/chrome/test/earl_grey/accessibility_util.mm
new file mode 100644
index 0000000000000000000000000000000000000000..ce17c3aebad7151002491b9e6cd0bfd1c6d2c965
--- /dev/null
+++ b/ios/chrome/test/earl_grey/accessibility_util.mm
@@ -0,0 +1,353 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <EarlGrey/EarlGrey.h>
+#import <XCTest/XCTest.h>
+
+#import "ios/chrome/test/earl_grey/accessibility_util.h"
+
+namespace {
+
+// Returns whether a UIView is hidden from the screen, based on the alpha
+// and hidden properties of the UIView.
+bool ViewIsHidden(UIView* view) {
+ return view.alpha == 0 || view.hidden;
+}
+
+// Returns whether a view should be accessible. A view should be accessible if
+// either it is a whitelisted type or its isAccessibilityElement property is
+// enabled.
+bool ViewShouldBeAccessible(UIView* view) {
+ NSArray* classList = @[
+ [UILabel class], [UIControl class], [UITableView class],
+ [UICollectionViewCell class]
+ ];
+ bool viewIsAccessibleType = false;
+ for (Class accessibleClass in classList) {
+ if ([view isKindOfClass:accessibleClass]) {
+ viewIsAccessibleType = true;
+ break;
+ }
+ }
+ return (viewIsAccessibleType || view.isAccessibilityElement) &&
+ !ViewIsHidden(view);
+}
+
+// Returns true if |element| or descendant has property accessibilityLabel
+// that is not an empty string. If |element| has isAccessibilityElement set to
+// true, then the |element| itself must have a label.
+bool ViewOrDescendantHasAccessibilityLabel(UIView* element) {
+ if ([element.accessibilityLabel length])
+ return true;
+ if (element.isAccessibilityElement)
+ return false;
+ for (UIView* view in element.subviews) {
+ if (ViewOrDescendantHasAccessibilityLabel(view))
+ return true;
+ }
+ return false;
+}
+
+// Returns true if |element|'s accessibilityLabel is a not a bad default value,
+// particularly that it does not match the name of an image in the bundle, since
+// UIKit can set the accessibilityLabel to an associated image name if no other
+// data is available.
+bool ViewHasNonDefaultAccessibilityLabel(UIView* element) {
+ if (element.accessibilityLabel) {
+ // Replace all spaces with underscores because when UIKit converts an
+ // image name to an accessibility label by default, it replaces underscores
+ // with spaces.
+ NSString* fileName =
+ [element.accessibilityLabel stringByReplacingOccurrencesOfString:@" "
+ withString:@"_"];
+ if ([fileName rangeOfString:@"/"].location != NSNotFound)
+ return true; // "/" is not a valid character in a file name
+ if ([UIImage imageNamed:fileName])
+ return false;
+ if ([UIImage imageNamed:[fileName stringByAppendingString:@".jpg"]])
+ return false;
+ if ([UIImage imageNamed:[fileName stringByAppendingString:@".gif"]])
+ return false;
+ }
+ return true;
+}
+
+// Returns an array of elements that should be accessible.
+// Helper method for accessibilityElementsStartingFromView, so that
+// |ancestorString|, which handles internal bookkeeping, is hidden from the top
+// level API. |ancestorString| is the description of the most recent ancestor
+// with isAccessibilityElement set to true, and thus when the method is first
+// called, it should always be set to nil.
+NSArray* AccessibilityElementsHelperStartingFromView(UIView* view,
+ NSString* ancestorString,
+ NSError** error) {
+ NSMutableArray* results = [NSMutableArray array];
+ // Add |view| to |results| if it should be accessible and is not hidden.
+ if (ViewShouldBeAccessible(view)) {
+ // UILabels have an extra check since some labels are dynamically set, and
+ // may not have text or an accessibilityLabel at a given point of
+ // execution.
+ if ([view isKindOfClass:[UILabel class]]) {
+ UILabel* label = static_cast<UILabel*>(view);
+ if ([label.text length])
+ [results addObject:label];
+ } else {
+ [results addObject:view];
+ if ([ancestorString length]) {
+ if (error != nil && !*error)
+ *error = [NSError errorWithDomain:@"Ancestor blocks VoiceOver"
+ code:1
+ userInfo:nil];
+ // The most recent ancestor with Accessibility Element set to true
+ // blocks VoiceOver for Element ancestorString
+ }
+ }
+ }
+ if (view.isAccessibilityElement && ![view isKindOfClass:[UILabel class]]) {
+ ancestorString = [view description];
+ }
+ if (![view isKindOfClass:[UITableView class]]) {
+ // Do not recurse below views which are accessible but may have children
+ // that default to being accessible. Also, do not recurse below views which
+ // implement the UIAccessibilityContainer informal protocol, as these views
+ // have taken ownership of accessibility behavior of descendents.
+ if ([view isKindOfClass:[UISwitch class]] ||
+ [view respondsToSelector:@selector(accessibilityElements)])
+ return results;
+ for (UIView* subView in [view subviews]) {
+ [results addObjectsFromArray:AccessibilityElementsHelperStartingFromView(
+ subView, ancestorString, error)];
+ }
+ }
+ return results;
+}
+
+// Recursively traverses the UIView tree and returns all views that should be
+// accessible. Views should be accessible if they are of type UIControl,
+// UILabel, UITableViewCell, or UICollectionViewCell or if their
+// isAccessibilityElement property is set to true. Also, it prints errors when
+// an ancestor of an element which should be accessible has
+// isAccessibilityElement set to true. It notifies the calling method of this
+// error by assigning |error| to a new NSError object. By passing in an NSError
+// object set to nil, it can be used to determine if there was an error.
+NSArray* AccessibilityElementsStartingFromView(UIView* view, NSError** error) {
+ return AccessibilityElementsHelperStartingFromView(view, nil, error);
+}
+
+// Starting from |view|, verifies that no view masks its descendants by having
+// isAccessibilityElement set to true. |ancestorString| is the description of
+// the most recent ancestor with isAccessibilityElement set to true, and thus
+// when the method is first called, it should always be set to nil.
+bool ViewAndDescendantsDoNotBlockVoiceOver(UIView* view,
+ NSString* ancestorString) {
+ if (![view isKindOfClass:[UILabel class]] && ViewShouldBeAccessible(view)) {
+ if ([ancestorString length]) {
+ // Ancestor String masks Descendant because the ancestor's
+ // isAccessibilityElement is set to true.
+ return false;
+ }
+ if (view.isAccessibilityElement) {
+ ancestorString = [view description];
+ }
+ }
+ // Do not recurse through hidden elements, as their descendants are also
+ // hidden.
+ if (ViewIsHidden(view))
+ return true;
+ // Do not recurse below views which are accessible but may have children
+ // that default to being accessible. Also, do not recurse below views which
+ // implement the UIAccessibilityContainer informal protocol, as these views
+ // have taken ownership of accessibility behavior of descendents.
+ if ([view isKindOfClass:[UISwitch class]] ||
+ [view respondsToSelector:@selector(accessibilityElements)])
+ return true;
+ bool ancestorMasksDescendant = true;
+ for (UIView* subView in [view subviews]) {
+ if (!ViewAndDescendantsDoNotBlockVoiceOver(subView, ancestorString)) {
+ ancestorMasksDescendant = false;
+ }
+ }
+ return ancestorMasksDescendant;
+}
+
+// Run accessibilityLabel tests on |view|. This method is used for cases where
+// tests are grouped by element, instead of the typical case where elements are
+// grouped by test.
+bool VerifyElementAccessibilityLabel(UIView* view) {
+ if (view && !ViewIsHidden(view)) {
+ if (!ViewOrDescendantHasAccessibilityLabel(view)) {
+ // TODO: (crbug.com/650800) Add more verbose fail case logging.
+ return false;
+ }
+ if (!ViewHasNonDefaultAccessibilityLabel(view)) {
+ // TODO: (crbug.com/650800) Add more verbose fail case logging.
+ return false;
+ }
+ }
+ return true;
+}
+
+// Verifies |tableView|'s accessibility by scrolling through to ensure that
+// accessibility tests are run on each cell. Cells which are offscreen may
+// not be in the UIView hierarchy, so UITableViews must be scrolled in order to
+// verify all of its cells. The method will scroll through the |tableView| no
+// more than the number of times specified with the |kMaxTableViewScrolls|
+// constant so that dynamically updated UITableViews do not scroll infinitely.
+bool VerifyTableViewAccessibility(UITableView* tableView) {
+ // Reload |tableView| in order to update its representation in the view
+ // hierarchy, which can be stale.
+ [tableView reloadData];
+ [tableView layoutIfNeeded];
+ bool hasRows = false;
+ NSInteger numberOfSections = [tableView numberOfSections];
+ for (NSInteger section = 0; section < numberOfSections; section++) {
+ if ([tableView numberOfRowsInSection:section]) {
+ hasRows = true;
+ break;
+ }
+ }
+ if (!hasRows) {
+ return ViewAndDescendantsDoNotBlockVoiceOver(tableView, nil);
+ }
+ bool tableViewIsAccessible = true;
+ NSIndexPath* prevIndexPath = nil;
+ // Cell index path to scroll to on each iteration.
+ NSIndexPath* nextIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
+ bool hasMoreRows = true;
+ // The maximum number of times that the test will scroll through a
+ // UITableView.
+ const NSUInteger kMaxTableViewScrolls = 1000;
+ NSUInteger numberOfScrolls = 0;
+ // Iterate until the tests have run on every cell in |tableView| or max number
+ // of scrolls is reached.
+ while (hasMoreRows && numberOfScrolls < kMaxTableViewScrolls) {
+ [tableView scrollToRowAtIndexPath:nextIndexPath
+ atScrollPosition:UITableViewScrollPositionTop
+ animated:false];
+ [tableView reloadData];
+ [tableView layoutIfNeeded];
+ if (!ViewAndDescendantsDoNotBlockVoiceOver(tableView, nil)) {
+ tableViewIsAccessible = false;
+ // TODO: (crbug.com/650800) Add more verbose fail case logging.
+ }
+ for (UITableViewCell* cell in tableView.visibleCells) {
+ NSError* error = nil;
+ NSArray* accessibleElements =
+ AccessibilityElementsStartingFromView(cell, &error);
+ for (UIView* view in accessibleElements) {
+ if (!VerifyElementAccessibilityLabel(view))
+ tableViewIsAccessible = false;
+ // TODO: (crbug.com/650800) Add more verbose fail case logging.
+ }
+ }
+ nextIndexPath =
+ [tableView indexPathForCell:[tableView.visibleCells lastObject]];
+ // If nextIndexPath is nil or it is greater than or equal to the last cell
+ // in the |tableView|, end loop.
+ if (!nextIndexPath ||
+ (nextIndexPath.section >= tableView.numberOfSections - 1 &&
+ nextIndexPath.row >=
+ [tableView numberOfRowsInSection:nextIndexPath.section] - 1)) {
+ hasMoreRows = false;
+ }
+ // If nextIndexPath is the same value as prev, which can happen if the
+ // scrolling fails, set nextIndexPath to the next cell.
+ if ([nextIndexPath isEqual:prevIndexPath]) {
+ if (nextIndexPath.row ==
+ [tableView numberOfRowsInSection:nextIndexPath.section] - 1) {
+ nextIndexPath =
+ [NSIndexPath indexPathForRow:0 inSection:nextIndexPath.section + 1];
+ } else {
+ nextIndexPath = [NSIndexPath indexPathForRow:nextIndexPath.row + 1
+ inSection:nextIndexPath.section];
+ }
+ }
+ prevIndexPath = nextIndexPath;
+ numberOfScrolls++;
+ }
+ return tableViewIsAccessible;
+}
+}
+
+namespace chrome_test_util {
+
+void VerifyAccessibilityForCurrentScreen() {
+ NSMutableArray* accessibilityElements = [NSMutableArray array];
+ NSError* inaccessibleChildrenError = nil;
+ // Checking for elements that are inaccessible because
+ // they have an ancestor whose isAccessibilityElement is set to
+ // true, blocking VoiceOver from reaching them...
+ for (UIWindow* window in [[UIApplication sharedApplication] windows]) {
+ // If window is UITextEffectsWindow or UIRemoteKeyboardWindow skip
+ // accessibility check as this is likely a native keyboard.
+ if (!([NSStringFromClass([window class])
+ isEqualToString:@"UITextEffectsWindow"]) &&
+ !([NSStringFromClass([window class])
+ isEqualToString:@"UIRemoteKeyboardWindow"])) {
+ NSArray* windowElements = AccessibilityElementsStartingFromView(
+ window, &inaccessibleChildrenError);
+ [accessibilityElements addObjectsFromArray:windowElements];
+ }
+ }
+
+ // Special case UITableViews. Some elements on UITableViews are not in
+ // the view tree, so we must scroll to ensure that each row in the
+ // UITableView is visible when the accessibility tests are run. Also
+ // removes all UITableViews from accessibilityElements to stop other
+ // tests from running on the table.
+ bool tableViewError = false;
+ NSMutableIndexSet* tableViewsToBeRemoved = [NSMutableIndexSet indexSet];
+ NSUInteger accessibilityIndex = 0;
+ // Checking for TableView errors...
+ for (UIView* view in accessibilityElements) {
+ if ([view isKindOfClass:[UITableView class]]) {
+ UITableView* table_view = static_cast<UITableView*>(view);
+ if (!VerifyTableViewAccessibility(table_view)) {
+ tableViewError = true;
+ }
+ [tableViewsToBeRemoved addIndex:accessibilityIndex];
+ }
+ accessibilityIndex++;
+ }
+ [accessibilityElements removeObjectsAtIndexes:tableViewsToBeRemoved];
+ // Find all elements without labels and generate associated error
+ // messages.
+ bool noLabels = false;
+ NSString* noLabelElementDesc = @"";
+ // Checking for elements without labels...
+ for (UIView* view in accessibilityElements) {
+ if (!ViewOrDescendantHasAccessibilityLabel(view)) {
+ [noLabelElementDesc
+ stringByAppendingString:[NSString
+ stringWithFormat:@"\n'%@'",
+ [view description]]];
+ noLabels = true;
+ }
+ }
+ // Find all elements which have set their accessibility labels to the
+ // name of an associated image, and generate associated error
+ // messages.
+ bool badDefaultLabel = false;
+ NSString* badDefaultLabelDesc = @"";
+ // Checking for labels with default values...
+ for (UIView* view in accessibilityElements) {
+ if (!ViewHasNonDefaultAccessibilityLabel(view)) {
+ [badDefaultLabelDesc
+ stringByAppendingString:[NSString
+ stringWithFormat:@"\n'%@'",
+ [view description]]];
+ badDefaultLabel = true;
+ }
+ }
+
+ GREYAssert(!inaccessibleChildrenError,
+ @"The accessibility tests failed: Inaccessible children error");
+ GREYAssert(!noLabels, @"The accessibility tests failed: No labels error");
+ GREYAssert(!badDefaultLabel,
+ @"The accessibility tests failed: Bad default labels error");
+ GREYAssert(!tableViewError,
+ @"The accessibility tests failed: Table view error");
+}
+
+} // namespace chrome_test_util
« 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