diff --git a/DeviceAgent.xcodeproj/project.pbxproj b/DeviceAgent.xcodeproj/project.pbxproj index d1342a2ed..b8dbdd5e4 100644 --- a/DeviceAgent.xcodeproj/project.pbxproj +++ b/DeviceAgent.xcodeproj/project.pbxproj @@ -47,6 +47,11 @@ /* 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 */; }; + 3B5444E52498241500532AE0 /* QuerySpecifierByDescendantType.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B81F693240E937F00825603 /* QuerySpecifierByDescendantType.h */; }; + 3B5444EA249838AC00532AE0 /* QuerySpecifierTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B5444E02498216000532AE0 /* QuerySpecifierTests.m */; }; + 3B81F695240E952F00825603 /* QuerySpecifierByDescendantType.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B81F694240E952F00825603 /* QuerySpecifierByDescendantType.m */; }; + 3B81F696240E952F00825603 /* QuerySpecifierByDescendantType.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B81F694240E952F00825603 /* QuerySpecifierByDescendantType.m */; }; + 3BE3ED1E2498CF0700830B19 /* XCTest+CBXAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = F547174F20496A200024AA0B /* XCTest+CBXAdditions.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 */; }; @@ -753,6 +758,9 @@ /* Begin PBXFileReference section */ 0AA3924D23279A68000E799B /* SpringBoardAlertsCurrentLanguageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = SpringBoardAlertsCurrentLanguageTests.m; sourceTree = ""; }; + 3B5444E02498216000532AE0 /* QuerySpecifierTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QuerySpecifierTests.m; sourceTree = ""; }; + 3B81F693240E937F00825603 /* QuerySpecifierByDescendantType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QuerySpecifierByDescendantType.h; sourceTree = ""; }; + 3B81F694240E952F00825603 /* QuerySpecifierByDescendantType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QuerySpecifierByDescendantType.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; }; @@ -2153,6 +2161,8 @@ 89D5380F1CA34CBE00F62E09 /* QuerySpecifierByTextLike.m */, 899696FD1CB5C93400BB42E2 /* QuerySpecifierByCoordinate.h */, 899696FE1CB5C93400BB42E2 /* QuerySpecifierByCoordinate.m */, + 3B81F693240E937F00825603 /* QuerySpecifierByDescendantType.h */, + 3B81F694240E952F00825603 /* QuerySpecifierByDescendantType.m */, 89B9519F1CF5B297007FD0AB /* QuerySpecifierByType.h */, 89B951A01CF5B297007FD0AB /* QuerySpecifierByType.m */, F536799F1D7C324E009956D0 /* QuerySpecifierByMark.h */, @@ -2267,6 +2277,7 @@ F58D27E81D4F947F000FF6C0 /* Queries */ = { isa = PBXGroup; children = ( + 3B5444E02498216000532AE0 /* QuerySpecifierTests.m */, F58D27EA1D4F947F000FF6C0 /* CoordinateQueryTests.m */, F58D27EB1D4F947F000FF6C0 /* Factory */, F58D27ED1D4F947F000FF6C0 /* QuerySelectors */, @@ -2501,6 +2512,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 3B5444E52498241500532AE0 /* QuerySpecifierByDescendantType.h in Headers */, F54C02B02043F0EF00FD8DDE /* XCTElementFilteringTransformer.h in Headers */, F54C027E2043F0EF00FD8DDE /* XCTKVOExpectation.h in Headers */, F54C02502043F0EF00FD8DDE /* XCUIApplicationProcess.h in Headers */, @@ -3356,6 +3368,7 @@ F55F81A41C6DD07500A945C8 /* HTTPFileResponse.m in Sources */, 896586831CEB9B9800E8329C /* QueryFactory.m in Sources */, F55F81941C6DD07500A945C8 /* HTTPServer.m in Sources */, + 3B81F695240E952F00825603 /* QuerySpecifierByDescendantType.m in Sources */, 89D538161CA351F400F62E09 /* QuerySpecifierByIndex.m in Sources */, 89B951A21CF5B297007FD0AB /* QuerySpecifierByType.m in Sources */, F5870B401CF0BEFF00B3376C /* CBXTouchEvent.m in Sources */, @@ -3434,6 +3447,7 @@ buildActionMask = 2147483647; files = ( F5061A8A2153B19900B85792 /* QueryConfigurationFactory.m in Sources */, + 3BE3ED1E2498CF0700830B19 /* XCTest+CBXAdditions.m in Sources */, F5061A742153B19900B85792 /* Testmanagerd.m in Sources */, F5061A512153B0FF00B85792 /* SpringBoardAlertsTest.m in Sources */, F5061A9C2153B4EF00B85792 /* HTTPAuthenticationRequest.m in Sources */, @@ -3461,6 +3475,7 @@ F5061AAC2153B4EF00B85792 /* RouteRequest.m in Sources */, F5061A682153B15E00B85792 /* ElementNotFoundException.m in Sources */, F5061A4C2153B0FF00B85792 /* CBXDecimalRounderTest.m in Sources */, + 3B81F696240E952F00825603 /* QuerySpecifierByDescendantType.m in Sources */, F5061A642153B15E00B85792 /* SpringBoard.m in Sources */, F5061A4E2153B0FF00B85792 /* CBXDeviceTest.m in Sources */, F5061A522153B0FF00B85792 /* CBXOrientationTest.m in Sources */, @@ -3503,6 +3518,7 @@ F5061A812153B19900B85792 /* QuerySpecifierByPropertyLike.m in Sources */, F5061A592153B14400B85792 /* ThreadUtils.m in Sources */, F5061A422153B0FF00B85792 /* QueryConfigurationFactoryTests.m in Sources */, + 3B5444EA249838AC00532AE0 /* QuerySpecifierTests.m in Sources */, F5061A502153B0FF00B85792 /* SpringBoardAlertTest.m in Sources */, F5061A582153B14400B85792 /* GeometryUtils.m in Sources */, F5061A782153B19900B85792 /* (null) in Sources */, diff --git a/Server/AutomationActions/Query/Specifiers/QuerySpecifierByDescendantType.h b/Server/AutomationActions/Query/Specifiers/QuerySpecifierByDescendantType.h new file mode 100644 index 000000000..d883f16b8 --- /dev/null +++ b/Server/AutomationActions/Query/Specifiers/QuerySpecifierByDescendantType.h @@ -0,0 +1,16 @@ + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +#import +#import "QuerySpecifier.h" + +/** + This specifier finds all descendants elements matching the descendant_type within the element matching the parent_type. + + ## Usage: + + { "descendant_element" : { "parent_type": "String", "descendant_type": "String" } } + */ +@interface QuerySpecifierByDescendantType : QuerySpecifier +@end diff --git a/Server/AutomationActions/Query/Specifiers/QuerySpecifierByDescendantType.m b/Server/AutomationActions/Query/Specifiers/QuerySpecifierByDescendantType.m new file mode 100644 index 000000000..b2d35664e --- /dev/null +++ b/Server/AutomationActions/Query/Specifiers/QuerySpecifierByDescendantType.m @@ -0,0 +1,35 @@ +#import "QuerySpecifierByDescendantType.h" +#import "JSONUtils.h" + +@implementation QuerySpecifierByDescendantType ++ (NSString *)name { return @"descendant_element"; } + +- (XCUIElementQuery *)applyInternal:(XCUIElementQuery *)query { + NSString *error_message = [NSString stringWithFormat:@"%@%@%@%@", + @"Malformed descendant_element value. ", + @"Expected dictionary like: ", + @"{ 'descendant_type' = Button; 'parent_type' = Keyboard; }", + [NSString stringWithFormat:@"but actual is '%@'", self.value]]; + NSAssert([self.value isKindOfClass:[NSDictionary class]], error_message); + + NSDictionary *parameters = self.value; + + NSAssert(parameters.count == 2, + @"Dictionary should have only 2 keys '%@' and '%@', but actual is '%@'", + CBX_PARENT_TYPE_KEY, CBX_DESCENDANT_TYPE_KEY, parameters); + NSAssert(parameters[CBX_PARENT_TYPE_KEY] != nil, + @"Value for key '%@' should not be nil. Actual dictionary: %@", + CBX_PARENT_TYPE_KEY, parameters); + NSAssert(parameters[CBX_DESCENDANT_TYPE_KEY] != nil, + @"Value for key '%@' should not be nil. Actual dictionary: %@", + CBX_DESCENDANT_TYPE_KEY, parameters); + + XCUIElementType parentType = [JSONUtils elementTypeForString:parameters[CBX_PARENT_TYPE_KEY]]; + XCUIElementQuery *contextQuery = [query matchingType:parentType identifier:nil]; + + XCUIElementType descendantType = [JSONUtils elementTypeForString:parameters[CBX_DESCENDANT_TYPE_KEY]]; + XCUIElementQuery *resultQuery = [contextQuery descendantsMatchingType:descendantType]; + + return resultQuery; +} +@end diff --git a/Server/CBXCUITestServer.m b/Server/CBXCUITestServer.m index c2b4011e8..7995acfdd 100644 --- a/Server/CBXCUITestServer.m +++ b/Server/CBXCUITestServer.m @@ -21,7 +21,7 @@ - (id)init_private; @implementation CBXCUITestServer -+ (NSString*)valueFromArguments: (NSArray *)arguments forKey: (NSString*)key ++ (NSString *)valueFromArguments: (NSArray *)arguments forKey: (NSString *)key { NSUInteger index = [arguments indexOfObject:key]; if (index == NSNotFound || index == arguments.count - 1) { diff --git a/Server/CBXConstants.h b/Server/CBXConstants.h index 928657021..cdcc8d405 100644 --- a/Server/CBXConstants.h +++ b/Server/CBXConstants.h @@ -61,6 +61,8 @@ static NSString *const CBX_HAS_KEYBOARD_FOCUS_KEY = @"has_keyboard_focus"; static NSString *const CBX_HITABLE_KEY = @"hitable"; static NSString *const CBX_HIT_POINT_KEY = @"hit_point"; static NSString *const CBX_INDEX_KEY = @"index"; +static NSString *const CBX_PARENT_TYPE_KEY = @"parent_type"; +static NSString *const CBX_DESCENDANT_TYPE_KEY = @"descendant_type"; static NSString *const CBX_PROPERTY_KEY = @"property"; static NSString *const CBX_PROPERTY_LIKE_KEY = @"property_like"; static NSString *const CBX_TEST_ID = @"test_id"; diff --git a/TestApp/DeviceAgentUnitTests/AutomationActions/Queries/QuerySpecifierTests.m b/TestApp/DeviceAgentUnitTests/AutomationActions/Queries/QuerySpecifierTests.m new file mode 100644 index 000000000..1378b813a --- /dev/null +++ b/TestApp/DeviceAgentUnitTests/AutomationActions/Queries/QuerySpecifierTests.m @@ -0,0 +1,170 @@ +#import +#import "QueryConfiguration.h" +#import "QueryFactory.h" +#import "QuerySpecifier.h" +#import "Application.h" +#import "CBXConstants.h" +#import "XCTest+CBXAdditions.h" +#import "CBXServerUnitTestUmbrellaHeader.h" + +@interface QuerySpecifierTests : XCTestCase +@end + +@implementation QuerySpecifierTests + +- (void)setUp { + [super setUp]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testQuerySpecifierByDescendantTypeThrowsExceptionForInvalidMainKeyCase { + id invalidJson = @{@"descendantElement": @{@"parent_type" : @"Keyboard", + @"descendant_type" : @"Button"}}; + + expect(^{ + [QueryConfiguration withJSON:invalidJson validator:nil]; + }).to.raise(@"InvalidArgumentException"); +} + +- (void)testQuerySpecifierByDescendantTypeThrowsExceptionForInvalidParentKeyCase { + // [Set Up] Stage 0: prepare query with invalid config + id invalidJson = @{@"descendant_element": @{@"parent_typeeeee" : @"Keyboard", + @"descendant_type" : @"Button"}}; + QueryConfiguration *queryConfig = [QueryConfiguration withJSON:invalidJson validator:nil]; + + Query *query = [QueryFactory queryWithQueryConfiguration:queryConfig]; + + // [Preconditions] Stage 1: prepare mock objects for app and query + id appMock = OCMClassMock([Application class]); + id uiAppMock = OCMClassMock([XCUIApplication class]); + + id queryMock = OCMClassMock([XCUIElementQuery class]); + + // [Preconditions] Stage 1: mock methods called in [query execute] + OCMStub([uiAppMock cbxQueryForDescendantsOfAnyType]).andReturn(queryMock); + OCMStub([appMock currentApplication]).andReturn(uiAppMock); + + // [Check] Stage 2: throw Exception in case parent type key is malformed + expect(^{ + [query execute]; + }).to.raise(@"NSInternalInconsistencyException"); +} + +- (void)testQuerySpecifierByDescendantTypeThrowsExceptionForInvalidDescendantKeyCase { + // [Set Up] Stage 0: prepare query with invalid config + id invalidJson = @{@"descendant_element": @{@"parent_type" : @"Keyboard", + @"descendantType" : @"Button"}}; + QueryConfiguration *queryConfig = [QueryConfiguration withJSON:invalidJson validator:nil]; + + Query *query = [QueryFactory queryWithQueryConfiguration:queryConfig]; + + // [Preconditions] Stage 1: prepare mock objects for app and query + id appMock = OCMClassMock([Application class]); + id uiAppMock = OCMClassMock([XCUIApplication class]); + + id queryMock = OCMClassMock([XCUIElementQuery class]); + + // [Preconditions] Stage 1: mock methods called in [query execute] + OCMStub([uiAppMock cbxQueryForDescendantsOfAnyType]).andReturn(queryMock); + OCMStub([appMock currentApplication]).andReturn(uiAppMock); + + // [Check] Stage 2: throw Exception in case descendant type key is malformed + expect(^{ + [query execute]; + }).to.raise(@"NSInternalInconsistencyException"); +} + +- (void)testQuerySpecifierByDescendantTypeThrowsExceptionForInvalidMissingKeyCase { + // [Set Up] Stage 0: prepare query with invalid config + id invalidJson = @{@"descendant_element": @{@"parent_type" : @"Keyboard" }}; + QueryConfiguration *queryConfig = [QueryConfiguration withJSON:invalidJson validator:nil]; + + Query *query = [QueryFactory queryWithQueryConfiguration:queryConfig]; + + // [Preconditions] Stage 1: prepare mock objects for app and query + id appMock = OCMClassMock([Application class]); + id uiAppMock = OCMClassMock([XCUIApplication class]); + + id queryMock = OCMClassMock([XCUIElementQuery class]); + + // [Preconditions] Stage 1: mock methods called in [query execute] + OCMStub([uiAppMock cbxQueryForDescendantsOfAnyType]).andReturn(queryMock); + OCMStub([appMock currentApplication]).andReturn(uiAppMock); + + // [Check] Stage 2: throw Exception in case missed descendant_type key + expect(^{ + [query execute]; + }).to.raise(@"NSInternalInconsistencyException"); +} + +- (void)testQuerySpecifierByDescendantTypeThrowsExceptionForInvalidParentTypeValueCase { + // [Set Up] Stage 0: prepare query with invalid config + id invalidJson = @{@"descendant_element": @{@"parent_type" : @"UIKeyboard", + @"descendant_type" : @"Button"}}; + QueryConfiguration *queryConfig = [QueryConfiguration withJSON:invalidJson validator:nil]; + + Query *query = [QueryFactory queryWithQueryConfiguration:queryConfig]; + + // [Preconditions] Stage 1: prepare mock objects for app and query + id appMock = OCMClassMock([Application class]); + id uiAppMock = OCMClassMock([XCUIApplication class]); + + id queryMock = OCMClassMock([XCUIElementQuery class]); + + // [Preconditions] Stage 1: mock methods called in [query execute] + OCMStub([uiAppMock cbxQueryForDescendantsOfAnyType]).andReturn(queryMock); + OCMStub([appMock currentApplication]).andReturn(uiAppMock); + + // [Check] Stage 2: throw Exception in case UIElement type value is invalid + expect(^{ + [query execute]; + }).to.raise(@"CBXException"); +} + +- (void)testQuerySpecifierByDescendantTypeReturnsArrayOfElementsForValidCase { + // [Set Up] Stage 0: prepare query with valid config + id validJson = @{@"descendant_element": @{@"parent_type" : @"Keyboard", + @"descendant_type" : @"Button"}}; + QueryConfiguration *queryConfig = [QueryConfiguration withJSON:validJson validator:nil]; + + Query *query = [QueryFactory queryWithQueryConfiguration:queryConfig]; + + // [Preconditions] Stage 1: prepare mock objects for app and query + // (we want mock interactions with real app object and + // check specific implementation of applyInternal for QuerySpecifierByDescendantType) + id appMock = OCMClassMock([Application class]); + id uiAppMock = OCMClassMock([XCUIApplication class]); + id element = OCMClassMock([XCUIElement class]); + id arrayOfElements = [NSArray arrayWithObject:element]; + + id queryMock = OCMClassMock([XCUIElementQuery class]); + id contextQueryMock = OCMClassMock([XCUIElementQuery class]); + id resultQueryMock = OCMClassMock([XCUIElementQuery class]); + + // [Preconditions] Stage 1: mock methods called in [query execute] + OCMStub([uiAppMock cbxQueryForDescendantsOfAnyType]).andReturn(queryMock); + OCMStub([appMock currentApplication]).andReturn(uiAppMock); + + // [Expectations] Stage 2: set up expectations for [specifier applyInternal:query] + // where specifier is QuerySpecifierByDescendantType + XCUIElementType keyboard = XCUIElementTypeKeyboard; + XCUIElementType button = XCUIElementTypeButton; + OCMExpect([queryMock matchingType:keyboard identifier:nil]).andReturn(contextQueryMock); + OCMExpect([contextQueryMock descendantsMatchingType:button]).andReturn(resultQueryMock); + // [Expectations] Stage 2: set up expectations for [query execute] + OCMExpect([resultQueryMock allElementsBoundByIndex]).andReturn(arrayOfElements); + + // [Test Step] Stage 3: perform test step + [query execute]; + + // [Check] Stage 4: verify [specifier applyInternal:query] + OCMVerifyAll(queryMock); + OCMVerifyAll(contextQueryMock); + // [Check] Stage 4: verify [query execute] + OCMVerifyAll(resultQueryMock); +} + +@end diff --git a/cucumber/features/query.feature b/cucumber/features/query.feature index de656b77e..9fddf2611 100644 --- a/cucumber/features/query.feature +++ b/cucumber/features/query.feature @@ -78,3 +78,12 @@ Then I query the text field using "marked" with string "Schreib!" and see value Then I query the text field using "marked" with string "Hello!" and see value "Hello!" Then I query the text field using "text" with string "Schreib!" and see value "Hello!" Then I query the text field using "text" with string "Hello!" and see value "Hello!" + +@query +Scenario: Descendant query returns array of elements +Given I am looking at the Text Input with placeholder +And I touch the text field +When the keyboard is visible +Then I query the keyboard using "descendant_element" and see keyboard buttons + | parent_type | descendant_type | + | Keyboard | Button | diff --git a/cucumber/features/steps/query.rb b/cucumber/features/steps/query.rb index e0af34a7c..73d874d73 100644 --- a/cucumber/features/steps/query.rb +++ b/cucumber/features/steps/query.rb @@ -316,3 +316,9 @@ def newlines_in_queries_supported? expect(actual["value"]).to be == value end + +Then(/^I query the keyboard using "descendant_element" and see keyboard buttons$/) do |table| + locator = {descendant_element: table.hashes.first} + actual_keyboard_buttons = wait_for_view(locator) + expect(actual_keyboard_buttons.count).to be > 1 +end