blob: af46b565cf22792431522438895f13e7b2ac0a97 [file] [log] [blame]
//
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#import "GTXErrorReporter.h"
#import "NSError+GTXAdditions.h"
@implementation GTXErrorReporter
+ (UIImage *)screenshotWithFailingElements:(NSArray *)elements {
// Render a gray scale screen shot of the app screen. Gray scale allows the element outlines to
// be seen clearly.
UIImage *image = [self gtx_takeScreenshot];
UIImage *grayScaleScreenShot = [self gtx_grayScaleImageFromImage:image];
// Outline the locations of elements on the gray scale screenshot.
UIScreen *mainScreen = [UIScreen mainScreen];
UIGraphicsBeginImageContextWithOptions(mainScreen.bounds.size, YES, mainScreen.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[grayScaleScreenShot drawInRect:mainScreen.bounds];
for (id element in elements) {
// Find the element's outline rect that needs to be marked on the screenshot.
CGRect elementOutline = [element accessibilityFrame];
if ([element isKindOfClass:[UIView class]]) {
UIView *view = element;
elementOutline = [view convertRect:view.bounds toView:nil];
elementOutline = [[view window] convertRect:elementOutline toWindow:nil];
}
// Draw a rectangle indicating the outline.
CGContextSetLineWidth(context, 3.0);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextStrokeRect(context, elementOutline);
}
// Save the currently rendered image.
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
+ (UIImage *)screenshotView:(UIView *)element {
CGRect rect = CGRectStandardize(element.bounds);
rect.origin = CGPointZero;
UIGraphicsBeginImageContext(rect.size);
[element drawViewHierarchyInRect:rect afterScreenUpdates:NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
+ (NSString *)hierarchyDescriptionOfRootElements:(NSArray *)rootElements {
NSMutableArray *descriptions = [[NSMutableArray alloc] init];
for (id rootElement in rootElements) {
[self gtx_getRecursiveHierarchyDescriptionOfRootElement:rootElement
indent:nil
outDescription:descriptions];
}
return [descriptions componentsJoinedByString:@"\n"];
}
#pragma mark - Private
/**
* Traverses the view hierarchy starting from the given root @c element and fills @c description
* with a list of all elements descriptions.
*
* @param element The root element to start traversal from.
* @param indent The indent to apply to the root element's description.
* @param description A mutable array to be filled with elements descriptions.
*/
+ (void)gtx_getRecursiveHierarchyDescriptionOfRootElement:(id)element
indent:(NSString *)indent
outDescription:(NSMutableArray *)description {
indent = indent ? indent : @"";
NSMutableSet<id> *axChildren = [[NSMutableSet alloc] init];
NSMutableSet<id> *subviews = [[NSMutableSet alloc] init];
NSMutableSet<id> *both = [[NSMutableSet alloc] init];
// Get all a11y children.
NSArray *elements = [element accessibilityElements];
NSInteger count = [element accessibilityElementCount];
if (elements) {
[axChildren addObjectsFromArray:elements];
} else if (count > 0 && count != NSNotFound) {
for (NSInteger i = 0; i < count; i++) {
[axChildren addObject:[element accessibilityElementAtIndex:i]];
}
}
// Get all subviews
elements = [element respondsToSelector:@selector(subviews)] ? [element subviews] : nil;
for (id element in elements) {
if ([axChildren containsObject:element]) {
[both addObject:element];
} else {
[subviews addObject:element];
}
}
[axChildren minusSet:both];
[description addObject:[self gtx_hierarchyDescriptionOfSingleElement:element indent:indent]];
NSString *nextIndent = [indent stringByAppendingString:@" | "];
if ([both count]) {
[description addObject:
[NSString stringWithFormat:@"%@Subviews and accessibility elements(%d):",
indent,
(int)[both count]]];
for (id element in both) {
[self gtx_getRecursiveHierarchyDescriptionOfRootElement:element
indent:nextIndent
outDescription:description];
}
}
if ([axChildren count]) {
[description addObject:[NSString stringWithFormat:@"%@Accessibility elements(%d):",
indent,
(int)[axChildren count]]];
for (id element in axChildren) {
[self gtx_getRecursiveHierarchyDescriptionOfRootElement:element
indent:nextIndent
outDescription:description];
}
}
if ([subviews count]) {
[description addObject:[NSString stringWithFormat:@"%@Subviews(%d):",
indent,
(int)[subviews count]]];
for (id element in subviews) {
[self gtx_getRecursiveHierarchyDescriptionOfRootElement:element
indent:nextIndent
outDescription:description];
}
}
}
/**
* Returns a pretty printed description of various attributes the given element such as class,
* a11y attributes etc.
*
* @param element The element whose description is needed.
* @param indent The indent to apply to the root element's description.
*
* @return The description of the given element prepended with the indent.
*/
+ (NSString *)gtx_hierarchyDescriptionOfSingleElement:(id)element indent:(NSString *)indent {
NSMutableArray<NSString *> *descriptions = [[NSMutableArray alloc] init];
[descriptions addObject:[NSString stringWithFormat:@"%@<%@:%p",
indent,
NSStringFromClass([element class]),
element]];
// Adds a key=value pair to the description array.
void (^AddKeyValue)(NSString *key, NSString *value) = ^(NSString *key, NSString *value) {
[descriptions addObject:[NSString stringWithFormat:@"%@=%@",
key,
value ? value : @"<nil>"]];
};
// Add all the attributes needed in the description.
AddKeyValue(@"AX", [element isAccessibilityElement] ? @"Y": @"N");
AddKeyValue(@"AXID", [element accessibilityIdentifier]);
NSString *stringValue = [element accessibilityLabel];
AddKeyValue(@"AXLabel", stringValue ? [NSString stringWithFormat:@"\"%@\"", stringValue] : nil);
stringValue = [NSString
stringWithFormat:@"(UIAccessibilityTrait)%@",
[self gtx_stringValueOfUIAccessibilityTraits:[element accessibilityTraits]]];
AddKeyValue(@"AXTraits", stringValue);
AddKeyValue(@"AXFrame", NSStringFromCGRect([element accessibilityFrame]));
stringValue = [element accessibilityHint];
AddKeyValue(@"AXHint", stringValue ? [NSString stringWithFormat:@"\"%@\"", stringValue] : nil);
return [descriptions componentsJoinedByString:@" "];
}
/**
* @return The UIAccessibilityTraits to NSString mapping dictionary as type
* NSDictionary<NSNumber *, NSString *> *.
*/
+ (NSDictionary<NSNumber *, NSString *> const *)gtx_traitsToStringDictionary {
// Each element below is an valid accessibility traits entity.
return @{@(UIAccessibilityTraitNone): @"None",
@(UIAccessibilityTraitButton): @"Button",
@(UIAccessibilityTraitLink): @"Link",
@(UIAccessibilityTraitSearchField): @"SearchField",
@(UIAccessibilityTraitImage): @"Image",
@(UIAccessibilityTraitSelected): @"Selected",
@(UIAccessibilityTraitPlaysSound): @"PlaysSound",
@(UIAccessibilityTraitKeyboardKey): @"KeyboardKey",
@(UIAccessibilityTraitStaticText): @"StaticText",
@(UIAccessibilityTraitSummaryElement): @"SummaryElement",
@(UIAccessibilityTraitNotEnabled): @"NotEnabled",
@(UIAccessibilityTraitUpdatesFrequently): @"UpdatesFrequently",
@(UIAccessibilityTraitStartsMediaSession): @"StartsMediaSession",
@(UIAccessibilityTraitAdjustable): @"Adjustable",
@(UIAccessibilityTraitAllowsDirectInteraction): @"AllowsDirectInteraction",
@(UIAccessibilityTraitCausesPageTurn): @"CausesPageTurn",
@(UIAccessibilityTraitHeader): @"Header"
};
}
/**
* @return The NSString value of the specified accessibility traits.
*/
+ (NSString *)gtx_stringValueOfUIAccessibilityTraits:(UIAccessibilityTraits)traits {
if (traits == UIAccessibilityTraitNone) {
return @"None";
}
NSMutableArray<NSString *> *traitsStrings = [[NSMutableArray alloc] init];
const NSDictionary<NSNumber *, NSString *> *traitsToString = [self gtx_traitsToStringDictionary];
for (NSNumber *trait in traitsToString.allKeys) {
if (traits & [trait unsignedLongLongValue]) {
[traitsStrings addObject:traitsToString[trait]];
}
}
return [traitsStrings componentsJoinedByString:@","];
}
/**
* @return an image of the current screen.
*/
+ (UIImage *)gtx_takeScreenshot {
UIGraphicsBeginImageContextWithOptions([UIScreen mainScreen].bounds.size,
YES,
[UIScreen mainScreen].scale);
for (UIWindow *window in [UIApplication sharedApplication].windows.reverseObjectEnumerator) {
[window drawViewHierarchyInRect:window.bounds afterScreenUpdates:NO];
}
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return screenshot;
}
/**
* @return a gray scale version of the provided @c image.
*/
+ (UIImage *)gtx_grayScaleImageFromImage:(UIImage *)image {
// Create a bitmap context with gray scale color space.
CGColorSpaceRef grayScaleColorSpace = CGColorSpaceCreateDeviceGray();
CGContextRef context =
CGBitmapContextCreate(NULL, (size_t)image.size.width, (size_t)image.size.height,
8, 0, grayScaleColorSpace, (uint32_t)kCGImageAlphaNone);
// Render the given image into it and create a UIImage with the current contents.
CGContextDrawImage(context, CGRectMake(0, 0, image.size.width, image.size.height),
[image CGImage]);
CGImageRef grayScaleCGImage = CGBitmapContextCreateImage(context);
UIImage *grayScaleUIImage = [UIImage imageWithCGImage:grayScaleCGImage];
// Release all the created CF Objects.
CGColorSpaceRelease(grayScaleColorSpace);
CGContextRelease(context);
CFRelease(grayScaleCGImage);
return grayScaleUIImage;
}
@end