Commit 2abfd4e4 authored by Lindsay Pasricha's avatar Lindsay Pasricha Committed by Commit Bot

Use gtxtoolkit for verifyAccessibilityOnScreen

Migrate from using our custom utility and helpers to a similar util
developed and maintained by the EG team. Doc at go/bling-axe

Bug: 856622
Cq-Include-Trybots: luci.chromium.try:ios-simulator-cronet;luci.chromium.try:ios-simulator-full-configs
Change-Id: Ib86b171447eac2961ec2591c8a08764d06b7a953
Reviewed-on: https://chromium-review.googlesource.com/c/1291570
Commit-Queue: Lindsay Pasricha <lindsayw@chromium.org>
Reviewed-by: default avatarSylvain Defresne <sdefresne@chromium.org>
Reviewed-by: default avatarSergio Collazos <sczs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#608924}
parent 1ab59f89
...@@ -2927,6 +2927,7 @@ id<GREYMatcher> SearchIconButton() { ...@@ -2927,6 +2927,7 @@ id<GREYMatcher> SearchIconButton() {
[super tearDown]; [super tearDown];
GREYAssert(chrome_test_util::ClearBookmarks(), GREYAssert(chrome_test_util::ClearBookmarks(),
@"Not all bookmarks were removed."); @"Not all bookmarks were removed.");
// Clear position cache so that Bookmarks starts at the root folder in next // Clear position cache so that Bookmarks starts at the root folder in next
// test. // test.
ios::ChromeBrowserState* browser_state = ios::ChromeBrowserState* browser_state =
......
...@@ -258,6 +258,7 @@ source_set("test_support") { ...@@ -258,6 +258,7 @@ source_set("test_support") {
public_deps = [ public_deps = [
"//build/config/ios:xctest", "//build/config/ios:xctest",
"//ios/third_party/earl_grey:earl_grey+link", "//ios/third_party/earl_grey:earl_grey+link",
"//ios/third_party/gtx:gtx+link",
"//ios/web/public/test", "//ios/web/public/test",
"//ios/web/public/test/fakes", "//ios/web/public/test/fakes",
"//net:test_support", "//net:test_support",
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
#import <EarlGrey/EarlGrey.h> #import <EarlGrey/EarlGrey.h>
#import <GTXiLib/GTXiLib.h>
#import <XCTest/XCTest.h> #import <XCTest/XCTest.h>
#include "ios/chrome/test/earl_grey/accessibility_util.h" #include "ios/chrome/test/earl_grey/accessibility_util.h"
...@@ -53,30 +54,6 @@ bool ViewOrDescendantHasAccessibilityLabel(UIView* element) { ...@@ -53,30 +54,6 @@ bool ViewOrDescendantHasAccessibilityLabel(UIView* element) {
return false; 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. // Returns an array of elements that should be accessible.
// Helper method for accessibilityElementsStartingFromView, so that // Helper method for accessibilityElementsStartingFromView, so that
// |ancestorString|, which handles internal bookkeeping, is hidden from the top // |ancestorString|, which handles internal bookkeeping, is hidden from the top
...@@ -126,232 +103,19 @@ NSArray* AccessibilityElementsHelperStartingFromView(UIView* view, ...@@ -126,232 +103,19 @@ NSArray* AccessibilityElementsHelperStartingFromView(UIView* view,
} }
return results; 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 { namespace chrome_test_util {
void VerifyAccessibilityForCurrentScreen() { void VerifyAccessibilityForCurrentScreen() {
NSMutableArray* accessibilityElements = [NSMutableArray array]; GTXToolKit* toolkit = [[GTXToolKit alloc] init];
NSError* inaccessibleChildrenError = nil; NSError* error = 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]) { for (UIWindow* window in [[UIApplication sharedApplication] windows]) {
// If window is UITextEffectsWindow or UIRemoteKeyboardWindow skip // Run the checks on all elements on the screen.
// accessibility check as this is likely a native keyboard. BOOL success =
if (!([NSStringFromClass([window class]) [toolkit checkAllElementsFromRootElements:@[ window ] error:&error];
isEqualToString:@"UITextEffectsWindow"]) && GREYAssert(success, @"Accessibility checks failed! Error: %@", error);
!([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 } // namespace chrome_test_util
...@@ -14,6 +14,7 @@ template("ios_eg_test") { ...@@ -14,6 +14,7 @@ template("ios_eg_test") {
} }
bundle_deps += [ bundle_deps += [
"//ios/third_party/earl_grey:earl_grey+bundle", "//ios/third_party/earl_grey:earl_grey+bundle",
"//ios/third_party/gtx:gtx+bundle",
"//ios/third_party/ochamcrest:ochamcrest+bundle", "//ios/third_party/ochamcrest:ochamcrest+bundle",
] ]
if (ios_enable_firebase_sdk) { if (ios_enable_firebase_sdk) {
......
...@@ -10,33 +10,83 @@ config("config") { ...@@ -10,33 +10,83 @@ config("config") {
} }
ios_framework_bundle("gtx") { ios_framework_bundle("gtx") {
output_name = "GTX" output_name = "GTXiLib"
info_plist = "src/FrameworkFiles/Info.plist" info_plist = "Info.plist"
testonly = true testonly = true
sources = [ sources = [
"src/Classes/GTXAccessibilityTree.h", "src/Classes/GTXAccessibilityTree.h",
"src/Classes/GTXAccessibilityTree.m",
"src/Classes/GTXAnalytics.h", "src/Classes/GTXAnalytics.h",
"src/Classes/GTXAnalytics.m",
"src/Classes/GTXAnalyticsUtils.h", "src/Classes/GTXAnalyticsUtils.h",
"src/Classes/GTXAnalyticsUtils.m",
"src/Classes/GTXAssertions.h", "src/Classes/GTXAssertions.h",
"src/Classes/GTXBlacklistBlock.h",
"src/Classes/GTXBlacklistBlock.m",
"src/Classes/GTXBlacklistFactory.h",
"src/Classes/GTXBlacklistFactory.m",
"src/Classes/GTXBlacklisting.h",
"src/Classes/GTXCheckBlock.h", "src/Classes/GTXCheckBlock.h",
"src/Classes/GTXCheckBlock.m",
"src/Classes/GTXChecking.h", "src/Classes/GTXChecking.h",
"src/Classes/GTXChecksCollection.h", "src/Classes/GTXChecksCollection.h",
"src/Classes/GTXChecksCollection.m",
"src/Classes/GTXCommon.h", "src/Classes/GTXCommon.h",
"src/Classes/GTXElementBlacklist.h", "src/Classes/GTXElementBlacklist.h",
"src/Classes/GTXElementBlacklist.m",
"src/Classes/GTXErrorReporter.h", "src/Classes/GTXErrorReporter.h",
"src/Classes/GTXErrorReporter.m",
"src/Classes/GTXImageAndColorUtils.h", "src/Classes/GTXImageAndColorUtils.h",
"src/Classes/GTXImageAndColorUtils.m",
"src/Classes/GTXImageRGBAData.h", "src/Classes/GTXImageRGBAData.h",
"src/Classes/GTXImageRGBAData.m",
"src/Classes/GTXLogging.h", "src/Classes/GTXLogging.h",
"src/Classes/GTXPluginXCTestCase.h", "src/Classes/GTXPluginXCTestCase.h",
"src/Classes/GTXPluginXCTestCase.m",
"src/Classes/GTXTestCase.h", "src/Classes/GTXTestCase.h",
"src/Classes/GTXTestCase.m",
"src/Classes/GTXTestEnvironment.h",
"src/Classes/GTXTestEnvironment.m",
"src/Classes/GTXTestSuite.h", "src/Classes/GTXTestSuite.h",
"src/Classes/GTXTestSuite.m",
"src/Classes/GTXToolKit.h", "src/Classes/GTXToolKit.h",
"src/Classes/GTXToolKit.m",
"src/Classes/GTXiLib.h", "src/Classes/GTXiLib.h",
"src/Classes/GTXiLibCore.h", "src/Classes/GTXiLibCore.h",
"src/Classes/GTXiLibCore.m",
"src/Classes/NSError+GTXAdditions.h", "src/Classes/NSError+GTXAdditions.h",
"src/Classes/NSError+GTXAdditions.m",
]
public_headers = [
"src/Classes/GTXAccessibilityTree.h",
"src/Classes/GTXAnalytics.h",
"src/Classes/GTXAnalyticsUtils.h",
"src/Classes/GTXAssertions.h",
"src/Classes/GTXBlacklistBlock.h",
"src/Classes/GTXBlacklistFactory.h",
"src/Classes/GTXBlacklisting.h",
"src/Classes/GTXCheckBlock.h",
"src/Classes/GTXChecking.h",
"src/Classes/GTXChecksCollection.h",
"src/Classes/GTXCommon.h",
"src/Classes/GTXElementBlacklist.h",
"src/Classes/GTXErrorReporter.h",
"src/Classes/GTXImageAndColorUtils.h",
"src/Classes/GTXImageRGBAData.h",
"src/Classes/GTXLogging.h",
"src/Classes/GTXPluginXCTestCase.h",
"src/Classes/GTXTestCase.h",
"src/Classes/GTXTestEnvironment.h",
"src/Classes/GTXTestSuite.h",
"src/Classes/GTXToolKit.h",
"src/Classes/GTXiLib.h",
"src/Classes/GTXiLibCore.h",
"src/Classes/NSError+GTXAdditions.h",
]
deps = [
"//build/config/ios:xctest",
] ]
public_headers = [ "src/Classes/GTXiLib.h" ]
libs = [ libs = [
"CoreGraphics.framework", "CoreGraphics.framework",
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleIdentifier</key>
<string>${IOS_BUNDLE_ID_PREFIX}.test.${EXECUTABLE_NAME:rfc1034identifier}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
</dict>
</plist>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment