diff --git a/DeviceAgent.xcodeproj/project.pbxproj b/DeviceAgent.xcodeproj/project.pbxproj index 691f55dd..9a31629c 100644 --- a/DeviceAgent.xcodeproj/project.pbxproj +++ b/DeviceAgent.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ /* Begin PBXBuildFile section */ 0AA3924F23279A68000E799B /* SpringBoardAlertsCurrentLanguageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0AA3924D23279A68000E799B /* SpringBoardAlertsCurrentLanguageTests.m */; }; 1A020FC02338BEB600D79E57 /* XCTest+CBXAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = F547174D204939DA0024AA0B /* XCTest+CBXAdditions.h */; }; + 3BE0245123F2E66D0052DD40 /* QuerySpecifierByPredicate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE0245023F2E66D0052DD40 /* QuerySpecifierByPredicate.m */; }; 4107F8FE231D7298003961AF /* Resources.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4107F8FC231D7262003961AF /* Resources.xcassets */; }; 41188FEA22E9958D0012886A /* XCWebViews.m in Sources */ = {isa = PBXBuildFile; fileRef = 4166C9AE22E7009800C8BEBF /* XCWebViews.m */; }; 419BE54B231E46D800DF0ABD /* Resources.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4107F8FC231D7262003961AF /* Resources.xcassets */; }; @@ -750,6 +751,8 @@ /* Begin PBXFileReference section */ 0AA3924D23279A68000E799B /* SpringBoardAlertsCurrentLanguageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = SpringBoardAlertsCurrentLanguageTests.m; sourceTree = ""; }; + 3BE0244F23F2E5810052DD40 /* QuerySpecifierByPredicate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QuerySpecifierByPredicate.h; sourceTree = ""; }; + 3BE0245023F2E66D0052DD40 /* QuerySpecifierByPredicate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QuerySpecifierByPredicate.m; sourceTree = ""; }; 4107F8FC231D7262003961AF /* Resources.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Resources.xcassets; sourceTree = ""; }; 4166C9AE22E7009800C8BEBF /* XCWebViews.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XCWebViews.m; sourceTree = ""; }; 634244EC948D56732C2565E5 /* SpringBoardAlerts.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.objc; path = SpringBoardAlerts.m; sourceTree = ""; tabWidth = 4; usesTabs = 0; }; @@ -2144,6 +2147,8 @@ 899696FE1CB5C93400BB42E2 /* QuerySpecifierByCoordinate.m */, 89B9519F1CF5B297007FD0AB /* QuerySpecifierByType.h */, 89B951A01CF5B297007FD0AB /* QuerySpecifierByType.m */, + 3BE0244F23F2E5810052DD40 /* QuerySpecifierByPredicate.h */, + 3BE0245023F2E66D0052DD40 /* QuerySpecifierByPredicate.m */, F536799F1D7C324E009956D0 /* QuerySpecifierByMark.h */, F53679A01D7C324E009956D0 /* QuerySpecifierByMark.m */, ); @@ -3309,6 +3314,7 @@ 899696D41CB44D9B00BB42E2 /* GestureConfiguration.m in Sources */, 899696EB1CB5857C00BB42E2 /* JSONKeyValidator.m in Sources */, F55F81981C6DD07500A945C8 /* MultipartMessageHeader.m in Sources */, + 3BE0245123F2E66D0052DD40 /* QuerySpecifierByPredicate.m in Sources */, F5538B8B1E28BEB9003EC5F3 /* CBXLogging.m in Sources */, 899697061CB5D5CF00BB42E2 /* Gesture+Options.m in Sources */, 89C5F35F1C9C51680093A018 /* Query.m in Sources */, diff --git a/Server/AutomationActions/Query/Specifiers/QuerySpecifierByPredicate.h b/Server/AutomationActions/Query/Specifiers/QuerySpecifierByPredicate.h new file mode 100644 index 00000000..c0b50ac4 --- /dev/null +++ b/Server/AutomationActions/Query/Specifiers/QuerySpecifierByPredicate.h @@ -0,0 +1,20 @@ + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +#import +#import "QuerySpecifier.h" + +/** + Specify elements by matchingPredicate (see XCUIElementQuery.h for all predicate methods). + + The predicate will be evaluated against objects of type id. + + This specifier matches any elements belonging to the provided predicate. + + ## Usage: + + { "predicate_string" : "label MATCHES '(Safari|News)' AND elementType == 'StaticText'" } + */ +@interface QuerySpecifierByPredicate : QuerySpecifier +@end diff --git a/Server/AutomationActions/Query/Specifiers/QuerySpecifierByPredicate.m b/Server/AutomationActions/Query/Specifiers/QuerySpecifierByPredicate.m new file mode 100644 index 00000000..d7fe3ed6 --- /dev/null +++ b/Server/AutomationActions/Query/Specifiers/QuerySpecifierByPredicate.m @@ -0,0 +1,83 @@ + +#import "QuerySpecifierByPredicate.h" +#import "JSONUtils.h" +#import "CBXException.h" +#import "CBX-XCTest-Umbrella.h" + +@implementation QuerySpecifierByPredicate + ++ (NSString *)name { return @"predicate_string"; } + +/* The method uses matchingPredicate to find UI elements by any type of predicate. Please + * The general form looks like: + * 1. Using XCUIElementAttributes: identifier, title, label, value, elementType, enabled, selected, hasFocus, placeholderValue + * Please, see XCUIElementAttributes protocol to check all the properties. + * 2. Using AND/OR logic to construct compound predicates directly: "label MATCHES '(Safari|News)' AND elementType == 'StaticText'". + * 3. Using any regular expressions: MATCHES, CONTAINS, ... . + * + * The full information about predicates and how to construct it can be found in official Apple source as: + * 1. 'Predicate Programming Guide': https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html#//apple_ref/doc/uid/TP40001789 + * 2. NSRegularExpression documentation: https://developer.apple.com/documentation/foundation/nsregularexpression?language=objc + * + * In this method, we replace only incoming element type on XCUIElementType int value. + * All the rest things in predicate should be constructed as normal predicate regarding guidelines from the document above. + */ +- (XCUIElementQuery *)applyInternal:(XCUIElementQuery *)query { + NSString *actualPredicateString = self.value; + NSString *resultPredicateString = nil; + + NSString *pattern = @"(.*)elementType(.*)"; + NSRange range = [actualPredicateString rangeOfString:pattern options:NSRegularExpressionSearch]; + + if (range.location != NSNotFound) { + NSLog(@"Predicate '%@' contains elementType. Proceeed with replacement of XCUIElementType String value on int value.", actualPredicateString); + + NSString *matchPattern = @"elementType == '(\\w+)'"; + NSRange matchRange = [actualPredicateString rangeOfString:matchPattern options:NSRegularExpressionSearch]; + + if (matchRange.location != NSNotFound) { + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:matchPattern options:0 error:nil]; + NSString *actualMatch = [actualPredicateString substringWithRange:matchRange]; + NSString *replacementString = [self replacementStringForElementType:actualMatch]; + + resultPredicateString = [regex stringByReplacingMatchesInString:actualPredicateString options:0 range:NSMakeRange(0, [actualPredicateString length]) withTemplate:replacementString]; + NSLog(@"Replace original predicate '%@' with the replacement: %@", actualPredicateString, resultPredicateString); + } else { + @throw [CBXException withFormat:@"Can not parse element type. Actual string: '%@'. Expected type predicate form as: elementType == 'Button'", self.value]; + } + } else { + resultPredicateString = actualPredicateString; + NSLog(@"Predicate does not contain elementType attribute. Proceeed with the original value: %@", resultPredicateString); + } + + NSPredicate *predicate = [NSPredicate predicateWithFormat:resultPredicateString]; + + return [query matchingPredicate:predicate]; +} + +/* + * Replace any occurences of XCUIElementType from string value with single quotes into int value. + * The method return the original string with this replacement + * + * For example: + * - incoming method argument as simple predicate of String type: original = "elementType == 'StaticText'" + * - returns replacement string as predicate as well: replacementString = "elementType == 48" + */ +- (NSString *)replacementStringForElementType:(NSString *)original { + // Find any text within single quotes + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"'(.*)'" options:0 error:NULL]; + NSTextCheckingResult *match = [regex firstMatchInString:original options:0 range:NSMakeRange(0, [original length])]; + NSString *shortTypeName = [original substringWithRange:[match rangeAtIndex:1]]; + NSLog(@"Found element type '%@' in predicate string: %@", shortTypeName, original); + + // Replace String type occurence on int + NSUInteger type = [JSONUtils elementTypeForString:shortTypeName]; + NSLog(@"Replace element type '%@' into int: %i", shortTypeName, (unsigned int)type); + + // Provide new simple predicate with int element type + NSString *replacementString = [NSString stringWithFormat:@"elementType == %i", (unsigned int)type]; + + return replacementString; +} + +@end