diff --git a/appium/webdriver/common/mobileby.py b/appium/webdriver/common/mobileby.py index 34970a7b8..2034275fc 100644 --- a/appium/webdriver/common/mobileby.py +++ b/appium/webdriver/common/mobileby.py @@ -22,6 +22,7 @@ class MobileBy(By): ANDROID_UIAUTOMATOR = '-android uiautomator' ANDROID_VIEWTAG = '-android viewtag' ANDROID_DATA_MATCHER = '-android datamatcher' + ANDROID_VIEW_MATCHER = '-android viewmatcher' WINDOWS_UI_AUTOMATION = '-windows uiautomation' ACCESSIBILITY_ID = 'accessibility id' IMAGE = '-image' diff --git a/appium/webdriver/extensions/search_context/android.py b/appium/webdriver/extensions/search_context/android.py index 7c6f127a5..942675c7c 100644 --- a/appium/webdriver/extensions/search_context/android.py +++ b/appium/webdriver/extensions/search_context/android.py @@ -24,6 +24,39 @@ class AndroidSearchContext(BaseSearchContext): """Define search context for Android""" + def find_element_by_android_view_matcher(self, name=None, args=None, className=None): + """Finds element by [onView](https://developer.android.com/training/testing/espresso/basics) in Android + + It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). + + Args: + name (:obj:`str`, optional): 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 (:obj:`str`, optional): The args provided to the method + className (:obj:`str`, optional): The class name that the method is part of (defaults to `org.hamcrest.Matchers`). + Can be fully qualified by having the androidx.test.espresso.matcher. prefix. + If the prefix is not provided then it is going to be added implicitly. + (e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers` + + Returns: + `appium.webdriver.webelement.WebElement`: The found element + + Raises: + TypeError - Raises a TypeError if the arguments are not validated for JSON format + + Usage: + driver.find_element_by_android_view_matcher(name='withText', args=['Accessibility'], className='ViewMatchers') + + # To enable auto completion in PyCharm(IDE) + :rtype: `appium.webdriver.webelement.WebElement` + """ + + return self.find_element( + by=MobileBy.ANDROID_VIEW_MATCHER, + value=self._build_data_matcher(name=name, args=args, className=className) + ) + 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 @@ -58,6 +91,7 @@ def find_element_by_android_data_matcher(self, name=None, args=None, className=N 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: diff --git a/test/functional/android/search_context/find_by_view_matcher_tests.py b/test/functional/android/search_context/find_by_view_matcher_tests.py new file mode 100644 index 000000000..0f8ed0d00 --- /dev/null +++ b/test/functional/android/search_context/find_by_view_matcher_tests.py @@ -0,0 +1,77 @@ +#!/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 os +import unittest + +import pytest +from selenium.common.exceptions import WebDriverException + +from appium import webdriver +from appium.webdriver.common.mobileby import MobileBy +from appium.webdriver.extensions.search_context.android import ( + AndroidSearchContext +) +from test.functional.android.helper.test_helper import ( + desired_capabilities, + is_ci +) + + +class FindByViewMatcherTests(unittest.TestCase): + + def setUp(self): + desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') + desired_caps['automationName'] = 'Espresso' + self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) + + def tearDown(self): + if is_ci(): + # Take the screenshot to investigate when tests failed only on CI + img_path = os.path.join(os.getcwd(), self._testMethodName + '.png') + self.driver.get_screenshot_as_file(img_path) + self.driver.quit() + + def test_find_single_element(self): + el = self.driver.find_element_by_android_view_matcher( + name='withText', args=['Accessibility'], className='ViewMatchers') + assert el.text == 'Accessibility' + + def test_find_single_element_ful_class_name(self): + el = self.driver.find_element_by_android_view_matcher( + name='withText', args=['Accessibility'], className='androidx.test.espresso.matcher.ViewMatchers') + assert el.text == 'Accessibility' + + def test_find_single_element_using_hamcrest_matcher(self): + el = self.driver.find_element_by_android_view_matcher( + name='withText', + args={ + 'name': 'containsString', + 'args': 'Animati', + 'class': 'org.hamcrest.Matchers'}, + className='ViewMatchers') + assert el.text == 'Animation' + + # androidx.test.espresso.AmbiguousViewMatcherException: + # 'with text: a string containing "Access"' matches multiple views in the hierarchy. + def test_find_multiple_elements(self): + value = AndroidSearchContext()._build_data_matcher( + name='withSubstring', args=['Access'], className='ViewMatchers') + with pytest.raises(WebDriverException): + self.driver.find_elements(by=MobileBy.ANDROID_VIEW_MATCHER, value=value) + + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(FindByViewMatcherTests) + unittest.TextTestRunner(verbosity=2).run(suite)