Skip to content
This repository has been archived by the owner on Dec 26, 2019. It is now read-only.

Element search by 'class chain' locator type  #442

Closed
wants to merge 2 commits into from
Closed

Element search by 'class chain' locator type  #442

wants to merge 2 commits into from

Conversation

mykola-mokhnach
Copy link
Contributor

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] - select the third child button of the first child window element (indexing starts from 1. zero index is invalid, negative indexes are allowed)

XCUIElementTypeWindow - select all the children windows

XCUIElementTypeWindow[2] - select the second child window in the hierarchy. Indexing starts at 1

XCUIElementTypeWindow/XCUIElementTypeAny[3] - select the third child (of any type) of the first child window

XCUIElementTypeWindow/*[3] - same as above

XCUIElementTypeWindow[2]/XCUIElementTypeAny - select all the children of the second child window

XCUIElementTypeWindow[2]/XCUIElementTypeAny[-2] - select the second last child of the second child window

I've also implemented nice descriptions for syntax errors.

@marekcirkos
Copy link
Contributor

@mykola-mokhnach Can you email me or poke me on Facebook?

@mykola-mokhnach
Copy link
Contributor Author

@marekcirkos Sorry, but I'm not registered on FB and not gonna do it ever.
I'd be happy to contact you via email, but don't know your address.

Copy link

@jlipps jlipps left a 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!

Copy link
Contributor

@marekcirkos marekcirkos left a 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]];
Copy link
Contributor

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

Copy link
Contributor Author

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;
Copy link
Contributor

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

Copy link
Contributor Author

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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This never happens

Copy link
Contributor Author

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) {
Copy link
Contributor

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)

Copy link
Contributor Author

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

Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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)]];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Contributor Author

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NSUInteger

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NSUInteger

Copy link
Contributor Author

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
Copy link
Contributor

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 ?

Copy link
Contributor Author

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

Copy link
Contributor

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.

@mykola-mokhnach
Copy link
Contributor Author

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

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.

@mykola-mokhnach
Copy link
Contributor Author

@marekcirkos There are already unit tests for chain parser. Please check FBClassChainTests.m in UnitTests folder.

@mykola-mokhnach
Copy link
Contributor Author

@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
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instancetype instead of id

Copy link
Contributor

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..

Copy link
Contributor Author

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
Copy link
Contributor

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

Copy link
Contributor Author

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];
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

@mykola-mokhnach mykola-mokhnach Feb 3, 2017

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Contributor Author

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];
Copy link
Contributor

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.

Copy link
Contributor Author

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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix spacing

Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added and squashed

@marekcirkos
Copy link
Contributor

Travis is not happy, looks like testSpecialXPathQuery and https://travis-ci.org/facebook/WebDriverAgent/jobs/206118659 are broken.

@mykola-mokhnach
Copy link
Contributor Author

Sorry, my bad. Both should be fixed now

@facebook-github-bot
Copy link

@marekcirkos has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@marekcirkos
Copy link
Contributor

Finally! Thank you @mykola-mokhnach for your patience

@mykola-mokhnach mykola-mokhnach deleted the class_chain_search branch March 6, 2017 16:06
imurchie referenced this pull request in appium/WebDriverAgent Nov 16, 2017
* Begin to work on CI metrics

* Actively call patch function instead of passively running on require

* Remove uuid dependency
dpgraham pushed a commit to dpgraham/appium-ios that referenced this pull request Sep 9, 2020
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
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants