From 83dd5f9a5d67f85cf7081ef1d53027a4be8b2e68 Mon Sep 17 00:00:00 2001 From: Atsushi Mori Date: Wed, 1 Jan 2020 00:09:26 +0900 Subject: [PATCH 1/6] Add android view matcher as strategy locator --- appium/webdriver/common/mobileby.py | 1 + .../webdriver/extensions/search_context/android.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/appium/webdriver/common/mobileby.py b/appium/webdriver/common/mobileby.py index 34970a7b..2034275f 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 7c6f127a..03eaed14 100644 --- a/appium/webdriver/extensions/search_context/android.py +++ b/appium/webdriver/extensions/search_context/android.py @@ -24,6 +24,18 @@ class AndroidSearchContext(BaseSearchContext): """Define search context for Android""" + def find_element_by_android_view_matcher(self, name=None, args=None, className=None): + return self.find_element( + by=MobileBy.ANDROID_VIEW_MATCHER, + value=self._build_data_matcher(name=name, args=args, className=className) + ) + + def find_elements_by_android_view_matcher(self, name=None, args=None, className=None): + return self.find_elements( + 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 From c771a410d996827e23b4eb976de5034bea6aba9d Mon Sep 17 00:00:00 2001 From: Atsushi Mori Date: Wed, 1 Jan 2020 13:13:41 +0900 Subject: [PATCH 2/6] Add docstring --- .../extensions/search_context/android.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/appium/webdriver/extensions/search_context/android.py b/appium/webdriver/extensions/search_context/android.py index 03eaed14..2a8b0492 100644 --- a/appium/webdriver/extensions/search_context/android.py +++ b/appium/webdriver/extensions/search_context/android.py @@ -25,12 +25,60 @@ 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, 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: + `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='hasEntry', args=['title', 'Animation']) + + # 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_elements_by_android_view_matcher(self, name=None, args=None, className=None): + """Finds elements 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, 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: + `appium.webdriver.webelement.WebElement`: The found elements + + Usage: + driver.find_elements_by_android_view_matcher(name='hasEntry', args=['title', 'Animation']) + + :rtype: `appium.webdriver.webelement.WebElement` + """ + return self.find_elements( by=MobileBy.ANDROID_VIEW_MATCHER, value=self._build_data_matcher(name=name, args=args, className=className) @@ -70,6 +118,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: From 79005fd033cf6266d4868647b5544377e43e8e3a Mon Sep 17 00:00:00 2001 From: Atsushi Mori Date: Wed, 1 Jan 2020 13:25:51 +0900 Subject: [PATCH 3/6] Add functional test --- .../find_by_view_matcher_tests.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/functional/android/search_context/find_by_view_matcher_tests.py 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 00000000..48c8e7bb --- /dev/null +++ b/test/functional/android/search_context/find_by_view_matcher_tests.py @@ -0,0 +1,46 @@ +#!/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 + +from appium import webdriver +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): + els = self.driver.find_elements_by_android_view_matcher(name='hasEntry', args=['title', 'Animation']) + assert len(els) == 1 + + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(FindByViewMatcherTests) + unittest.TextTestRunner(verbosity=2).run(suite) From 654b8061fead3506676c50e90a402fe5ec75b405 Mon Sep 17 00:00:00 2001 From: Atsushi Mori Date: Sun, 9 Feb 2020 10:34:11 +0900 Subject: [PATCH 4/6] Remove find_elements_by_android_data_matcher --- .../extensions/search_context/android.py | 28 --------------- .../find_by_view_matcher_tests.py | 35 +++++++++++++++++-- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/appium/webdriver/extensions/search_context/android.py b/appium/webdriver/extensions/search_context/android.py index 2a8b0492..cb8b6757 100644 --- a/appium/webdriver/extensions/search_context/android.py +++ b/appium/webdriver/extensions/search_context/android.py @@ -56,34 +56,6 @@ def find_element_by_android_view_matcher(self, name=None, args=None, className=N value=self._build_data_matcher(name=name, args=args, className=className) ) - def find_elements_by_android_view_matcher(self, name=None, args=None, className=None): - """Finds elements 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, 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: - `appium.webdriver.webelement.WebElement`: The found elements - - Usage: - driver.find_elements_by_android_view_matcher(name='hasEntry', args=['title', 'Animation']) - - :rtype: `appium.webdriver.webelement.WebElement` - """ - - return self.find_elements( - 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 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 index 48c8e7bb..0f8ed0d0 100644 --- a/test/functional/android/search_context/find_by_view_matcher_tests.py +++ b/test/functional/android/search_context/find_by_view_matcher_tests.py @@ -15,7 +15,14 @@ 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 @@ -37,8 +44,32 @@ def tearDown(self): self.driver.quit() def test_find_single_element(self): - els = self.driver.find_elements_by_android_view_matcher(name='hasEntry', args=['title', 'Animation']) - assert len(els) == 1 + 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__': From 425039bcb51c0480edf9890d96922c113b1b4ea7 Mon Sep 17 00:00:00 2001 From: Atsushi Mori Date: Sun, 9 Feb 2020 10:37:50 +0900 Subject: [PATCH 5/6] Fix docstring --- appium/webdriver/extensions/search_context/android.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appium/webdriver/extensions/search_context/android.py b/appium/webdriver/extensions/search_context/android.py index cb8b6757..38fa527d 100644 --- a/appium/webdriver/extensions/search_context/android.py +++ b/appium/webdriver/extensions/search_context/android.py @@ -35,7 +35,8 @@ def find_element_by_android_view_matcher(self, name=None, args=None, className=N [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, or simple, and simple defaults to `androidx.test.espresso.matcher` package + 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: From 0063c068eecd7327726766997a9e8ac1f6e513db Mon Sep 17 00:00:00 2001 From: Atsushi Mori Date: Sun, 9 Feb 2020 22:23:44 +0900 Subject: [PATCH 6/6] tweak docstring --- appium/webdriver/extensions/search_context/android.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/webdriver/extensions/search_context/android.py b/appium/webdriver/extensions/search_context/android.py index 38fa527d..942675c7 100644 --- a/appium/webdriver/extensions/search_context/android.py +++ b/appium/webdriver/extensions/search_context/android.py @@ -46,7 +46,7 @@ def find_element_by_android_view_matcher(self, name=None, args=None, className=N TypeError - Raises a TypeError if the arguments are not validated for JSON format Usage: - driver.find_element_by_android_view_matcher(name='hasEntry', args=['title', 'Animation']) + 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`