Skip to content
1 change: 1 addition & 0 deletions appium/webdriver/common/mobileby.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class MobileBy(By):
IOS_CLASS_CHAIN = '-ios class chain'
ANDROID_UIAUTOMATOR = '-android uiautomator'
ANDROID_VIEWTAG = '-android viewtag'
ANDROID_DATA_MATCHER = '-android datamatcher'
ACCESSIBILITY_ID = 'accessibility id'
IMAGE = '-image'
CUSTOM = '-custom'
105 changes: 105 additions & 0 deletions appium/webdriver/extensions/search_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# pylint: disable=abstract-method

import json

from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement

from appium.webdriver.common.mobileby import MobileBy


class BaseSearchContext(object):
"""Used by each search context. Dummy find_element/s are for preventing pylint error"""

def find_element(self, by=None, value=None):
raise NotImplementedError

def find_elements(self, by=None, value=None):
raise NotImplementedError


class AndroidSearchContext(BaseSearchContext):
"""Define search context for Android"""

def find_element_by_android_data_matcher(self, name=None, args=None, className=None):
"""Finds element by [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android
It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver).

:Args:
- name - The name of a method to invoke.
The method must return a Hamcrest
[Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html)
- args - The args provided to the method
- className - The class name that the method is part of (defaults to `org.hamcrest.Matchers`).
Can be fully qualified, or simple, and simple defaults to `androidx.test.espresso.matcher` package
(e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers`

:Returns:
An Element object

:Raises:
- TypeError - Raises a TypeError if the arguments are not validated for JSON format

:Usage:
driver.find_element_by_android_data_matcher(name='hasEntry', args=['title', 'Animation'])
"""

return self.find_element(
by=MobileBy.ANDROID_DATA_MATCHER,
value=self._build_data_matcher(name=name, args=args, className=className)
)

def find_elements_by_android_data_matcher(self, name=None, args=None, className=None):
"""Finds elements by [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android
It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver).

:Args:
- name - The name of a method to invoke.
The method must return a Hamcrest
[Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html)
- args - The args provided to the method
- className - The class name that the method is part of (defaults to `org.hamcrest.Matchers`).
Can be fully qualified, or simple, and simple defaults to `androidx.test.espresso.matcher` package
(e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers`

:Usage:
driver.find_elements_by_android_data_matcher(name='hasEntry', args=['title', 'Animation'])
"""

return self.find_elements(
by=MobileBy.ANDROID_DATA_MATCHER,
value=self._build_data_matcher(name=name, args=args, className=className)
)

def _build_data_matcher(self, name=None, args=None, className=None):
result = {}

for key, value in {'name': name, 'args': args, 'class': className}.items():
Copy link
Member Author

Choose a reason for hiding this comment

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

Python 3 has only items()

if value is not None:
result[key] = value

return json.dumps(result)


class AppiumSearchContext(webdriver.Remote,
AndroidSearchContext):
"""Returns appium driver search conext"""


class AppiumWebElementSearchContext(SeleniumWebElement,
AndroidSearchContext):
"""Returns appium web element search context"""
3 changes: 3 additions & 0 deletions appium/webdriver/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@
from .extensions.network import Network
from .extensions.remote_fs import RemoteFS
from .extensions.screen_record import ScreenRecord
from .extensions.search_context import AppiumSearchContext
from .mobilecommand import MobileCommand as Command
from .switch_to import MobileSwitchTo
from .webelement import WebElement as MobileWebElement


