Skip to content

Commit 1c179d2

Browse files
authored
add datamatcher (appium#335)
* add datamatcher * add zero case * defines search context for driver and element
1 parent bdbdc2f commit 1c179d2

File tree

7 files changed

+257
-3
lines changed

7 files changed

+257
-3
lines changed

appium/webdriver/common/mobileby.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class MobileBy(By):
2121
IOS_CLASS_CHAIN = '-ios class chain'
2222
ANDROID_UIAUTOMATOR = '-android uiautomator'
2323
ANDROID_VIEWTAG = '-android viewtag'
24+
ANDROID_DATA_MATCHER = '-android datamatcher'
2425
ACCESSIBILITY_ID = 'accessibility id'
2526
IMAGE = '-image'
2627
CUSTOM = '-custom'
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env python
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# pylint: disable=abstract-method
16+
17+
import json
18+
19+
from selenium import webdriver
20+
from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement
21+
22+
from appium.webdriver.common.mobileby import MobileBy
23+
24+
25+
class BaseSearchContext(object):
26+
"""Used by each search context. Dummy find_element/s are for preventing pylint error"""
27+
28+
def find_element(self, by=None, value=None):
29+
raise NotImplementedError
30+
31+
def find_elements(self, by=None, value=None):
32+
raise NotImplementedError
33+
34+
35+
class AndroidSearchContext(BaseSearchContext):
36+
"""Define search context for Android"""
37+
38+
def find_element_by_android_data_matcher(self, name=None, args=None, className=None):
39+
"""Finds element by [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android
40+
It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver).
41+
42+
:Args:
43+
- name - The name of a method to invoke.
44+
The method must return a Hamcrest
45+
[Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html)
46+
- args - The args provided to the method
47+
- className - The class name that the method is part of (defaults to `org.hamcrest.Matchers`).
48+
Can be fully qualified, or simple, and simple defaults to `androidx.test.espresso.matcher` package
49+
(e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers`
50+
51+
:Returns:
52+
An Element object
53+
54+
:Raises:
55+
- TypeError - Raises a TypeError if the arguments are not validated for JSON format
56+
57+
:Usage:
58+
driver.find_element_by_android_data_matcher(name='hasEntry', args=['title', 'Animation'])
59+
"""
60+
61+
return self.find_element(
62+
by=MobileBy.ANDROID_DATA_MATCHER,
63+
value=self._build_data_matcher(name=name, args=args, className=className)
64+
)
65+
66+
def find_elements_by_android_data_matcher(self, name=None, args=None, className=None):
67+
"""Finds elements by [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android
68+
It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver).
69+
70+
:Args:
71+
- name - The name of a method to invoke.
72+
The method must return a Hamcrest
73+
[Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html)
74+
- args - The args provided to the method
75+
- className - The class name that the method is part of (defaults to `org.hamcrest.Matchers`).
76+
Can be fully qualified, or simple, and simple defaults to `androidx.test.espresso.matcher` package
77+
(e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers`
78+
79+
:Usage:
80+
driver.find_elements_by_android_data_matcher(name='hasEntry', args=['title', 'Animation'])
81+
"""
82+
83+
return self.find_elements(
84+
by=MobileBy.ANDROID_DATA_MATCHER,
85+
value=self._build_data_matcher(name=name, args=args, className=className)
86+
)
87+
88+
def _build_data_matcher(self, name=None, args=None, className=None):
89+
result = {}
90+
91+
for key, value in {'name': name, 'args': args, 'class': className}.items():
92+
if value is not None:
93+
result[key] = value
94+
95+
return json.dumps(result)
96+
97+
98+
class AppiumSearchContext(webdriver.Remote,
99+
AndroidSearchContext):
100+
"""Returns appium driver search conext"""
101+
102+
103+
class AppiumWebElementSearchContext(SeleniumWebElement,
104+
AndroidSearchContext):
105+
"""Returns appium web element search context"""

appium/webdriver/webdriver.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@
3838
from .extensions.network import Network
3939
from .extensions.remote_fs import RemoteFS
4040
from .extensions.screen_record import ScreenRecord
41+
from .extensions.search_context import AppiumSearchContext
4142
from .mobilecommand import MobileCommand as Command
4243
from .switch_to import MobileSwitchTo
4344
from .webelement import WebElement as MobileWebElement
4445

46+
4547
# From remote/webdriver.py
4648
_W3C_CAPABILITY_NAMES = frozenset([
4749
'acceptInsecureCerts',
@@ -97,6 +99,7 @@ def _make_w3c_caps(caps):
9799

98100

99101
class WebDriver(
102+
AppiumSearchContext,
100103
ActionHelpers,
101104
Activities,
102105
Applications,

appium/webdriver/webelement.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import json
16+
1517
from selenium.webdriver.common.by import By
16-
from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement
1718
from selenium.webdriver.remote.command import Command as RemoteCommand
1819

1920
from appium.webdriver.common.mobileby import MobileBy
2021

22+
from .extensions.search_context import AppiumWebElementSearchContext
2123
from .mobilecommand import MobileCommand as Command
2224

2325
# Python 3 imports
@@ -27,7 +29,7 @@
2729
pass
2830

2931

30-
class WebElement(SeleniumWebElement):
32+
class WebElement(AppiumWebElementSearchContext):
3133
# Override
3234
def get_attribute(self, name):
3335
"""Gets the given attribute or property of the element.

ci.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ if [[ $result ]] ; then
77
exit 1
88
fi
99

10-
python -m pylint --rcfile .pylintrc appium test --py3k
10+
python -m pylint --rcfile .pylintrc appium test --errors-only
1111
python -m pytest test/unit/

test/unit/webdriver/webdriver_test.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818

1919
from appium import version as appium_version
2020

21+
from test.unit.helper.test_helper import (
22+
appium_command,
23+
android_w3c_driver,
24+
get_httpretty_request_body
25+
)
26+
2127

2228
class TestWebDriverWebDriver(object):
2329

@@ -118,3 +124,55 @@ def test_create_session_change_session_id(self):
118124
driver.session_id = 'another-session-id'
119125
assert driver.title == 'title on another session id'
120126
assert driver.session_id == 'another-session-id'
127+
128+
@httpretty.activate
129+
def test_find_element_by_android_data_matcher(self):
130+
driver = android_w3c_driver()
131+
httpretty.register_uri(
132+
httpretty.POST,
133+
appium_command('/session/1234567890/element'),
134+
body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}'
135+
)
136+
el = driver.find_element_by_android_data_matcher(
137+
name='title', args=['title', 'Animation'], className='class name')
138+
139+
d = get_httpretty_request_body(httpretty.last_request())
140+
assert d['using'] == '-android datamatcher'
141+
value_dict = json.loads(d['value'])
142+
assert value_dict['args'] == ['title', 'Animation']
143+
assert value_dict['name'] == 'title'
144+
assert value_dict['class'] == 'class name'
145+
assert el.id == 'element-id'
146+
147+
@httpretty.activate
148+
def test_find_elements_by_android_data_matcher(self):
149+
driver = android_w3c_driver()
150+
httpretty.register_uri(
151+
httpretty.POST,
152+
appium_command('/session/1234567890/elements'),
153+
body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, {"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}'
154+
)
155+
els = driver.find_elements_by_android_data_matcher(name='title', args=['title', 'Animation'])
156+
157+
d = get_httpretty_request_body(httpretty.last_request())
158+
assert d['using'] == '-android datamatcher'
159+
value_dict = json.loads(d['value'])
160+
assert value_dict['args'] == ['title', 'Animation']
161+
assert value_dict['name'] == 'title'
162+
assert els[0].id == 'element-id1'
163+
assert els[1].id == 'element-id2'
164+
165+
@httpretty.activate
166+
def test_find_elements_by_android_data_matcher_no_value(self):
167+
driver = android_w3c_driver()
168+
httpretty.register_uri(
169+
httpretty.POST,
170+
appium_command('/session/1234567890/elements'),
171+
body='{"value": []}'
172+
)
173+
els = driver.find_elements_by_android_data_matcher()
174+
175+
d = get_httpretty_request_body(httpretty.last_request())
176+
assert d['using'] == '-android datamatcher'
177+
assert d['value'] == '{}'
178+
assert len(els) == 0
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env python
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import httpretty
17+
18+
from appium import webdriver
19+
from appium.webdriver.webelement import WebElement as MobileWebElement
20+
21+
from appium import version as appium_version
22+
23+
from test.unit.helper.test_helper import (
24+
appium_command,
25+
android_w3c_driver,
26+
get_httpretty_request_body
27+
)
28+
29+
30+
class TestWebElement(object):
31+
32+
@httpretty.activate
33+
def test_find_element_by_android_data_matcher(self):
34+
driver = android_w3c_driver()
35+
element = MobileWebElement(driver, 'element_id', w3c=True)
36+
httpretty.register_uri(
37+
httpretty.POST,
38+
appium_command('/session/1234567890/element/element_id/element'),
39+
body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "child-element-id"}}'
40+
)
41+
el = element.find_element_by_android_data_matcher(
42+
name='title', args=['title', 'Animation'], className='class name')
43+
44+
d = get_httpretty_request_body(httpretty.last_request())
45+
assert d['using'] == '-android datamatcher'
46+
value_dict = json.loads(d['value'])
47+
assert value_dict['args'] == ['title', 'Animation']
48+
assert value_dict['name'] == 'title'
49+
assert value_dict['class'] == 'class name'
50+
assert el.id == 'child-element-id'
51+
52+
@httpretty.activate
53+
def test_find_elements_by_android_data_matcher(self):
54+
driver = android_w3c_driver()
55+
element = MobileWebElement(driver, 'element_id', w3c=True)
56+
httpretty.register_uri(
57+
httpretty.POST,
58+
appium_command('/session/1234567890/element/element_id/elements'),
59+
body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, {"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}'
60+
)
61+
els = element.find_elements_by_android_data_matcher(name='title', args=['title', 'Animation'])
62+
63+
d = get_httpretty_request_body(httpretty.last_request())
64+
assert d['using'] == '-android datamatcher'
65+
value_dict = json.loads(d['value'])
66+
assert value_dict['args'] == ['title', 'Animation']
67+
assert value_dict['name'] == 'title'
68+
assert els[0].id == 'child-element-id1'
69+
assert els[1].id == 'child-element-id2'
70+
71+
@httpretty.activate
72+
def test_find_elements_by_android_data_matcher_no_value(self):
73+
driver = android_w3c_driver()
74+
element = MobileWebElement(driver, 'element_id', w3c=True)
75+
httpretty.register_uri(
76+
httpretty.POST,
77+
appium_command('/session/1234567890/element/element_id/elements'),
78+
body='{"value": []}'
79+
)
80+
els = element.find_elements_by_android_data_matcher()
81+
82+
d = get_httpretty_request_body(httpretty.last_request())
83+
assert d['using'] == '-android datamatcher'
84+
assert d['value'] == '{}'
85+
assert len(els) == 0

0 commit comments

Comments
 (0)