-
-
Notifications
You must be signed in to change notification settings - Fork 187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: native CSS selector for Android UiAutomator #410
Conversation
lib/css-converter.js
Outdated
* | ||
* @param {*} str | ||
*/ | ||
function escapeRegexLiterals (str) { |
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.
lodash already has a helper to escape regex
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.
Found this: https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript ... copied and pasted. I couldn't find lodash version of this.
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.
* Convert a CSS rule to a UiSelector | ||
* @param {*} cssRule CSS rule definition | ||
*/ | ||
function parseCssRule (cssRule) { |
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 think it makes sense to have a class for such transformation rather than a set of functions.
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.
@mykola-mokhnach I prefer the procedural approach myself, but that's a much larger debate.
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.
having a class could help to avoid passing the css object (and possibly many other shared args) to all the functions
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.
also, this could be refactored to have a main class that performs css expression validation and tokenization and then passing a tranformer class/function to it. This class/function could be responsible for the expression tranformation itself to, let say, UiSelector or class chain expression. This way we could extract the common part of the code and possibly reuse it by moving to appium-support module. So, only the driver-specific transformation method would stay in the driver source.
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 chose this approach because of my personal preference ... I find the testing simpler and I personally lean towards avoiding the cognitive overhead of thinking about "state". That is it to say, when I'm writing and troubleshooting functions, I prefer the functions to be "pure" so that I only have to think about the inputs (the args) and the output and that I don't have to think about the state of the object under test.
I still believe there are many cases where using classes/objects is the right approach. I won't be advocating that we port Appium to Haskell any time soon. I like that Javascript affords the choice between functional programming and object-oriented :)
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.
It is fine to use Haskell-based approach. For me it's more important to have it properly designed from architectural perspective. E.g. that we could easily separate different entities (tokenization, validation and transformation) and then implement the feature in different drivers without copying the same parts of code here and there
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.
For this feature, my plan is to not share any code between the drivers and just copy-paste + refactor.
Normally this is terrible coding practice, but I'm making an exception because this is based on the CSS selector standard, which isn't going to be introducing breaking changes any time soon and it's very unlikely that we'll ever need to make alterations to the code that would have to be ported to all drivers. I would rather each driver do it's own independent implementation.
@@ -0,0 +1,78 @@ | |||
import chai from 'chai'; |
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 like the tests. It would also make sense to add a documentation to this feature
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.
@mykola-mokhnach definitely. I'll add something to the appium docs and then probably an AppiumPro article.
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'm going to do the iOS and Android-Espresso versions of this next. The Espresso version is going to be awesome... a hybrid of datamatchers and viewmatchers.
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.
for sure, if you want to guest write an appium pro article, i'd love to feature it
Suggestions implemented. |
|
||
describe('css-converter.js', function () { | ||
describe('simple cases', function () { | ||
const simpleCases = [ |
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.
awesome
lib/css-converter.js
Outdated
} catch (e) { | ||
log.errorAndThrow(`Could not parse CSS '${cssSelector}'. Reason: '${e}'`); | ||
} | ||
return parseCssObject(cssObj); |
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.
what if we mix these helper function into cssObj to avoid passing cssObj argument around:
function parseRule () {
...}
....
// was function parseCssObject (css)
function parse () {
switch (this.type) {
case 'rule':
return this.parseRule();
...
}
...
cssObj.parse = parse.bind(cssObj);
cssObj.parseRule = parseRule.bind(cssObj);
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.
No object gets passed around multiple times, they only ever get passed into one function and then the result is returned.
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 think we're going to agree on this one 😄 ... I'd be happy to have a third Appium contributor be a tie-breaker.
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 want to block the PR. You've done a great work and it would be a waste not to have it merged.
@jlipps Do you have any more comments or suggestions on the PR?
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.
Thanks @mykola-mokhnach !
lib/css-converter.js
Outdated
} | ||
|
||
if (!STR_ATTRS.includes(attrName)) { | ||
return; |
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.
according to the docstring this method cannot return undefined
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 wasn't able to give the code a very close look but i think overall approach is great! i guess we could end up doing the same for iOS too.
@@ -0,0 +1,78 @@ | |||
import chai from 'chai'; |
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.
for sure, if you want to guest write an appium pro article, i'd love to feature it
@jlipps yeah my plan is to do this for Espresso (viewmatchers + datamatchers) and then iOS (class chain). |
lib/css-converter.js
Outdated
@@ -0,0 +1,319 @@ | |||
import { CssSelectorParser } from 'css-selector-parser'; | |||
import _ from 'lodash'; |
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't we just do this
import _ from 'lodash'; | |
import { escapeRegExp } from 'lodash'; |
and change the _ escapeRegExp
into escapeRegExp
like we normally do with imports?
} | ||
|
||
let uiAutomatorSelector = 'new UiSelector()'; | ||
if (cssRule.tagName && cssRule.tagName !== '*') { |
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.
Not sure how to do this differently, but I always have some issues reading multiple if|else
-statements like this
lib/css-converter.js
Outdated
|
||
// Validate that it's a supported attribute | ||
if (!STR_ATTRS.includes(attrName) && !BOOLEAN_ATTRS.includes(attrName)) { | ||
throw new Error(`'${attrName}' is not a support attribute. Supported attributes are ` + |
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.
'${attrName}' attribute is not supported
The CSS selector is going unused in all Appium Drivers 😢 . Why not put it to use?
This PR adds support for a native CSS selector strategy. Instead of using CSS to select HTML elements, it parses CSS selectors and translate the selector to a native
-android uiautomator
selector.Example
android.widget.TextView[description="Some description"]
translates to
new UiSelector().className("android.widget.TextView").description("Some description")
ie:
elementByCss('android.widget.TextView[description="Some description"]')
does the same thing as
elementBy('-android uiautomator', 'new UiSelector().className("android.widget.TextView").description("Some description")')
many more examples in css-converter-specs.js and by-css-e2e-specs.js
Versatile
This selector can be used to replace every other selector:
find by id:
elementsById("someResourceID")
-->elementsByCss("#someResourceID")
find by class name:
elementsByClassName("android.widget.TextView")
-->elementsByCss("android.widget.TextView")
find by accessibility id:
elementsByAccessibilityId("Some Content Description")
-->elementsByCss('*[description="Some Content Description"]')
find by xpath:
elementsByXpath("//android.widget.TextView[@description='Accessibility']")
-->elementsByCss("android.widget.TextView[description='Accessibility']")
Performance
Unlike Xpath, this selector doesn't require a document to be generated that the selector is applied to. It just parses the CSS selector and translates it to an analagous UiAutomator selector.
How is it done?
-android uiautomator
selectorHow is it different from CSS selectors being applied to HTML?
>
,~
, ...) are unsupported.p.classOne.classTwo
means that it's matching an HTML<p>
element that hasclassOne
andclassTwo
as class attributes (order doesn't matter).android.widget.TextView
means that it's matching a widget with the fully qualified Java class nameandroid.widget.TextView
Next Step
Support this in iOS with class chain and in Espresso with ViewMatchers and DataMatchers