# From remote/webdriver.py
_W3C_CAPABILITY_NAMES = frozenset([
'acceptInsecureCerts',
Expand Down Expand Up @@ -97,6 +99,7 @@ def _make_w3c_caps(caps):


class WebDriver(
AppiumSearchContext,
ActionHelpers,
Activities,
Applications,
Expand Down
6 changes: 4 additions & 2 deletions appium/webdriver/webelement.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import json

from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement
from selenium.webdriver.remote.command import Command as RemoteCommand

from appium.webdriver.common.mobileby import MobileBy

from .extensions.search_context import AppiumWebElementSearchContext
from .mobilecommand import MobileCommand as Command

# Python 3 imports
Expand All @@ -27,7 +29,7 @@
pass


class WebElement(SeleniumWebElement):
class WebElement(AppiumWebElementSearchContext):
# Override
def get_attribute(self, name):
"""Gets the given attribute or property of the element.
Expand Down
2 changes: 1 addition & 1 deletion ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ if [[ $result ]] ; then
exit 1
fi

python -m pylint --rcfile .pylintrc appium test --py3k
python -m pylint --rcfile .pylintrc appium test --errors-only
python -m pytest test/unit/
58 changes: 58 additions & 0 deletions test/unit/webdriver/webdriver_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@

from appium import version as appium_version

from test.unit.helper.test_helper import (
appium_command,
android_w3c_driver,
get_httpretty_request_body
)


class TestWebDriverWebDriver(object):

Expand Down Expand Up @@ -118,3 +124,55 @@ def test_create_session_change_session_id(self):
driver.session_id = 'another-session-id'
assert driver.title == 'title on another session id'
assert driver.session_id == 'another-session-id'

@httpretty.activate
def test_find_element_by_android_data_matcher(self):
driver = android_w3c_driver()
httpretty.register_uri(
httpretty.POST,
appium_command('/session/1234567890/element'),
body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}'
)
el = driver.find_element_by_android_data_matcher(
name='title', args=['title', 'Animation'], className='class name')

d = get_httpretty_request_body(httpretty.last_request())
assert d['using'] == '-android datamatcher'
value_dict = json.loads(d['value'])
assert value_dict['args'] == ['title', 'Animation']
assert value_dict['name'] == 'title'
assert value_dict['class'] == 'class name'
assert el.id == 'element-id'

@httpretty.activate
def test_find_elements_by_android_data_matcher(self):
driver = android_w3c_driver()
httpretty.register_uri(
httpretty.POST,
appium_command('/session/1234567890/elements'),
body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, {"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}'
)
els = driver.find_elements_by_android_data_matcher(name='title', args=['title', 'Animation'])

d = get_httpretty_request_body(httpretty.last_request())
assert d['using'] == '-android datamatcher'
value_dict = json.loads(d['value'])
assert value_dict['args'] == ['title', 'Animation']
assert value_dict['name'] == 'title'
assert els[0].id == 'element-id1'
assert els[1].id == 'element-id2'

@httpretty.activate
def test_find_elements_by_android_data_matcher_no_value(self):
driver = android_w3c_driver()
httpretty.register_uri(
httpretty.POST,
appium_command('/session/1234567890/elements'),
body='{"value": []}'
)
els = driver.find_elements_by_android_data_matcher()

d = get_httpretty_request_body(httpretty.last_request())
assert d['using'] == '-android datamatcher'
assert d['value'] == '{}'
assert len(els) == 0
85 changes: 85 additions & 0 deletions test/unit/webdriver/webelement_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import httpretty

from appium import webdriver
from appium.webdriver.webelement import WebElement as MobileWebElement

from appium import version as appium_version

from test.unit.helper.test_helper import (
appium_command,
android_w3c_driver,
get_httpretty_request_body
)


class TestWebElement(object):

@httpretty.activate
def test_find_element_by_android_data_matcher(self):
driver = android_w3c_driver()
element = MobileWebElement(driver, 'element_id', w3c=True)
httpretty.register_uri(
httpretty.POST,
appium_command('/session/1234567890/element/element_id/element'),
body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "child-element-id"}}'
)
el = element.find_element_by_android_data_matcher(
name='title', args=['title', 'Animation'], className='class name')

d = get_httpretty_request_body(httpretty.last_request())
assert d['using'] == '-android datamatcher'
value_dict = json.loads(d['value'])
assert value_dict['args'] == ['title', 'Animation']
assert value_dict['name'] == 'title'
assert value_dict['class'] == 'class name'
assert el.id == 'child-element-id'

@httpretty.activate
def test_find_elements_by_android_data_matcher(self):
driver = android_w3c_driver()
element = MobileWebElement(driver, 'element_id', w3c=True)
httpretty.register_uri(
httpretty.POST,
appium_command('/session/1234567890/element/element_id/elements'),
body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, {"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}'
)
els = element.find_elements_by_android_data_matcher(name='title', args=['title', 'Animation'])

d = get_httpretty_request_body(httpretty.last_request())
assert d['using'] == '-android datamatcher'
value_dict = json.loads(d['value'])
assert value_dict['args'] == ['title', 'Animation']
assert value_dict['name'] == 'title'
assert els[0].id == 'child-element-id1'
assert els[1].id == 'child-element-id2'

@httpretty.activate
def test_find_elements_by_android_data_matcher_no_value(self):
driver = android_w3c_driver()
element = MobileWebElement(driver, 'element_id', w3c=True)
httpretty.register_uri(
httpretty.POST,
appium_command('/session/1234567890/element/element_id/elements'),
body='{"value": []}'
)
els = element.find_elements_by_android_data_matcher()

d = get_httpretty_request_body(httpretty.last_request())
assert d['using'] == '-android datamatcher'
assert d['value'] == '{}'
assert len(els) == 0