Skip to content

Commit

Permalink
Merge pull request calabash#376 from agileseph/support_descendant_mat…
Browse files Browse the repository at this point in the history
…cher

[Support] add descendant matching query specifier
  • Loading branch information
jmoody authored Jun 29, 2020
2 parents b04a389 + 2f0c2c5 commit 0a8e030
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 0 deletions.
16 changes: 16 additions & 0 deletions DeviceAgent.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -753,6 +758,9 @@

/* Begin PBXFileReference section */
0AA3924D23279A68000E799B /* SpringBoardAlertsCurrentLanguageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = SpringBoardAlertsCurrentLanguageTests.m; sourceTree = "<group>"; };
3B5444E02498216000532AE0 /* QuerySpecifierTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QuerySpecifierTests.m; sourceTree = "<group>"; };
3B81F693240E937F00825603 /* QuerySpecifierByDescendantType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QuerySpecifierByDescendantType.h; sourceTree = "<group>"; };
3B81F694240E952F00825603 /* QuerySpecifierByDescendantType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QuerySpecifierByDescendantType.m; sourceTree = "<group>"; };
4107F8FC231D7262003961AF /* Resources.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Resources.xcassets; sourceTree = "<group>"; };
4166C9AE22E7009800C8BEBF /* XCWebViews.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XCWebViews.m; sourceTree = "<group>"; };
634244EC948D56732C2565E5 /* SpringBoardAlerts.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.objc; path = SpringBoardAlerts.m; sourceTree = "<group>"; tabWidth = 4; usesTabs = 0; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -2267,6 +2277,7 @@
F58D27E81D4F947F000FF6C0 /* Queries */ = {
isa = PBXGroup;
children = (
3B5444E02498216000532AE0 /* QuerySpecifierTests.m */,
F58D27EA1D4F947F000FF6C0 /* CoordinateQueryTests.m */,
F58D27EB1D4F947F000FF6C0 /* Factory */,
F58D27ED1D4F947F000FF6C0 /* QuerySelectors */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

#import <Foundation/Foundation.h>
#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<QuerySpecifier>
@end
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions Server/CBXConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#import <XCTest/XCTest.h>
#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
9 changes: 9 additions & 0 deletions cucumber/features/query.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
6 changes: 6 additions & 0 deletions cucumber/features/steps/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 0a8e030

Please sign in to comment.