-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Element search by 'class chain' locator type #442
Element search by 'class chain' locator type #442
Conversation
@mykola-mokhnach Can you email me or poke me on Facebook? |
@marekcirkos Sorry, but I'm not registered on FB and not gonna do it ever. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have the obj c expertise to evaluate this code, but I like the idea!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please resolve conflicts.
- Add tests for
FBClassChainQueryParser
.
I am not a big fun of this approach. It is super sensitive to tree hierarchy changes and class name changes. Instead people should use explicit accessibility identifiers and the whole xpath stuff is not needed.
@throw [NSException exceptionWithName:FBClassChainQueryParseException reason:error.localizedDescription userInfo:error.userInfo]; | ||
return nil; | ||
} | ||
NSMutableArray *unmatchedSnapshots = [NSMutableArray arrayWithArray:[self fb_lookupChain:parsedChain root:self.fb_lastSnapshot]]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or [self fb_lookupChain:parsedChain root:self.fb_lastSnapshot].mutableCopy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
while (candidateElementIdx < candidateElements.count) { | ||
XCUIElement *candidateElement = [candidateElements objectAtIndex:candidateElementIdx]; | ||
NSUInteger snapshotIdx = 0; | ||
BOOL isMatchFound = NO; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is always false. Doesn't seem like you need it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Fixed.
break; | ||
} | ||
if (isMatchFound) { | ||
[candidateElements removeObjectAtIndex:candidateElementIdx]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This never happens
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
candidateElements = [[[candidateElements reverseObjectEnumerator] allObjects] mutableCopy]; | ||
} | ||
NSUInteger candidateElementIdx = 0; | ||
while (candidateElementIdx < candidateElements.count) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fast enumeration should be better for (element in candidateElements)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now this is OK after isMatchFound behaviour is fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not talking about speed only. Also readability will be much better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need while loop here, because the candidateElements array is changed while iteration is in progress. This won't work for for .. in loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't you simply iterate candidateElements
? This is effectively what is happening, but is really hard to follow when reading this code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[candidateElements removeObjectAtIndex:candidateElementIdx];
because of that. I cannot change the structure being currently iterated, otherwise this will corrupt my iterator.
I want to remove matched items immediately to reduce iterations count
FBClassChainElement *chainElement = [query firstObject]; | ||
NSArray *filteredChildren = root.children; | ||
if (XCUIElementTypeAny != chainElement.type) { | ||
filteredChildren = [filteredChildren filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"elementType == %@", @(chainElement.type)]]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use FBStringify
instead of plain strings, https://github.com/facebook/WebDriverAgent/blob/2eefc210ab3a775cfd1eff1b47e1a81484b64b50/WebDriverAgentLib/Commands/FBFindElementCommands.m#L77
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed. thx for the hint
{ | ||
self = [super init]; | ||
if (self) { | ||
self.asString = stringValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
return [NSCharacterSet characterSetWithCharactersInString:@""]; | ||
} | ||
|
||
+ (unsigned int)maxLength |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NSUInteger
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
|
||
@implementation FBClassChainQueryParser | ||
|
||
+ (void)tokenizationErrorWithIndex:(unsigned int)index originalQuery:(NSString *)originalQuery error:(NSError **)error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you accept an error you should return a BOOL
to indicate whether operation was successful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a helper method to initialize NSError instance with extended description. Anyway, I've changed returned result type to BOOL.
|
||
+ (nullable NSArray<Token *> *)tokenizedQueryWithQuery:(NSString*)classChainQuery error:(NSError **)error | ||
{ | ||
unsigned int queryStringLength = (unsigned int)classChainQuery.length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NSUInteger
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
@interface Token : NSObject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need FB
prefix in all of your classes. Also I would be more precise and name it something like FBClassChainToken
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an internal class, which is not used anywhere else, except of this particular implementation . Do we still need the prefix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Objc you need to always prefix class names.
Me too, but I still see many people are using terribly long xpath locators autogenerated by inspector, because either application developers are too lazy to assign identifiers or these people don't know Appium/WDA features well enough. Class chain locator usage could be kind of quick patch for lookup speed in such case. |
@marekcirkos There are already unit tests for chain parser. Please check FBClassChainTests.m in UnitTests folder. |
@marekcirkos Are there any new comments to this PR? |
Returns an array of descendants matching given class chain query. This type of queries is a subset of xpath queries, but works much faster because of the internal implementation features. | ||
|
||
@param classChainQuery requested class chain query. This query is constructed like xpath, but can only include indexes and valid class names. Only search by direct children elements of the current element is supported. Examples of such requests: | ||
XCUIElementTypeWindow/XCUIElementTypeButton[3] - select the third child button of the first child window element |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I would keep @param
short and keep juicy documentation above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
-1 is the last child element and -2 is the second last element | ||
@return FBClassChainElement instance | ||
*/ | ||
- (id) initWithType:(XCUIElementType)type position:(NSInteger)position; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instancetype instead of id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kill space after (id)
. I noticed it in few other places cross diff..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
return YES; | ||
} | ||
|
||
+ (nullable FBClassChain)compiledQueryWithTokenizedQuery:(NSArray<FBBaseClassChainToken *> *)tokenizedQuery originalQuery:(NSString *)originalQuery error:(NSError **)error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an NSArray
not FBClassChain
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+/*! Type alias for the product of class chain query parsing */
+typedef NSArray<FBClassChainElement *> * FBClassChain;
- (NSArray<XCUIElement *> *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch | ||
{ | ||
NSError *error; | ||
FBClassChain parsedChain = [FBClassChainQueryParser parseQuery:classChainQuery error:&error]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This NSArray
not FBClassChain
. FBClassChain
is undefined, I can't find any definition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+/*! Type alias for the product of class chain query parsing */
+typedef NSArray<FBClassChainElement *> * FBClassChain;
|
||
@implementation XCUIElement (FBClassChain) | ||
|
||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is really hard to read. Can you break it to smaller methods that are more descriptive?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure will do
@interface FBClassChainElement : NSObject | ||
|
||
/*! Element's position */ | ||
@property (readonly) NSInteger position; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should make all properties nonatomic
unless you really, really want them atomic
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
|
||
@implementation FBClassChainQueryParser | ||
|
||
+ (BOOL)tokenizationErrorWithIndex:(NSUInteger)index originalQuery:(NSString *)originalQuery error:(NSError **)error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not return an error here, if it is a error constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed both methods to return NSError* instead
return result.copy; | ||
} | ||
|
||
+ (BOOL)compilationErrorWithQuery:(NSString *)originalQuery description:(NSString *)description error:(NSError **)error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed both method to return NSError* instead
[self.class compilationErrorWithQuery:originalQuery description:description error:error]; | ||
return nil; | ||
} | ||
NSNumberFormatter *f = [[NSNumberFormatter alloc] init]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatters are expensive to create you should do this in constructor and reuse it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, thanks for the hint
- (void)testInvalidChains | ||
{ | ||
NSArray *invalidQueries = @[ | ||
@"/XCUIElementTypeWindow" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix spacing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
|
||
NSString *const FBClassChainQueryParseException = @"FBClassChainQueryParseException"; | ||
|
||
@implementation XCUIElement (FBClassChain) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can add fb_
prefix to all methods in that class? Let's land it then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added and squashed
Travis is not happy, looks like |
Sorry, my bad. Both should be fixed now |
@marekcirkos has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |
Finally! Thank you @mykola-mokhnach for your patience |
* Begin to work on CI metrics * Actively call patch function instead of passively running on require * Remove uuid dependency
Summary: The idea for this PR is borrowed from Adobe Flex open-source testing framework, where xpath queries are not supported natively. I did it, because I observe many people still build xpath queries, like this ``` /XCUIElementTypeApplication/XCUIElementTypeWindow/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeOther[2]/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeTable/XCUIElementTypeCell/XCUIElementTypeStaticText[2] ``` And they are quite slow, since xpath is not natively supported by XCTest. My implementation uses the same query syntax as xpath (only class names and indexes are supported though), but the search itself takes much less time in comparison to xpath, because it looks for direct child nodes only and uses single native method call (_childrenMatchingType_) for this purpose. Examples of valid class chain queries: ``` XCUIElementTypeWindow/XCUIElementTypeButton[3] - Closes facebookarchive/WebDriverAgent#442 Differential Revision: D4642444 Pulled By: marekcirkos fbshipit-source-id: 9c5e4dd884ddfde3e2f394de9a413f0015e30efb
The idea for this PR is borrowed from Adobe Flex open-source testing framework, where xpath queries are not supported natively. I did it, because I observe many people still build xpath queries, like this
And they are quite slow, since xpath is not natively supported by XCTest.
My implementation uses the same query syntax as xpath (only class names and indexes are supported though), but the search itself takes much less time in comparison to xpath, because it looks for direct child nodes only and uses single native method call (childrenMatchingType) for this purpose. Examples of valid class chain queries:
I've also implemented nice descriptions for syntax errors.