blob: 1188de3ffe69ee6fb4a8ae517f261439dcb102a9 [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 "GTXiLibCore.h"
#import "GTXAssertions.h"
#import "GTXChecking.h"
#import "GTXChecksCollection.h"
#import "GTXLogging.h"
#import "GTXPluginXCTestCase.h"
#import "GTXToolKit.h"
#import "GTXXCUIApplicationProxy.h"
#import "NSError+GTXAdditions.h"
#pragma mark - Global definitions.
double gGTXiLibVersionNumber = 4.0;
const unsigned char GTXiLibVersionString[] = "4.0";
NSString *const gtxTestCaseDidBeginNotification = @"gtxTestCaseDidBeginNotification";
NSString *const gtxTestCaseDidEndNotification = @"gtxTestCaseDidEndNotification";
NSString *const gtxTestClassUserInfoKey = @"gtxTestClassUserInfoKey";
NSString *const gtxTestInvocationUserInfoKey = @"gtxTestInvocationUserInfoKey";
NSString *const gtxTestInteractionDidBeginNotification = @"gtxTestInteractionDidBeginNotification";
NSString *const gtxTestInteractionDidEndNotification = @"gtxTestInteractionDidEndNotification";
@interface GTXInstallOptions : NSObject
@property(nonatomic, strong) NSArray *checks;
@property(nonatomic, strong) NSArray *elementExcludeList;
@property(nonatomic, strong) GTXTestSuite *suite;
@end
@implementation GTXInstallOptions
@end
/**
The GTXToolKit instance used to handle accessibility checking in GTXiLib class.
*/
static GTXToolKit *gToolkit;
/**
The an array of installation options specified by the user so far.
*/
static NSMutableArray *gIntsallOptions;
/**
The pointer to current options being used by GTXiLib.
*/
static GTXInstallOptions *gCurrentOptions;
/**
The failure handler block.
*/
static GTXiLibFailureHandler gFailureHandler;
/**
Boolean that indicates if GTXiLib has detected an on-going interaction.
*/
static BOOL gIsInInteraction;
/**
Boolean that indicates if GTXiLib has detected a test case tearDown.
*/
static BOOL gIsInTearDown;
#pragma mark - Implementation
@implementation GTXiLib
+ (void)installOnTestSuite:(GTXTestSuite *)suite
checks:(NSArray<id<GTXChecking>> *)checks
elementExcludeLists:(NSArray<id<GTXExcludeListing>> *)excludeLists {
[GTXPluginXCTestCase installPlugin];
if (!gIntsallOptions) {
gIntsallOptions = [[NSMutableArray alloc] init];
}
GTXInstallOptions *options = [[GTXInstallOptions alloc] init];
options.checks = checks;
options.elementExcludeList = excludeLists;
options.suite = suite;
// Assert that this suite has no test cases also specified in other install calls.
for (GTXInstallOptions *existing in gIntsallOptions) {
GTXTestSuite *intersection = [existing.suite intersection:suite];
NSAssert(intersection.tests.count == 0,
@"Error! Attempting to install GTXChecks multiple times on the same test cases: %@",
intersection);
(void)intersection; // Ensures 'intersection' is marked as used even if NSAssert is removed.
}
[gIntsallOptions addObject:options];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(gtx_testCaseDidBegin:)
name:gtxTestCaseDidBeginNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(gtx_testCaseDidTearDown:)
name:gtxTestCaseDidEndNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(gtx_testInteractionDidBegin:)
name:gtxTestInteractionDidBeginNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(gtx_testInteractionDidFinish:)
name:gtxTestInteractionDidEndNotification
object:nil];
});
}
+ (void)setFailureHandler:(GTXiLibFailureHandler)handler {
gFailureHandler = handler;
}
+ (GTXiLibFailureHandler)failureHandler {
return gFailureHandler ?: ^(NSError *error) {
NSString *formattedError =
[NSString stringWithFormat:@"\n\n%@\n\n", error.localizedDescription];
[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd
object:self
file:@(__FILE__)
lineNumber:__LINE__
description:@"%@", formattedError];
};
}
+ (id<GTXChecking>)checkWithName:(NSString *)name block:(GTXCheckHandlerBlock)block {
return [GTXToolKit checkWithName:name block:block];
}
#pragma mark - Private
/**
@return Array of root elements on the screen or @c nil if no application/window was found.
*/
+ (NSArray<NSObject *> *)gtx_onScreenRootElements {
if (GTXXCUIApplicationProxy.lastKnownApplicationProxy) {
// GTX is running from Out-of-process.
return [GTXXCUIApplicationProxy.lastKnownApplicationProxy gtx_onScreenAccessibilityElements];
} else {
UIWindow *window = UIApplication.sharedApplication.keyWindow;
if (window) {
return @[ window ];
}
}
return nil;
}
/**
Executes the currently installed checks on the given element. In case of failures, the failure
handler is invoked.
@param element The element on which the checks need to be executed.
@return @c NO if any of the checks failed, @c YES otherwise.
*/
+ (BOOL)gtx_checkElement:(id)element {
NSError *error;
BOOL success = [gToolkit checkElement:element error:&error];
if (error) {
self.failureHandler(error);
}
return success;
}
/**
Executes the currently installed checks on all the elements of the accessibility tree under the
given root elements. In case of failures, the failure handler is invoked.
@param rootElements An array of root elements.
@return @c NO if any of the checks failed, @c YES otherwise.
*/
+ (BOOL)gtx_checkAllElementsFromRootElements:(NSArray *)rootElements {
GTXResult *result = [gToolkit resultFromCheckingAllElementsFromRootElements:rootElements];
BOOL success = [result allChecksPassed];
if (!success) {
self.failureHandler([result aggregatedError]);
}
return success;
}
/**
Notification handler for handling gtxTestCaseDidBeginNotification.
@param notification The notification that was posted.
*/
+ (void)gtx_testCaseDidBegin:(NSNotification *)notification {
// check if new test class has started
gIsInTearDown = NO;
Class currentTestClass = notification.userInfo[gtxTestClassUserInfoKey];
SEL currentTestSelector =
((NSInvocation *)notification.userInfo[gtxTestInvocationUserInfoKey]).selector;
GTXInstallOptions *currentTestCaseOptions = nil;
for (GTXInstallOptions *options in gIntsallOptions) {
if ([options.suite hasTestCaseWithClass:currentTestClass testMethod:currentTestSelector]) {
currentTestCaseOptions = options;
break;
}
}
if (gCurrentOptions != currentTestCaseOptions) {
gCurrentOptions = currentTestCaseOptions;
if (gCurrentOptions) {
if (gCurrentOptions.checks.count > 0) {
gToolkit = [GTXToolKit toolkitWithNoChecks];
for (id<GTXChecking> check in gCurrentOptions.checks) {
[gToolkit registerCheck:check];
}
} else {
gToolkit = [GTXToolKit defaultToolkit];
}
for (id<GTXExcludeListing> excludeList in gCurrentOptions.elementExcludeList) {
[gToolkit registerExcludeList:excludeList];
}
}
}
}
/**
Notification handler for handling gtxTestCaseDidEndNotification.
@param notification The notification that was posted.
*/
+ (void)gtx_testCaseDidTearDown:(NSNotification *)notification {
if (gIsInTearDown) {
return;
}
gIsInTearDown = YES;
if (gCurrentOptions) {
// Run all the checks.
NSArray<id> *rootElements = [self gtx_onScreenRootElements];
if (rootElements) {
[self gtx_checkAllElementsFromRootElements:rootElements];
}
}
}
/**
Notification handler for handling gtxTestInteractionDidBeginNotification.
@param notification The notification that was posted.
*/
+ (void)gtx_testInteractionDidBegin:(NSNotification *)notification {
if (!gIsInInteraction) {
NSArray<id> *rootElements = [self gtx_onScreenRootElements];
if (rootElements) {
[self gtx_checkAllElementsFromRootElements:rootElements];
}
}
gIsInInteraction = YES;
}
/**
Notification handler for handling gtxTestInteractionDidEndNotification.
@param notification The notification that was posted.
*/
+ (void)gtx_testInteractionDidFinish:(NSNotification *)notification {
gIsInInteraction = NO;
}
@end