diff --git a/.gitignore b/.gitignore index e4760485..911a3616 100644 --- a/.gitignore +++ b/.gitignore @@ -15,13 +15,17 @@ MANIFEST build dist +# Cache .cache __pycache__ .idea .pytest_cache +.mypy_cache # Virtual Environments venv* .tox Pipfile.lock + +.coverage diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f1a8ae78..94cbed85 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,171 @@ Changelog ========= +v0.52 (2020-04-23) +------------------ + +Fix +~~~ +- Handling of dictionary-values in WebElement.get_attribute() (#521) + [Hannes Hauer] + +Other +~~~~~ +- Bump 0.52. [Kazuaki Matsuo] +- Chore: Fix int - str comparison error in ios desired capabilities + (#517) [Venkatesh] + + if number >= PytestXdistWorker.COUNT: +- Test: Add unit test for set_value (setImmediateValue) (#518) [Nrupesh + Patel] +- Test: Fix test_clear flaky functional test (#519) [Nrupesh Patel] +- Update changelog for 0.51. [Kazuaki Matsuo] + + +v0.51 (2020-04-12) +------------------ +- Bump 0.51. [Kazuaki Matsuo] +- Feat: Override send_keys without file upload function (#515) [Kazuaki + Matsuo] + + * add send_keys_direct + + * override send_keys + + * tune + + * add unittest instead of functional test + + * tweak syntax +- Feat: Add idempotency key header to create session requests (#514) + [Mykola Mokhnach] +- Fix flaky functional tests (#473) [Mori Atsushi] + + * Run all tests + + * Fix apk file path + + * Skip find_element_by_image test cases + + * Skip context switching test + + * Skip multi tap test on CI + + * Change strategy for waiting element + + * Add functions for same steps + + * Restore unexpected changes + + * Fix touch_action_tests + + * Fix + + * Fix + Fix test_driver_swipe + + * fix + + * Create _move_to_[target_view] + + * [test_driver_swipe] Add wait +- Update changelog for 0.50. [Kazuaki Matsuo] + + +v0.50 (2020-02-10) +------------------ +- Bump 0.50. [Kazuaki Matsuo] +- Feat: Add viewmatcher (#480) [Mori Atsushi] + + * Add android view matcher as strategy locator + + * Add docstring + + * Add functional test + + * Remove find_elements_by_android_data_matcher + + * Fix docstring + + * tweak docstring +- Chore: Fix find_by_images_tests.py (#495) [Mori Atsushi] + + * chore: Fix find_by_images_tests.py + + * Add installation opencv4nodejs + + * Fix typo + + * Add taking screen record to find_by_image_test + + * Fix errors on the emulator + + * Remove unused imports +- Update tox requirement from ~=3.6 to ~=3.14 (#494) [dependabot- + preview[bot]] + + Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox/releases) + - [Changelog](https://github.com/tox-dev/tox/blob/master/docs/changelog.rst) + - [Commits](https://github.com/tox-dev/tox/compare/3.6.0...3.14.3) +- Update tox-travis requirement from ~=0.11 to ~=0.12 (#491) + [dependabot-preview[bot]] + + Updates the requirements on [tox-travis](https://github.com/tox-dev/tox-travis) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox-travis/releases) + - [Changelog](https://github.com/tox-dev/tox-travis/blob/master/HISTORY.rst) + - [Commits](https://github.com/tox-dev/tox-travis/compare/0.11...0.12) +- Update autopep8 requirement from ~=1.4 to ~=1.5 (#490) [dependabot- + preview[bot]] + + Updates the requirements on [autopep8](https://github.com/hhatto/autopep8) to permit the latest version. + - [Release notes](https://github.com/hhatto/autopep8/releases) + - [Commits](https://github.com/hhatto/autopep8/compare/v1.4...v1.5) +- Update pytest-cov requirement from ~=2.6 to ~=2.8 (#489) [dependabot- + preview[bot]] + + Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version. + - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) + - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) + - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.6.0...v2.8.1) +- Chore: add try/catch in release script (#479) [Kazuaki Matsuo] + + * Add m and try/catch in pushing + + * fix error message + + * remove -m since it does not work for this usage +- [CI] Run with iOS 13.3 and Xcode 11.3 (#477) [Mori Atsushi] + + * [CI] Run with iOS 13.3 and Xcode 11.3 + + * Skip the case which has problem on Xcode 11.3 + + * Update FyndByIOClassChainTests along to iOS13 + + * Update FyndByElementWebelementTests along to iOS13 + + * Update KeyboardTests along to iOS13 + + * Update webdriver_tests along to iOS13 + + * Run test_find_element_by_isvisible with simpleIsVisibleCheck caps + + * Run test_hide_keyboard_no_key_name + + * Remove unused codes + + * [Readme] py.test -> pytest +- Ci: Take screen record as evidence (#481) [Mori Atsushi] + + * Take screen record for android + + * Take screen record for iOS + + * Save screen record for iOS +- Update changelog for 0.49. [Kazuaki Matsuo] + + v0.49 (2019-12-24) ------------------ - Bump 0.49. [Kazuaki Matsuo] diff --git a/README.md b/README.md index 50834ce6..d54e88ed 100644 --- a/README.md +++ b/README.md @@ -96,18 +96,18 @@ $ pytest -n 2 test/unit ### Functional ``` -$ pytest test/functional/ios/find_by_ios_class_chain_tests.py +$ pytest test/functional/ios/search_context/find_by_ios_class_chain_tests.py ``` ### In parallel for iOS -1. Create simulators named 'iPhone 6s - 8100' and 'iPhone 6s - 8101' +1. Create simulators named 'iPhone 8 - 8100' and 'iPhone 8 - 8101' 2. Install test libraries via pip ``` $ pip install pytest pytest-xdist ``` 3. Run tests ``` - $ pytest -n 2 test/functional/ios/find_by_ios_class_chain_tests.py + $ pytest -n 2 test/functional/ios/search_context/find_by_ios_class_chain_tests.py ``` # Release diff --git a/appium/version.py b/appium/version.py index fda2e626..04e6d68c 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1 @@ -version = '0.49' +version = '0.52' diff --git a/appium/webdriver/appium_connection.py b/appium/webdriver/appium_connection.py index f89998af..f643a168 100644 --- a/appium/webdriver/appium_connection.py +++ b/appium/webdriver/appium_connection.py @@ -12,19 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Dict +import uuid +from typing import TYPE_CHECKING, Any, Dict from selenium.webdriver.remote.remote_connection import RemoteConnection from appium.common.helper import library_version +if TYPE_CHECKING: + from urllib.parse import ParseResult + class AppiumConnection(RemoteConnection): @classmethod - def get_remote_connection_headers(cls, parsed_url: str, keep_alive: bool = True) -> Dict[str, Any]: + def get_remote_connection_headers(cls, parsed_url: 'ParseResult', keep_alive: bool = True) -> Dict[str, Any]: """Override get_remote_connection_headers in RemoteConnection""" headers = RemoteConnection.get_remote_connection_headers(parsed_url, keep_alive=keep_alive) headers['User-Agent'] = 'appium/python {} ({})'.format(library_version(), headers['User-Agent']) + if parsed_url.path.endswith('/session'): + # https://github.com/appium/appium-base-driver/pull/400 + headers['X-Idempotency-Key'] = str(uuid.uuid4()) return headers diff --git a/appium/webdriver/appium_service.py b/appium/webdriver/appium_service.py index 8aecc145..ca190d88 100644 --- a/appium/webdriver/appium_service.py +++ b/appium/webdriver/appium_service.py @@ -170,7 +170,7 @@ def start(self, **kwargs: Any) -> sp.Popen: if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, STATUS_URL, timeout_ms)): error_msg = f'Appium has failed to start on {host}:{port} within {timeout_ms}ms timeout' if error_msg is not None: - if stderr == sp.PIPE: + if stderr == sp.PIPE and self._process.stderr is not None: err_output = self._process.stderr.read() if err_output: error_msg += f'\nOriginal error: {str(err_output)}' 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 4197eb99..a4f3a958 100644 --- a/appium/webdriver/extensions/search_context/android.py +++ b/appium/webdriver/extensions/search_context/android.py @@ -15,7 +15,7 @@ # pylint: disable=abstract-method import json -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Any, List, Optional from appium.webdriver.common.mobileby import MobileBy @@ -28,8 +28,42 @@ class AndroidSearchContext(BaseSearchContext): """Define search context for Android""" + def find_element_by_android_view_matcher( + self, name: Optional[str] = None, args: Optional[Any] = None, className: Optional[str] = None) -> 'WebElement': + """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:`Any`, 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: Optional[str] = None, args: Optional[str] = None, className: Optional[str] = None) -> 'WebElement': + self, name: Optional[str] = None, args: Optional[Any] = None, className: Optional[str] = None) -> 'WebElement': """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). @@ -38,7 +72,7 @@ def find_element_by_android_data_matcher( 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 + args (:obj:`Any`, 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` @@ -62,7 +96,7 @@ def find_element_by_android_data_matcher( ) def find_elements_by_android_data_matcher( - self, name: Optional[str] = None, args: Optional[str] = None, className: Optional[str] = None) -> List['WebElement']: + self, name: Optional[str] = None, args: Optional[Any] = None, className: Optional[str] = None) -> List['WebElement']: """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). @@ -70,7 +104,7 @@ def find_elements_by_android_data_matcher( 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 + args (:obj:`Any`, 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` @@ -89,7 +123,7 @@ def find_elements_by_android_data_matcher( value=self._build_data_matcher(name=name, args=args, className=className) ) - def _build_data_matcher(self, name: Optional[str] = None, args: Optional[str] + def _build_data_matcher(self, name: Optional[str] = None, args: Optional[Any] = None, className: Optional[str] = None) -> str: result = {} diff --git a/appium/webdriver/webelement.py b/appium/webdriver/webelement.py index 2eb191aa..42af922b 100644 --- a/appium/webdriver/webelement.py +++ b/appium/webdriver/webelement.py @@ -15,6 +15,7 @@ from typing import Dict, List, Optional, TypeVar, Union from selenium.webdriver.common.by import By +from selenium.webdriver.common.utils import keys_to_typing from selenium.webdriver.remote.command import Command as RemoteCommand from .extensions.search_context import AppiumWebElementSearchContext @@ -24,7 +25,7 @@ class WebElement(AppiumWebElementSearchContext): - def get_attribute(self, name: str) -> Optional[str]: + def get_attribute(self, name: str) -> Optional[Union[str, Dict]]: """Gets the given attribute or property of the element. Override for Appium @@ -56,6 +57,9 @@ def get_attribute(self, name: str) -> Optional[str]: if attributeValue is None: return None + if isinstance(attributeValue, dict): + return attributeValue + # Convert to str along to the spec if not isinstance(attributeValue, str): attributeValue = str(attributeValue) @@ -205,3 +209,18 @@ def set_value(self, value: str) -> T: } self._execute(Command.SET_IMMEDIATE_VALUE, data) return self + + # Override + def send_keys(self, *value: str) -> T: + """Simulates typing into the element. + + Args: + value (str): A string for typing. + + Returns: + `appium.webdriver.webelement.WebElement` + """ + keys = keys_to_typing(value) + self._execute(RemoteCommand.SEND_KEYS_TO_ELEMENT, + {'text': ''.join(keys), 'value': keys}) + return self diff --git a/ci-jobs/functional/run_android_test.yml b/ci-jobs/functional/run_android_test.yml index cedf2ac7..1786e8dc 100644 --- a/ci-jobs/functional/run_android_test.yml +++ b/ci-jobs/functional/run_android_test.yml @@ -7,6 +7,8 @@ jobs: CI: ${{ parameters.ci }} steps: - template: ./run_appium.yml + parameters: + OPENCV: ${{ parameters.opencv }} - script: bash ci-jobs/functional/start-emulator.sh displayName: Create and run Emulator - script: | diff --git a/ci-jobs/functional/run_appium.yml b/ci-jobs/functional/run_appium.yml index 42ebadd3..dbc773b1 100644 --- a/ci-jobs/functional/run_appium.yml +++ b/ci-jobs/functional/run_appium.yml @@ -5,6 +5,9 @@ steps: displayName: Install Node 11.x - script: npm install -g appium@beta --chromedriver_version='2.44' displayName: Install appium +- script: npm install -g opencv4nodejs + condition: and(succeeded(), eq('${{ parameters.opencv }}', true)) + displayName: Install opencv4nodejs - task: UsePythonVersion@0 inputs: versionSpec: '3.x' diff --git a/ci-jobs/functional_test.yml b/ci-jobs/functional_test.yml index cc807502..200b7f20 100644 --- a/ci-jobs/functional_test.yml +++ b/ci-jobs/functional_test.yml @@ -5,8 +5,6 @@ parameters: xcodeForIOS: 11.3 CI: true -# [Android] Need to fix and add flaky tests for activities_tests, find_by_uiautomator_tests - jobs: - template: ./functional/run_ios_test.yml parameters: @@ -32,6 +30,7 @@ jobs: testFiles: 'device_time_tests.py search_context/find_by_*.py' sdkVer: ${{ parameters.androidSdkVer }} CI: ${{ parameters.ci }} + OPENCV: true - template: ./functional/run_android_test.yml parameters: name: 'func_test_android2' @@ -69,7 +68,7 @@ jobs: name: 'func_test_android6' vmImage: ${{ parameters.vmImage }} pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'common_tests.py' + testFiles: 'common_tests.py multi_action_tests.py webelement_tests.py' sdkVer: ${{ parameters.androidSdkVer }} CI: ${{ parameters.ci }} - template: ./functional/run_android_test.yml @@ -85,6 +84,6 @@ jobs: name: 'func_test_android8' vmImage: ${{ parameters.vmImage }} pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'network_connection_tests.py log_event_tests.py' + testFiles: 'network_connection_tests.py log_event_tests.py activities_tests.py hw_actions_tests.py touch_action_tests.py' sdkVer: ${{ parameters.androidSdkVer }} CI: ${{ parameters.ci }} diff --git a/test/functional/android/applications_tests.py b/test/functional/android/applications_tests.py index 118af363..ea2216a8 100644 --- a/test/functional/android/applications_tests.py +++ b/test/functional/android/applications_tests.py @@ -13,12 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from time import sleep import pytest from appium.webdriver.applicationstate import ApplicationState +from .helper.desired_capabilities import PATH from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase @@ -36,7 +38,7 @@ def test_is_app_installed(self) -> None: @pytest.mark.skip('This causes the server to crash. no idea why') def test_install_app(self) -> None: assert not self.driver.is_app_installed('io.selendroid.testapp') - self.driver.install_app('/Users/isaac/code/python-client/test/apps/selendroid-test-app.apk') + self.driver.install_app(PATH(os.path.join('../..', 'apps', 'selendroid-test-app.apk'))) assert self.driver.is_app_installed('io.selendroid.testapp') def test_remove_app(self) -> None: diff --git a/test/functional/android/file/find_by_image_success.png b/test/functional/android/file/find_by_image_success.png index a7f06ca9..39435fd3 100644 Binary files a/test/functional/android/file/find_by_image_success.png and b/test/functional/android/file/find_by_image_success.png differ diff --git a/test/functional/android/multi_action_tests.py b/test/functional/android/multi_action_tests.py index 24b4b404..6739bf59 100644 --- a/test/functional/android/multi_action_tests.py +++ b/test/functional/android/multi_action_tests.py @@ -14,33 +14,19 @@ from time import sleep +import pytest + from appium.webdriver.common.mobileby import MobileBy from appium.webdriver.common.multi_action import MultiAction from appium.webdriver.common.touch_action import TouchAction -from .helper.test_helper import BaseTestCase, wait_for_element +from .helper.test_helper import BaseTestCase, is_ci, wait_for_element class TestMultiAction(BaseTestCase): def test_parallel_actions(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - self.driver.scroll(el1, el2) - - el = self.driver.find_element_by_accessibility_id('Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - # simulate a swipe/scroll - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists') - action.press(el).move_to(x=100, y=-1000).release().perform() - el = self.driver.find_element_by_accessibility_id('Layouts') - action.press(el).move_to(x=100, y=-1000).release().perform() - - el = self.driver.find_element_by_accessibility_id('Splitting Touches across Views') - action.tap(el).perform() + self._move_to_splitting_touches_accros_views() - wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.ListView') els = self.driver.find_elements_by_class_name('android.widget.ListView') a1 = TouchAction() a1.press(els[0]) \ @@ -55,24 +41,8 @@ def test_parallel_actions(self) -> None: ma.perform() def test_actions_with_waits(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - self.driver.scroll(el1, el2) + self._move_to_splitting_touches_accros_views() - el = self.driver.find_element_by_accessibility_id('Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - # simulate a swipe/scroll - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists') - action.press(el).move_to(x=100, y=-1000).release().perform() - el = self.driver.find_element_by_accessibility_id('Layouts') - action.press(el).move_to(x=100, y=-1000).release().perform() - - el = self.driver.find_element_by_accessibility_id('Splitting Touches across Views') - action.tap(el).perform() - - wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.ListView') els = self.driver.find_elements_by_class_name('android.widget.ListView') a1 = TouchAction() a1.press(els[0]) \ @@ -94,6 +64,27 @@ def test_actions_with_waits(self) -> None: ma.add(a1, a2) ma.perform() + def _move_to_splitting_touches_accros_views(self) -> None: + el1 = self.driver.find_element_by_accessibility_id('Content') + el2 = self.driver.find_element_by_accessibility_id('Animation') + self.driver.scroll(el1, el2) + + el = self.driver.find_element_by_accessibility_id('Views') + action = TouchAction(self.driver) + action.tap(el).perform() + + # simulate a swipe/scroll + el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists') + action.press(el).move_to(x=100, y=-1000).release().perform() + el = self.driver.find_element_by_accessibility_id('Layouts') + action.press(el).move_to(x=100, y=-1000).release().perform() + + el = self.driver.find_element_by_accessibility_id('Splitting Touches across Views') + action.tap(el).perform() + + wait_for_element(self.driver, MobileBy.ID, 'io.appium.android.apis:id/list1') + + @pytest.mark.skipif(condition=is_ci(), reason='Skip since the test must be watched to check if it works') def test_driver_multi_tap(self) -> None: el = self.driver.find_element_by_accessibility_id('Graphics') action = TouchAction(self.driver) diff --git a/test/functional/android/search_context/find_by_image_tests.py b/test/functional/android/search_context/find_by_image_tests.py index 03b552c2..24dbde03 100644 --- a/test/functional/android/search_context/find_by_image_tests.py +++ b/test/functional/android/search_context/find_by_image_tests.py @@ -21,14 +21,16 @@ from selenium.webdriver.support.ui import WebDriverWait from appium import webdriver +from appium.webdriver.common.mobileby import MobileBy from test.functional.android.helper import desired_capabilities +from ..helper.test_helper import wait_for_element + -@pytest.mark.skip(reason="Need to fix broken test") class TestFindByImage(object): def setup_method(self) -> None: - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk') + desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) # relax template matching @@ -45,9 +47,7 @@ def test_find_based_on_image_template(self) -> None: with open(image_path, 'rb') as png_file: b64_data = base64.b64encode(png_file.read()).decode('UTF-8') - el = WebDriverWait(self.driver, 3).until( - EC.presence_of_element_located((By.IMAGE, b64_data)) - ) + el = wait_for_element(self.driver, MobileBy.IMAGE, b64_data) size = el.size assert size['width'] is not None assert size['height'] is not None @@ -61,16 +61,14 @@ def test_find_based_on_image_template(self) -> None: assert rect['y'] is not None assert el.is_displayed() el.click() - self.driver.find_element_by_accessibility_id("Alarm") + wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "Alarm") def test_find_multiple_elements_by_image_just_returns_one(self) -> None: - WebDriverWait(self.driver, 3).until( - EC.presence_of_element_located((By.ACCESSIBILITY_ID, "App")) - ) + wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "App") image_path = desired_capabilities.PATH('file/find_by_image_success.png') els = self.driver.find_elements_by_image(image_path) els[0].click() - self.driver.find_element_by_accessibility_id("Alarm") + wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "Alarm") def test_find_throws_no_such_element(self) -> None: image_path = desired_capabilities.PATH('file/find_by_image_failure.png') @@ -78,8 +76,7 @@ def test_find_throws_no_such_element(self) -> None: b64_data = base64.b64encode(png_file.read()).decode('UTF-8') with pytest.raises(TimeoutException): - WebDriverWait(self.driver, 3).until( - EC.presence_of_element_located((By.IMAGE, b64_data)) - ) + wait_for_element(self.driver, MobileBy.IMAGE, b64_data, timeout=3) + with pytest.raises(NoSuchElementException): self.driver.find_element_by_image(image_path) 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..5bb0a47d --- /dev/null +++ b/test/functional/android/search_context/find_by_view_matcher_tests.py @@ -0,0 +1,69 @@ +#!/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 ( + BaseTestCase, + desired_capabilities, + is_ci +) + + +class TestFindByViewMatcher(BaseTestCase): + + # Override + def setup_method(self, method) -> None: # type: ignore + 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) + if is_ci(): + self.driver.start_recording_screen() + + def test_find_single_element(self) -> None: + 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) -> None: + 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) -> None: + 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) -> None: + 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) diff --git a/test/functional/android/touch_action_tests.py b/test/functional/android/touch_action_tests.py index 59ee65a4..42f22fb6 100644 --- a/test/functional/android/touch_action_tests.py +++ b/test/functional/android/touch_action_tests.py @@ -21,6 +21,7 @@ from .helper.test_helper import ( APIDEMO_PKG_NAME, BaseTestCase, + is_ci, wait_for_element ) @@ -41,6 +42,7 @@ def test_tap_x_y(self) -> None: el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls') assert el is not None + @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI.') def test_tap_twice(self) -> None: el = self.driver.find_element_by_accessibility_id('Text') action = TouchAction(self.driver) @@ -72,20 +74,8 @@ def test_press_and_immediately_release_x_y(self) -> None: assert el is not None def test_press_and_wait(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - + self._move_to_custom_adapter() action = TouchAction(self.driver) - action.press(el1).move_to(el2).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views') - action.tap(el).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists') - action.tap(el).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, '1. Custom Adapter') - action.tap(el).perform() el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("People Names")') @@ -117,20 +107,8 @@ def test_press_and_moveto_x_y(self) -> None: assert el is not None def test_long_press(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - + self._move_to_custom_adapter() action = TouchAction(self.driver) - action.press(el1).move_to(el2).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views') - action.tap(el).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists') - action.tap(el).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, '1. Custom Adapter') - action.tap(el).perform() el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("People Names")') @@ -141,21 +119,10 @@ def test_long_press(self) -> None: 'new UiSelector().text("Sample menu")') assert el is not None + @pytest.mark.skipif(condition=is_ci(), reason='Skip since this check is low robust due to hard-coded position.') def test_long_press_x_y(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - + self._move_to_custom_adapter() action = TouchAction(self.driver) - action.press(el1).move_to(el2).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views') - action.tap(el).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists') - action.tap(el).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, '1. Custom Adapter') - action.tap(el).perform() # the element "People Names" is located at 430:310 (top left corner) # location can be changed by phone resolusion, OS version @@ -167,13 +134,8 @@ def test_long_press_x_y(self) -> None: assert el is not None def test_drag_and_drop(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - self.driver.scroll(el1, el2) - - el = self.driver.find_element_by_accessibility_id('Views') + self._move_to_views() action = TouchAction(self.driver) - action.tap(el).perform() el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Drag and Drop') action.tap(el).perform() @@ -184,17 +146,12 @@ def test_drag_and_drop(self) -> None: # dnd is stimulated by longpress-move_to-release action.long_press(dd3).move_to(dd2).release().perform() - el = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_text'.format(APIDEMO_PKG_NAME)) - assert 'drag_dot_3' in el.text + el = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_result_text'.format(APIDEMO_PKG_NAME)) + assert 'Dropped!' in el.text def test_driver_drag_and_drop(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - self.driver.scroll(el1, el2) - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views') + self._move_to_views() action = TouchAction(self.driver) - action.tap(el).perform() el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Drag and Drop') action.tap(el).perform() @@ -204,8 +161,8 @@ def test_driver_drag_and_drop(self) -> None: self.driver.drag_and_drop(dd3, dd2) - el = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_text'.format(APIDEMO_PKG_NAME)) - assert 'drag_dot_3' in el.text + el = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_result_text'.format(APIDEMO_PKG_NAME)) + assert 'Dropped!' in el.text def test_driver_swipe(self) -> None: el = self.driver.find_element_by_accessibility_id('Views') @@ -216,5 +173,24 @@ def test_driver_swipe(self) -> None: self.driver.find_element_by_accessibility_id('ImageView') self.driver.swipe(100, 1000, 100, 100, 800) - el = self.driver.find_element_by_accessibility_id('ImageView') + el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'ImageView') assert el is not None + + def _move_to_views(self) -> None: + el1 = self.driver.find_element_by_accessibility_id('Content') + el2 = self.driver.find_element_by_accessibility_id('Animation') + self.driver.scroll(el1, el2) + + el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views') + action = TouchAction(self.driver) + action.tap(el).perform() + + def _move_to_custom_adapter(self) -> None: + self._move_to_views() + action = TouchAction(self.driver) + + el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists') + action.tap(el).perform() + + el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, '1. Custom Adapter') + action.tap(el).perform() diff --git a/test/functional/ios/helper/desired_capabilities.py b/test/functional/ios/helper/desired_capabilities.py index 633b8e2c..50810667 100644 --- a/test/functional/ios/helper/desired_capabilities.py +++ b/test/functional/ios/helper/desired_capabilities.py @@ -66,7 +66,7 @@ def wda_port() -> int: return 8100 -# Before running tests, you must have iOS simulators named 'iPhone 6s - 8100' and 'iPhone 6s - 8101' +# Before running tests, you must have iOS simulators named 'iPhone 8 - 8100' and 'iPhone 8 - 8101' def iphone_device_name() -> str: diff --git a/test/functional/ios/webdriver_tests.py b/test/functional/ios/webdriver_tests.py index 629190ef..5f3cecbf 100644 --- a/test/functional/ios/webdriver_tests.py +++ b/test/functional/ios/webdriver_tests.py @@ -80,6 +80,8 @@ def test_clear(self) -> None: input_text = 'blah' el.click() el.send_keys(input_text) + self.driver.hide_keyboard() + # TODO Needs to get the element again to update value in the element. Remove below one line when it's fixed. el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] text = el.get_attribute('value') diff --git a/test/unit/webdriver/webelement_test.py b/test/unit/webdriver/webelement_test.py new file mode 100644 index 00000000..88347b57 --- /dev/null +++ b/test/unit/webdriver/webelement_test.py @@ -0,0 +1,100 @@ +#!/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 os +import tempfile + +import httpretty + +from appium.webdriver.webelement import WebElement as MobileWebElement +from test.unit.helper.test_helper import ( + android_w3c_driver, + appium_command, + get_httpretty_request_body +) + + +class TestWebElement(object): + + @httpretty.activate + def test_set_value(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/element/element_id/value') + ) + + element = MobileWebElement(driver, 'element_id', w3c=True) + value = 'happy testing' + element.set_value(value) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['value'] == [value] + + @httpretty.activate + def test_send_key(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/value') + ) + + element = MobileWebElement(driver, 'element_id', w3c=True) + element.send_keys('happy testing') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['text'] == ''.join(d['value']) + + @httpretty.activate + def test_send_key_with_file(self): + driver = android_w3c_driver() + # Should not send this file + tmp_f = tempfile.NamedTemporaryFile() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/value') + ) + + try: + element = MobileWebElement(driver, 'element_id', w3c=True) + element.send_keys(tmp_f.name) + finally: + tmp_f.close() + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['text'] == ''.join(d['value']) + + @httpretty.activate + def test_get_attribute_with_dict(self): + driver = android_w3c_driver() + rect_dict = { + 'y': 200, + 'x': 100, + 'width': 300, + 'height': 56 + } + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/element/element_id/attribute/rect'), + body=json.dumps({"value": rect_dict}) + ) + + element = MobileWebElement(driver, 'element_id', w3c=True) + ef = element.get_attribute('rect') + + d = httpretty.last_request() + + assert isinstance(ef, dict) + assert ef == rect_dict