├── .github └── workflows │ └── publish-pypi.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── easyium ├── __init__.py ├── alert.py ├── context.py ├── decorator.py ├── dynamic_element.py ├── element.py ├── enumeration.py ├── exceptions.py ├── identifier.py ├── locator.py ├── static_element.py ├── utils.py ├── waiter.py └── web_driver.py ├── examples └── google.py └── setup.py /.github/workflows/publish-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to pypi 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*-release' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Setup Python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.8 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | with: 20 | ref: 'refs/heads/master' 21 | - name: Build dist 22 | run: python setup.py sdist --formats=gztar 23 | - name: Publish 24 | uses: pypa/gh-action-pypi-publish@release/v1 25 | with: 26 | user: __token__ 27 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Pycharm 60 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015-present Karl Gong 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include setup.py 4 | graft examples 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | easyium 3 | ======= 4 | .. image:: https://img.shields.io/pypi/v/easyium.svg 5 | :target: https://pypi.python.org/pypi/easyium 6 | 7 | .. image:: https://img.shields.io/pypi/pyversions/easyium.svg 8 | :target: https://pypi.python.org/pypi/easyium 9 | 10 | easyium is an easy-to-use wrapper for selenium&appium and it can make you more focus on business not the element. 11 | 12 | Find the latest version on github: https://github.com/KarlGong/easyium-python or PyPI: https://pypi.python.org/pypi/easyium 13 | 14 | Advantages 15 | ---------- 16 | - easyium provides unified apis to test on browsers and devices. 17 | 18 | - easyium adds a global implicit wait for elements and you rarely need to consider waiting a element to be visible or existing. 19 | 20 | - easyium introduces a simple and clear way to build model objects for UI. 21 | 22 | - easyium has a better performance, the element will lazily load WebElement reference and reuses it if necessary. 23 | 24 | - easyium provides easy-to-use wait method for element. e.g., my_element.wait_for().not_().exists() 25 | 26 | - easyium provides a simple way to define a locator. e.g., use ``"xpath=.//mytag"`` instead of ``By.XPATH, ".//mytag"`` 27 | 28 | - easyium provides a mechanism to avoid StaleElementReferenceException. 29 | 30 | Installation 31 | ------------ 32 | The last stable release is available on PyPI and can be installed with ``pip``. 33 | 34 | :: 35 | 36 | $ pip install easyium 37 | 38 | Glossary 39 | -------- 40 | WebDriver 41 | ~~~~~~~~~ 42 | It is a wrapper for selenium&appium's web driver. You can create a new instance by providing web driver type. 43 | 44 | DynamicElement 45 | ~~~~~~~~~~~~~~ 46 | DynamicElement is one type of Element in easyium. It refers to the element which is dynamic relative to its parent. 47 | 48 | You can get it only by calling ``WebDriver.find_element(locator)`` or ``Element.find_element(locator)`` and you can not create a new instance by yourself. 49 | 50 | StaticElement 51 | ~~~~~~~~~~~~~ 52 | StaticElement is the other type of Element in easyium. It refers to the element which is static relative to its parent. 53 | 54 | You can create a new instance by providing parent and locator. 55 | 56 | Example 57 | ------- 58 | For detailed examples, please refer to the ``examples`` folder in source distribution or visit https://github.com/KarlGong/easyium-python/tree/master/examples 59 | 60 | Contact me 61 | ---------- 62 | For information and suggestions you can contact me at karl.gong@outlook.com 63 | 64 | Change Log 65 | ---------- 66 | 2.0.0 (compared to 1.3.8) 67 | 68 | - Retire python 2.x 69 | 70 | - Support selenium==4.1.0 and appium==2.1.1 71 | 72 | 1.3.8 (compared to 1.3.7) 73 | 74 | - Freeze selenium and appium version. 75 | 76 | 1.3.7 (compared to 1.3.6) 77 | 78 | - Add web_driver.get_viewport_size, web_driver.set_viewport_size 79 | 80 | 1.3.6 (compared to 1.3.5) 81 | 82 | - Support appium>=0.46 83 | 84 | 1.3.5 (compared to 1.3.4) 85 | 86 | - Optimize condition in Context.find_element(s). 87 | 88 | 1.3.4 (compared to 1.3.3) 89 | 90 | - Fix InvalidElementStateException is not caught. 91 | 92 | 1.3.3 (compared to 1.3.2) 93 | 94 | - Fix Element.set_selection_range() issue. 95 | 96 | 1.3.2 (compared to 1.3.1) 97 | 98 | - Handle ElementNotInteractableException in element actions. 99 | 100 | 1.3.1 (compared to 1.3.0) 101 | 102 | - Support selenium>=3.141.0 and appium>=0.30 103 | 104 | - Implement double_click and context_click for safari. 105 | 106 | 1.3.0 (compared to 1.2.10) 107 | 108 | - Support appium>=0.27 and add the new actions. 109 | 110 | - Shorten the locator name. e.g., accessibility_id -> acc_id 111 | 112 | - Add waiting for WebDriver.switch_to_context. 113 | 114 | 1.2.10 (compared to 1.2.9) 115 | 116 | - Support selenium>=3.13.0 and appium>=0.26 117 | 118 | 1.2.9 (compared to 1.2.8) 119 | 120 | - Support selenium>=3.8.0 and appium>=0.25 121 | 122 | 1.2.8 (compared to 1.2.7) 123 | 124 | - Fix desired_capabilities issue. 125 | 126 | 1.2.7 (compared to 1.2.6) 127 | 128 | - Fix the string format of web driver. 129 | 130 | 1.2.6 (compared to 1.2.5) 131 | 132 | - Add Remote web driver back. 133 | 134 | - Support selenium>=3.6.0 135 | 136 | - Remove Android, Ios driver. 137 | 138 | 1.2.5 (compared to 1.2.4) 139 | 140 | - Enhance element actions. 141 | 142 | 1.2.4 (compared to 1.2.3) 143 | 144 | - Support selenium>=3.4.0 145 | 146 | - Add Remote web driver. 147 | 148 | 1.2.3 (compared to 1.2.2) 149 | 150 | - Support selenium>=3.0.2, appium>=0.24 151 | 152 | - Add focus() for element. 153 | 154 | 1.2.2 (compared to 1.2.1) 155 | 156 | - Add waiting for WebDriver.switch_to_frame(). 157 | 158 | - Add WebDriver.wait_for().reloaded(). 159 | 160 | 1.2.1 (compared to 1.2.0) 161 | 162 | - Remove at_least argument in context.find_elements. 163 | 164 | - Support find element(s) condition in Context.find_element(s). 165 | 166 | 1.2.0 (compared to 1.1.5) 167 | 168 | - Add WebDriver Ie, Firefox, Chrome, Opera, Safari, Edge, PhantomJS, Ios and Android. 169 | 170 | - Add scroll_to() in WebDriver. 171 | 172 | - Add has_child() in Context. 173 | 174 | - Add get_center() in Element. 175 | 176 | - Add wait_for_server_started() in utils. 177 | 178 | - Support WebDriver.wait_for().text_equals(), WebDriver.wait_for().activity_present(). 179 | 180 | - Support with statement for WebDriver. 181 | 182 | - Support at_least in Context.find_elements(). 183 | 184 | - Support drag_and_drop_to_with_offset, drag_and_drop_by_offset for mobile. 185 | 186 | - Remove pre and post wait time. 187 | 188 | 1.1.5 (compared to 1.1.4) 189 | 190 | - Add scroll(), switch_to_new_window() to WebDriver. 191 | 192 | - Add scroll(), scroll_into_view() to Element. 193 | 194 | - Raise InvalidLocatorException when the locator is invalid. 195 | 196 | 1.1.4 (compared to 1.1.3) 197 | 198 | - Add get_screenshot_as_xxx() to Element. 199 | 200 | 1.1.3 (compared to 1.1.2) 201 | 202 | - Add docstring for apis. 203 | 204 | - Add post wait time for waiter. 205 | 206 | 1.1.2 (compared to 1.1.1) 207 | 208 | - Add pre wait time for waiter. 209 | 210 | 1.1.1 (compared to 1.1.0) 211 | 212 | - Optimize the waiter. 213 | 214 | - Add blur() for class Element. 215 | 216 | 1.1.0 (compared to 1.0.0) 217 | 218 | - Refactor the waiter. 219 | 220 | 1.0.0 221 | 222 | - Baby easyium. 223 | -------------------------------------------------------------------------------- /easyium/__init__.py: -------------------------------------------------------------------------------- 1 | from .dynamic_element import DynamicElement 2 | from .element import Element 3 | from .enumeration import WebDriverContext, WebDriverPlatform 4 | from .exceptions import EasyiumException, TimeoutException, ElementTimeoutException, WebDriverTimeoutException, \ 5 | NoSuchElementException, NotPersistException, LatePersistException, InvalidLocatorException, \ 6 | UnsupportedOperationException 7 | from .identifier import Identifier 8 | from .static_element import StaticElement 9 | from .waiter import Waiter 10 | from .web_driver import WebDriver, Ie, Firefox, Chrome, Opera, Safari, Edge, Appium 11 | -------------------------------------------------------------------------------- /easyium/alert.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.alert import Alert as SeleniumAlert 2 | 3 | 4 | class Alert: 5 | def __init__(self, selenium_alert: SeleniumAlert): 6 | self.__selenium_alert = selenium_alert 7 | 8 | def get_text(self) -> str: 9 | """ 10 | Gets the text of the Alert. 11 | """ 12 | return self.__selenium_alert.text 13 | 14 | def dismiss(self): 15 | """ 16 | Dismisses the alert available. 17 | """ 18 | self.__selenium_alert.dismiss() 19 | 20 | def accept(self): 21 | """ 22 | Accepts the alert available. 23 | """ 24 | self.__selenium_alert.accept() 25 | 26 | def send_keys(self, keys: str): 27 | """ 28 | Send Keys to the Alert. 29 | 30 | :param keys: The text to be sent to Alert. 31 | """ 32 | self.__selenium_alert.send_keys(keys) 33 | -------------------------------------------------------------------------------- /easyium/context.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Callable, List, TYPE_CHECKING 2 | 3 | from appium.webdriver.webdriver import WebDriver as AppiumWebDriver 4 | from appium.webdriver.webelement import WebElement as AppiumElement 5 | from selenium.common.exceptions import StaleElementReferenceException as SeleniumStaleElementReferenceException, NoSuchElementException as SeleniumNoSuchElementException, \ 6 | InvalidSelectorException as SeleniumInvalidSelectorException, WebDriverException as SeleniumWebDriverException 7 | 8 | from .exceptions import InvalidLocatorException, NoSuchElementException, EasyiumException, TimeoutException, ElementTimeoutException 9 | from .identifier import Identifier 10 | from .locator import locator_to_by_value 11 | from .waiter import Waiter, WebDriverWaitFor, ElementWaitFor 12 | 13 | if TYPE_CHECKING: 14 | from .dynamic_element import DynamicElement 15 | from .web_driver import WebDriver, WebDriverInfo 16 | 17 | 18 | class Context: 19 | def __init__(self): 20 | self.__wait_interval = None 21 | self.__wait_timeout = None 22 | 23 | def get_web_driver(self) -> "WebDriver": 24 | pass 25 | 26 | def get_web_driver_info(self) -> "WebDriverInfo": 27 | pass 28 | 29 | def _selenium_context(self) -> Union[AppiumWebDriver, AppiumElement]: 30 | pass 31 | 32 | def _refresh(self): 33 | pass 34 | 35 | def persist(self): 36 | pass 37 | 38 | def get_screenshot_as_file(self, filename: str) -> bool: 39 | pass 40 | 41 | def save_screenshot(self, filename: str) -> bool: 42 | pass 43 | 44 | def get_screenshot_as_png(self) -> bytes: 45 | pass 46 | 47 | def get_screenshot_as_base64(self) -> str: 48 | pass 49 | 50 | def get_wait_interval(self) -> int: 51 | """ 52 | Get the wait interval of this context. 53 | If the wait interval for element is not set, return the driver's wait interval. 54 | 55 | :return: the wait interval 56 | """ 57 | if self.__wait_interval is not None: 58 | return self.__wait_interval 59 | return self.get_web_driver().get_wait_interval() 60 | 61 | def set_wait_interval(self, interval: int): 62 | """ 63 | Set the wait interval of this context. 64 | 65 | :param interval: the new wait interval (in milliseconds) 66 | """ 67 | self.__wait_interval = interval 68 | 69 | def get_wait_timeout(self) -> int: 70 | """ 71 | Get the wait timeout of this context. 72 | If the wait timeout for element is not set, return the driver's wait timeout. 73 | 74 | :return: the wait timeout 75 | """ 76 | if self.__wait_timeout is not None: 77 | return self.__wait_timeout 78 | return self.get_web_driver().get_wait_timeout() 79 | 80 | def set_wait_timeout(self, timeout: int): 81 | """ 82 | Set the wait timeout of this context. 83 | 84 | :param timeout: the new wait timeout (in milliseconds) 85 | """ 86 | self.__wait_timeout = timeout 87 | 88 | def wait_for(self, interval=None, timeout=None) -> Union[ElementWaitFor, WebDriverWaitFor]: 89 | pass 90 | 91 | def waiter(self, interval: int = None, timeout: int = None): 92 | """ 93 | Get a Waiter instance. 94 | 95 | :param interval: the wait interval (in milliseconds). If None, use context's wait interval. 96 | :param timeout: the wait timeout (in milliseconds). If None, use context's wait interval. 97 | """ 98 | _interval = self.get_wait_interval() if interval is None else interval 99 | _timeout = self.get_wait_timeout() if timeout is None else timeout 100 | return Waiter(_interval, _timeout) 101 | 102 | def _find_selenium_element(self, locator: str) -> AppiumElement: 103 | by, value = locator_to_by_value(locator) 104 | try: 105 | try: 106 | return self._selenium_context().find_element(by, value) 107 | except SeleniumStaleElementReferenceException: 108 | self._refresh() 109 | return self._selenium_context().find_element(by, value) 110 | except SeleniumInvalidSelectorException: 111 | raise InvalidLocatorException("The value <%s> of locator <%s> is not a valid expression." % (value, locator), self) 112 | except SeleniumNoSuchElementException: 113 | raise NoSuchElementException("Cannot find element by <%s> under:" % locator, self) 114 | except SeleniumWebDriverException as wde: 115 | raise EasyiumException(wde.msg, self) 116 | 117 | def has_child(self, locator: str) -> bool: 118 | """ 119 | Whether this context has a child element. 120 | 121 | :param locator: 122 | the locator (relative to this context) of the child element. 123 | The format of locator is: "by=value", the possible values of "by" are:: 124 | 125 | "id": By.ID 126 | "xpath": By.XPATH 127 | "link": By.LINK_TEXT 128 | "partial_link": By.PARTIAL_LINK_TEXT 129 | "name": By.NAME 130 | "tag": By.TAG_NAME 131 | "class": By.CLASS_NAME 132 | "css": By.CSS_SELECTOR 133 | "ios_pre": MobileBy.IOS_PREDICATE 134 | "ios_ui": MobileBy.IOS_UIAUTOMATION 135 | "ios_class": MobileBy.IOS_CLASS_CHAIN 136 | "android_ui": MobileBy.ANDROID_UIAUTOMATOR 137 | "android_tag": MobileBy.ANDROID_VIEWTAG 138 | "android_data": MobileBy.ANDROID_DATA_MATCHER 139 | "acc_id": MobileBy.ACCESSIBILITY_ID 140 | "custom": MobileBy.CUSTOM 141 | :return: whether this context has a child element. 142 | """ 143 | return self.find_element(locator) is not None 144 | 145 | def find_element(self, locator: str, identifier: Callable[["DynamicElement"], str] = Identifier.id, condition: Callable[["DynamicElement"], bool] = lambda element: True) \ 146 | -> "DynamicElement": 147 | """ 148 | Find a DynamicElement under this context. 149 | Note: if no element is found, None will be returned. 150 | 151 | :param locator: 152 | the locator (relative to this context) of the element to be found. 153 | The format of locator is: "by=value", the possible values of "by" are:: 154 | 155 | "id": By.ID 156 | "xpath": By.XPATH 157 | "link": By.LINK_TEXT 158 | "partial_link": By.PARTIAL_LINK_TEXT 159 | "name": By.NAME 160 | "tag": By.TAG_NAME 161 | "class": By.CLASS_NAME 162 | "css": By.CSS_SELECTOR 163 | "ios_pre": MobileBy.IOS_PREDICATE 164 | "ios_ui": MobileBy.IOS_UIAUTOMATION 165 | "ios_class": MobileBy.IOS_CLASS_CHAIN 166 | "android_ui": MobileBy.ANDROID_UIAUTOMATOR 167 | "android_tag": MobileBy.ANDROID_VIEWTAG 168 | "android_data": MobileBy.ANDROID_DATA_MATCHER 169 | "acc_id": MobileBy.ACCESSIBILITY_ID 170 | "custom": MobileBy.CUSTOM 171 | :param identifier: 172 | the identifier is a function to generate the locator of the found element, you can get the standard ones in class Identifier. 173 | Otherwise, you can create one like this:: 174 | 175 | context.find_element("class=foo", lambda e: "xpath=.//*[@bar='%s']" % e.get_attribute("bar")) 176 | :param condition: 177 | end finding element when the found element match the condition function. 178 | e.g., end finding element when the found element is not None 179 | 180 | context.find_element("class=foo", condition=lambda element: element) 181 | :return: the DynamicElement found by locator 182 | """ 183 | # import the DynamicElement here to avoid cyclic dependency 184 | from .dynamic_element import DynamicElement 185 | 186 | by, value = locator_to_by_value(locator) 187 | element = {"inner": None} 188 | 189 | def _find_element(): 190 | try: 191 | try: 192 | element["inner"] = DynamicElement(self, self._selenium_context().find_element(by, value), locator, identifier) 193 | return element["inner"] 194 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 195 | # Only Element can reach here 196 | self.wait_for().exists() 197 | element["inner"] = DynamicElement(self, self._selenium_context().find_element(by, value), locator, identifier) 198 | return element["inner"] 199 | except SeleniumInvalidSelectorException: 200 | raise InvalidLocatorException("The value <%s> of locator <%s> is not a valid expression." % (value, locator), self) 201 | except SeleniumNoSuchElementException: 202 | element["inner"] = None 203 | return element["inner"] 204 | except SeleniumWebDriverException as wde: 205 | raise EasyiumException(wde.msg, self) 206 | 207 | try: 208 | self.waiter().wait_for(lambda: condition(_find_element())) 209 | except TimeoutException as e: 210 | if e.__class__ == ElementTimeoutException: 211 | # raised by self.wait_for().exists() in _find_element() 212 | raise 213 | raise TimeoutException("Timed out waiting for the found element by <%s> under:\n%s\nmatches condition <%s>." % (locator, self, condition.__name__)) 214 | 215 | return element["inner"] 216 | 217 | def find_elements(self, locator: str, identifier: Callable[["DynamicElement"], str] = Identifier.id, 218 | condition: Callable[[List["DynamicElement"]], bool] = lambda elements: True) \ 219 | -> List["DynamicElement"]: 220 | """ 221 | Find DynamicElement list under this context. 222 | Note: if no elements is found, empty list will be returned. 223 | 224 | :param locator: 225 | the locator (relative to this context) of the elements to be found. 226 | The format of locator is: "by=value", the possible values of "by" are:: 227 | 228 | "id": By.ID 229 | "xpath": By.XPATH 230 | "link": By.LINK_TEXT 231 | "partial_link": By.PARTIAL_LINK_TEXT 232 | "name": By.NAME 233 | "tag": By.TAG_NAME 234 | "class": By.CLASS_NAME 235 | "css": By.CSS_SELECTOR 236 | "ios_pre": MobileBy.IOS_PREDICATE 237 | "ios_ui": MobileBy.IOS_UIAUTOMATION 238 | "ios_class": MobileBy.IOS_CLASS_CHAIN 239 | "android_ui": MobileBy.ANDROID_UIAUTOMATOR 240 | "android_tag": MobileBy.ANDROID_VIEWTAG 241 | "android_data": MobileBy.ANDROID_DATA_MATCHER 242 | "acc_id": MobileBy.ACCESSIBILITY_ID 243 | "custom": MobileBy.CUSTOM 244 | :param identifier: 245 | the identifier is a function to generate the locator of the found elements, you can get the standard ones in class Identifier. 246 | Otherwise, you can create one like this:: 247 | 248 | context.find_elements("class=foo", identifier=lambda element: "xpath=.//*[@bar='%s']" % element.get_attribute("bar")) 249 | :param condition: 250 | end finding elements when the found element list match the condition function. 251 | e.g., end finding elements when the found element list is not empty 252 | 253 | context.find_elements("class=foo", condition=lambda elements: elements) 254 | :return: the DynamicElement list found by locator 255 | """ 256 | # import the DynamicElement here to avoid cyclic dependency 257 | from .dynamic_element import DynamicElement 258 | 259 | by, value = locator_to_by_value(locator) 260 | elements = {"inner": []} 261 | 262 | def _find_elements(): 263 | try: 264 | try: 265 | selenium_elements = self._selenium_context().find_elements(by, value) 266 | elements["inner"] = [DynamicElement(self, selenium_element, locator, identifier) for selenium_element in selenium_elements] 267 | return elements["inner"] 268 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 269 | # Only Element can reach here 270 | self.wait_for().exists() 271 | selenium_elements = self._selenium_context().find_elements(by, value) 272 | elements["inner"] = [DynamicElement(self, selenium_element, locator, identifier) for selenium_element in selenium_elements] 273 | return elements["inner"] 274 | except SeleniumInvalidSelectorException: 275 | raise InvalidLocatorException("The value <%s> of locator <%s> is not a valid expression." % (value, locator), self) 276 | except SeleniumWebDriverException as wde: 277 | raise EasyiumException(wde.msg, self) 278 | 279 | try: 280 | self.waiter().wait_for(lambda: condition(_find_elements())) 281 | except TimeoutException as e: 282 | if e.__class__ == ElementTimeoutException: 283 | # raised by self.wait_for().exists() in _find_elements() 284 | raise 285 | raise TimeoutException("Timed out waiting for the found element list by <%s> under:\n%s\nmatches condition <%s>." % (locator, self, condition.__name__)) 286 | 287 | return elements["inner"] 288 | -------------------------------------------------------------------------------- /easyium/decorator.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from .exceptions import UnsupportedOperationException 4 | 5 | 6 | def SupportedBy(*_platforms): 7 | def handle_func(func): 8 | @functools.wraps(func) 9 | def handle_args(*args, **kwargs): 10 | platforms = [] 11 | for platform in _platforms: 12 | if isinstance(platform, list): 13 | platforms += platform 14 | else: 15 | platforms += [platform] 16 | 17 | from .element import Element 18 | from .web_driver import WebDriver 19 | from .waiter import ElementWaitFor, WebDriverWaitFor 20 | 21 | if isinstance(args[0], Element): 22 | platform = args[0].get_web_driver_info().platform 23 | if platform not in platforms: 24 | raise UnsupportedOperationException( 25 | "Operation [element.%s()] is not supported by platform [%s]." % (func.__name__, platform)) 26 | elif isinstance(args[0], WebDriver): 27 | platform = args[0].get_web_driver_info().platform 28 | if platform not in platforms: 29 | raise UnsupportedOperationException( 30 | "Operation [webdriver.%s()] is not supported by platform [%s]." % (func.__name__, platform)) 31 | elif isinstance(args[0], ElementWaitFor): 32 | platform = args[0]._get_element().get_web_driver_info().platform 33 | if platform not in platforms: 34 | raise UnsupportedOperationException( 35 | "Operation [element.wait_for().%s()] is not supported by platform [%s]." % ( 36 | func.__name__, platform)) 37 | elif isinstance(args[0], WebDriverWaitFor): 38 | platform = args[0]._get_web_driver().get_web_driver_info().platform 39 | if platform not in platforms: 40 | raise UnsupportedOperationException( 41 | "Operation [webdriver.wait_for().%s()] is not supported by platform [%s]." % ( 42 | func.__name__, platform)) 43 | 44 | return func(*args, **kwargs) 45 | 46 | return handle_args 47 | 48 | return handle_func 49 | -------------------------------------------------------------------------------- /easyium/dynamic_element.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from appium.webdriver.webelement import WebElement as AppiumElement 4 | 5 | from .context import Context 6 | from .element import Element 7 | from .exceptions import NotPersistException, LatePersistException 8 | 9 | 10 | class DynamicElement(Element): 11 | def __init__(self, parent: Context, selenium_element: AppiumElement, found_by, identifier: Callable[[Element], str]): 12 | Element.__init__(self, parent) 13 | # from element 14 | self._inner_selenium_element = selenium_element 15 | self._locator = None 16 | # self 17 | self.__found_by = found_by 18 | self.__identifier = identifier 19 | 20 | def _refresh(self): 21 | if self._locator is None: 22 | raise NotPersistException("persist() was not invoked so this Element cannot auto-refresh.", self) 23 | self._inner_selenium_element = None 24 | self._inner_selenium_element = self.get_parent()._find_selenium_element(self._locator) 25 | 26 | def persist(self): 27 | """ 28 | Generate the locator of this element by identifier, so this element can auto-refresh. 29 | """ 30 | self.get_parent().persist() 31 | 32 | try: 33 | if self._locator is None: 34 | self._locator = self.__identifier(self) 35 | except NotPersistException: 36 | raise LatePersistException( 37 | "Trying to persist() a stale element. Try invoking persist() earlier.", self) 38 | 39 | def __str__(self): 40 | if self._inner_selenium_element is None: 41 | return "%s\n|- DynamicElement " % ( 42 | self.get_parent(), None, self._locator, self.__found_by) 43 | else: 44 | return "%s\n|- DynamicElement " % ( 45 | self.get_parent(), self._inner_selenium_element.id, self._locator, self.__found_by) 46 | -------------------------------------------------------------------------------- /easyium/element.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from appium.webdriver.webelement import WebElement as AppiumElement 4 | from selenium.common.exceptions import WebDriverException as SeleniumWebDriverException, StaleElementReferenceException as SeleniumStaleElementReferenceException, \ 5 | InvalidElementStateException as SeleniumInvalidElementStateException 6 | 7 | from .context import Context 8 | from .decorator import SupportedBy 9 | from .enumeration import WebDriverContext, WebDriverPlatform 10 | from .exceptions import EasyiumException, NoSuchElementException 11 | from .waiter import ElementWaitFor 12 | from .web_driver import WebDriver, WebDriverInfo 13 | 14 | 15 | class Element(Context): 16 | def __init__(self, parent: Context): 17 | Context.__init__(self) 18 | # self 19 | self._inner_selenium_element = None 20 | self._locator = None 21 | self.__parent = parent 22 | 23 | def get_web_driver(self) -> WebDriver: 24 | """ 25 | Get the web driver of this element. 26 | 27 | :return: the web driver 28 | """ 29 | return self.get_parent().get_web_driver() 30 | 31 | def get_web_driver_info(self) -> WebDriverInfo: 32 | """ 33 | Get current info of this web driver. 34 | 35 | :return: the web driver info 36 | """ 37 | return self.get_web_driver().get_web_driver_info() 38 | 39 | def get_parent(self) -> Context: 40 | """ 41 | Get the parent of this element. 42 | 43 | :return: the parent of this element 44 | """ 45 | return self.__parent 46 | 47 | def _selenium_context(self) -> AppiumElement: 48 | if self._inner_selenium_element is None: 49 | self._refresh() 50 | return self._inner_selenium_element 51 | 52 | def _selenium_element(self) -> AppiumElement: 53 | if self._inner_selenium_element is None: 54 | self._refresh() 55 | return self._inner_selenium_element 56 | 57 | def wait_for(self, interval: int = None, timeout: int = None) -> ElementWaitFor: 58 | """ 59 | Get a ElementWaitFor instance. 60 | 61 | :param interval: the wait interval (in milliseconds). If None, use element's wait interval. 62 | :param timeout: the wait timeout (in milliseconds). If None, use element's wait timeout. 63 | """ 64 | _interval = self.get_wait_interval() if interval is None else interval 65 | _timeout = self.get_wait_timeout() if timeout is None else timeout 66 | return ElementWaitFor(self, _interval, _timeout) 67 | 68 | def focus(self): 69 | """ 70 | Focus this element. 71 | """ 72 | try: 73 | self.get_web_driver().execute_script("arguments[0].focus()", self) 74 | except SeleniumWebDriverException as wde: 75 | raise EasyiumException(wde.msg, self) 76 | 77 | def blur(self): 78 | """ 79 | Removes keyboard focus from this element. 80 | """ 81 | try: 82 | self.get_web_driver().execute_script("arguments[0].blur()", self) 83 | except SeleniumWebDriverException as wde: 84 | raise EasyiumException(wde.msg, self) 85 | 86 | def clear(self): 87 | """ 88 | Clears the text if it's a text entry element. 89 | """ 90 | try: 91 | try: 92 | self._selenium_element().clear() 93 | except (NoSuchElementException, SeleniumStaleElementReferenceException, SeleniumInvalidElementStateException): 94 | self.wait_for().visible() 95 | self._selenium_element().clear() 96 | except SeleniumWebDriverException as wde: 97 | raise EasyiumException(wde.msg, self) 98 | 99 | def click(self): 100 | """ 101 | Clicks this element. 102 | """ 103 | try: 104 | try: 105 | self._selenium_element().click() 106 | except (NoSuchElementException, SeleniumStaleElementReferenceException, SeleniumInvalidElementStateException): 107 | self.wait_for().visible() 108 | self._selenium_element().click() 109 | except SeleniumWebDriverException as wde: 110 | raise EasyiumException(wde.msg, self) 111 | 112 | def double_click(self): 113 | """ 114 | Double click this element. 115 | """ 116 | script = """ 117 | var dblclickEventObj = null; 118 | if (typeof window.Event == "function") { 119 | dblclickEventObj = new MouseEvent('dblclick', {'bubbles': true, 'cancelable': true}); 120 | } else { 121 | dblclickEventObj = document.createEvent("MouseEvents"); 122 | dblclickEventObj.initMouseEvent('dblclick', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 123 | } 124 | arguments[0].dispatchEvent(dblclickEventObj); 125 | """ 126 | try: 127 | try: 128 | if self.get_web_driver_info().context == WebDriverContext.SAFARI \ 129 | and self.get_web_driver_info().platform == WebDriverPlatform.PC: 130 | self.get_web_driver().execute_script(script, self) 131 | else: 132 | self.get_web_driver().create_action_chains().double_click(self._selenium_element()).perform() 133 | except (NoSuchElementException, SeleniumStaleElementReferenceException, SeleniumInvalidElementStateException): 134 | self.wait_for().visible() 135 | if self.get_web_driver_info().context == WebDriverContext.SAFARI \ 136 | and self.get_web_driver_info().platform == WebDriverPlatform.PC: 137 | self.get_web_driver().execute_script(script, self) 138 | else: 139 | self.get_web_driver().create_action_chains().double_click(self._selenium_element()).perform() 140 | except SeleniumWebDriverException as wde: 141 | raise EasyiumException(wde.msg, self) 142 | 143 | def context_click(self): 144 | """ 145 | Context click this element. 146 | """ 147 | script = """ 148 | var clickEventObj = null; 149 | if (typeof window.Event == "function") { 150 | clickEventObj = new MouseEvent('click', {'bubbles': true, 'cancelable': true, 'button': 2, 'buttons': 2}); 151 | } else { 152 | clickEventObj = document.createEvent("MouseEvents"); 153 | clickEventObj.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 2, 2); 154 | } 155 | arguments[0].dispatchEvent(clickEventObj); 156 | """ 157 | try: 158 | try: 159 | if self.get_web_driver_info().context == WebDriverContext.SAFARI \ 160 | and self.get_web_driver_info().platform == WebDriverPlatform.PC: 161 | self.get_web_driver().execute_script(script, self) 162 | else: 163 | self.get_web_driver().create_action_chains().context_click(self._selenium_element()).perform() 164 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 165 | self.wait_for().visible() 166 | if self.get_web_driver_info().context == WebDriverContext.SAFARI \ 167 | and self.get_web_driver_info().platform == WebDriverPlatform.PC: 168 | self.get_web_driver().execute_script(script, self) 169 | else: 170 | self.get_web_driver().create_action_chains().context_click(self._selenium_element()).perform() 171 | except SeleniumWebDriverException as wde: 172 | raise EasyiumException(wde.msg, self) 173 | 174 | def send_keys(self, *value: str): 175 | """ 176 | Simulates typing into this element. 177 | 178 | :param value: A string for typing, or setting form fields. For setting 179 | file inputs, this could be a local file path. For special keys codes, 180 | use enum selenium.webdriver.common.Keys. 181 | 182 | Use this to send simple key events or to fill out form fields:: 183 | 184 | form_textfield = driver.find_element('name=username') 185 | form_textfield.send_keys("admin") 186 | 187 | This can also be used to set file inputs:: 188 | 189 | file_input = driver.find_element('name=profilePic') 190 | file_input.send_keys("path/to/profilepic.gif") 191 | # Generally it's better to wrap the file path in one of the methods 192 | # in os.path to return the actual path to support cross OS testing. 193 | # file_input.send_keys(os.path.abspath("path/to/profilepic.gif")) 194 | 195 | """ 196 | try: 197 | try: 198 | self._selenium_element().send_keys(*value) 199 | except (NoSuchElementException, SeleniumStaleElementReferenceException, SeleniumInvalidElementStateException): 200 | self.wait_for().visible() 201 | self._selenium_element().send_keys(*value) 202 | except SeleniumWebDriverException as wde: 203 | raise EasyiumException(wde.msg, self) 204 | 205 | def submit(self): 206 | """ 207 | Submits a form. 208 | """ 209 | try: 210 | try: 211 | self._selenium_element().submit() 212 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 213 | self.wait_for().visible() 214 | self._selenium_element().submit() 215 | except SeleniumWebDriverException as wde: 216 | raise EasyiumException(wde.msg, self) 217 | 218 | def get_property(self, name: str) -> str: 219 | """ 220 | Gets the given property of the element. 221 | 222 | :param name: Name of the property to retrieve. 223 | 224 | :Usage: 225 | text_length = target_element.get_property("text_length") 226 | """ 227 | try: 228 | try: 229 | return self._selenium_element().get_property(name) 230 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 231 | self.wait_for().exists() 232 | return self._selenium_element().get_property(name) 233 | except SeleniumWebDriverException as wde: 234 | raise EasyiumException(wde.msg, self) 235 | 236 | def get_dom_attribute(self, name: str) -> str: 237 | """ 238 | Gets the given attribute of the element. Unlike :func:`get_attribute`, this method only returns attributes declared in the element's HTML markup. 239 | 240 | :param name: Name of the attribute to retrieve. 241 | 242 | :Usage: 243 | cls = target_element.get_dom_attribute("class") 244 | """ 245 | try: 246 | try: 247 | return self._selenium_element().get_dom_attribute(name) 248 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 249 | self.wait_for().exists() 250 | return self._selenium_element().get_dom_attribute(name) 251 | except SeleniumWebDriverException as wde: 252 | raise EasyiumException(wde.msg, self) 253 | 254 | def get_attribute(self, name: str) -> Union[str, bool]: 255 | """ 256 | Gets the given attribute or property of this element. 257 | 258 | This method will first try to return the value of a property with the 259 | given name. If a property with that name doesn't exist, it returns the 260 | value of the attribute with the same name. If there's no attribute with 261 | that name, ``None`` is returned. 262 | 263 | Values which are considered truthy, that is equals "true" or "false", 264 | are returned as booleans. All other non-``None`` values are returned 265 | as strings. For attributes or properties which do not exist, ``None`` 266 | is returned. 267 | 268 | :param name: name of the attribute/property to retrieve. 269 | 270 | :Usage: 271 | # Check if the "active" CSS class is applied to an element. 272 | is_active = "active" in target_element.get_attribute("class") 273 | """ 274 | try: 275 | try: 276 | return self._selenium_element().get_attribute(name) 277 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 278 | self.wait_for().exists() 279 | return self._selenium_element().get_attribute(name) 280 | except SeleniumWebDriverException as wde: 281 | raise EasyiumException(wde.msg, self) 282 | 283 | def set_attribute(self, name: str, value: str): 284 | """ 285 | Set the attribute of this element to value. 286 | 287 | :param name: the attribute name 288 | :param value: the value to be set 289 | """ 290 | try: 291 | self.get_web_driver().execute_script("arguments[0].setAttribute('%s', '%s')" % (name, value), self) 292 | except SeleniumWebDriverException as wde: 293 | raise EasyiumException(wde.msg, self) 294 | 295 | def get_css_value(self, property_name: str) -> str: 296 | """ 297 | Gets the value of a CSS property. 298 | 299 | :param property_name: the property name 300 | """ 301 | try: 302 | try: 303 | return self._selenium_element().value_of_css_property(property_name) 304 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 305 | self.wait_for().exists() 306 | return self._selenium_element().value_of_css_property(property_name) 307 | except SeleniumWebDriverException as wde: 308 | raise EasyiumException(wde.msg, self) 309 | 310 | def get_value_of_css_property(self, property_name: str) -> str: 311 | """ 312 | Gets the value of a CSS property. 313 | 314 | :param property_name: the property name 315 | """ 316 | return self.get_css_value(property_name) 317 | 318 | def get_location(self) -> dict: 319 | """ 320 | Gets the location for the top-left corner of this element. 321 | 322 | :return: the location dict, {'x': x, 'y': y} 323 | """ 324 | try: 325 | try: 326 | return self._selenium_element().location 327 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 328 | self.wait_for().exists() 329 | return self._selenium_element().location 330 | except SeleniumWebDriverException as wde: 331 | raise EasyiumException(wde.msg, self) 332 | 333 | def get_location_in_view(self) -> dict: 334 | """ 335 | Use this to discover where on the screen this element is. 336 | THIS METHOD SHOULD CAUSE THE ELEMENT TO BE SCROLLED INTO VIEW. 337 | 338 | Returns the top-left corner location on the screen, or ``None`` if 339 | this element is not visible. 340 | """ 341 | context = self.get_web_driver_info().context 342 | try: 343 | try: 344 | if context == WebDriverContext.NATIVE_APP: 345 | return self._selenium_element().location_in_view 346 | else: 347 | return self._selenium_element().location_once_scrolled_into_view 348 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 349 | self.wait_for().exists() 350 | if context == WebDriverContext.NATIVE_APP: 351 | return self._selenium_element().location_in_view 352 | else: 353 | return self._selenium_element().location_once_scrolled_into_view 354 | except SeleniumWebDriverException as wde: 355 | raise EasyiumException(wde.msg, self) 356 | 357 | def get_size(self) -> dict: 358 | """ 359 | Gets the size (including border) of this element. 360 | 361 | :return: the size dict, {'width': width, 'height': height} 362 | """ 363 | try: 364 | try: 365 | return self._selenium_element().size 366 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 367 | self.wait_for().exists() 368 | return self._selenium_element().size 369 | except SeleniumWebDriverException as wde: 370 | raise EasyiumException(wde.msg, self) 371 | 372 | def get_rect(self) -> dict: 373 | """ 374 | Gets a dictionary with the size and location of this element. 375 | 376 | :return: the rect dict, {'width': width, 'height': height, 'x': x, 'y': y} 377 | """ 378 | try: 379 | try: 380 | return self._selenium_element().rect 381 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 382 | self.wait_for().exists() 383 | return self._selenium_element().rect 384 | except SeleniumWebDriverException as wde: 385 | raise EasyiumException(wde.msg, self) 386 | 387 | def get_center(self) -> dict: 388 | """ 389 | Gets the location for the center of this element. 390 | 391 | :return: the location dict, {'x': x, 'y': y} 392 | """ 393 | rect = self.get_rect() 394 | return {"x": rect["x"] + rect["width"] / 2, 395 | "y": rect["y"] + rect["height"] / 2} 396 | 397 | def get_tag_name(self) -> str: 398 | """ 399 | Gets this element's tagName property. 400 | """ 401 | try: 402 | try: 403 | return self._selenium_element().tag_name 404 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 405 | self.wait_for().exists() 406 | return self._selenium_element().tag_name 407 | except SeleniumWebDriverException as wde: 408 | raise EasyiumException(wde.msg, self) 409 | 410 | def get_value(self) -> str: 411 | """ 412 | Gets the value of this element. 413 | Can be used to get the text of a text entry element. 414 | Text entry elements are INPUT and TEXTAREA elements. 415 | """ 416 | try: 417 | try: 418 | return self._selenium_element().get_attribute("value") 419 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 420 | self.wait_for().exists() 421 | return self._selenium_element().get_attribute("value") 422 | except SeleniumWebDriverException as wde: 423 | raise EasyiumException(wde.msg, self) 424 | 425 | def set_value(self, value: str): 426 | """ 427 | Set the value on this element. 428 | 429 | :param value: the value to be set on this element 430 | """ 431 | context = self.get_web_driver_info().context 432 | try: 433 | try: 434 | if context == WebDriverContext.NATIVE_APP: 435 | self._selenium_element().set_value(value) 436 | else: 437 | self.get_web_driver().execute_script("arguments[0].setAttribute('value', '%s')" % value, self) 438 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 439 | self.wait_for().exists() 440 | if context == WebDriverContext.NATIVE_APP: 441 | self._selenium_element().set_value(value) 442 | else: 443 | self.get_web_driver().execute_script("arguments[0].setAttribute('value', '%s')" % value, self) 444 | except SeleniumWebDriverException as wde: 445 | raise EasyiumException(wde.msg, self) 446 | 447 | def get_text(self) -> str: 448 | """ 449 | Gets the text of this element(including the text of its children). 450 | """ 451 | try: 452 | try: 453 | return self._selenium_element().text 454 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 455 | self.wait_for().exists() 456 | return self._selenium_element().text 457 | except SeleniumWebDriverException as wde: 458 | raise EasyiumException(wde.msg, self) 459 | 460 | def set_text(self, text: str): 461 | """ 462 | Sends text to this element. Previous text is removed. 463 | 464 | :param text: the text to be sent to this element 465 | """ 466 | context = self.get_web_driver_info().context 467 | try: 468 | try: 469 | if context == WebDriverContext.NATIVE_APP: 470 | self._selenium_element().set_text(text) 471 | else: 472 | self.get_web_driver().execute_script("arguments[0].innerText = '%s'" % text, self) 473 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 474 | self.wait_for().exists() 475 | if context == WebDriverContext.NATIVE_APP: 476 | self._selenium_element().set_text(text) 477 | else: 478 | self.get_web_driver().execute_script("arguments[0].innerText = '%s'" % text, self) 479 | except SeleniumWebDriverException as wde: 480 | raise EasyiumException(wde.msg, self) 481 | 482 | def get_text_node_content(self, text_node_index: int) -> str: 483 | """ 484 | Get content of the text node in this element. 485 | If the text_node_index refers to a non-text node or be out of bounds, an exception will be thrown. 486 | 487 | :param text_node_index: index of text node in this element 488 | :return: the content of the text node in this element. 489 | """ 490 | try: 491 | content = self.get_web_driver().execute_script( 492 | "return arguments[0].childNodes[%s].nodeValue" % text_node_index, self) 493 | except SeleniumWebDriverException as wde: 494 | raise EasyiumException(wde.msg, self) 495 | 496 | if content is None: 497 | raise EasyiumException("Cannot get text content of a non-text node in element:", self) 498 | return content 499 | 500 | def set_selection_range(self, start: int, end: int): 501 | """ 502 | Set the selection range for text in this element. 503 | 504 | :param start: start position 505 | :param end: end position 506 | """ 507 | script = """ 508 | function getTextNodesIn(node) { 509 | var textNodes = []; 510 | if (node.nodeType == 3) { 511 | textNodes.push(node); 512 | } else { 513 | var children = node.childNodes; 514 | for (var i = 0, len = children.length; i < len; ++i) { 515 | textNodes.push.apply(textNodes, getTextNodesIn(children[i])); 516 | } 517 | } 518 | return textNodes; 519 | } 520 | 521 | function setSelectionRange(el, start, end) { 522 | if (el.tagName == 'INPUT' || el.tagName == 'TEXTAREA'){ 523 | if(el.createTextRange){ 524 | var Range=el.createTextRange(); 525 | Range.collapse(); 526 | Range.moveEnd('character',end); 527 | Range.moveStart('character',start); 528 | Range.select(); 529 | }else if(el.setSelectionRange){ 530 | el.focus(); 531 | el.setSelectionRange(start,end); 532 | } 533 | } else { 534 | if (document.createRange && window.getSelection) { 535 | var range = document.createRange(); 536 | range.selectNodeContents(el); 537 | var textNodes = getTextNodesIn(el); 538 | var foundStart = false; 539 | var charCount = 0, endCharCount; 540 | 541 | for (var i = 0, textNode; textNode = textNodes[i++]; ) { 542 | endCharCount = charCount + textNode.length; 543 | if (!foundStart && start >= charCount 544 | && (start < endCharCount || 545 | (start == endCharCount && i <= textNodes.length))) { 546 | range.setStart(textNode, start - charCount); 547 | foundStart = true; 548 | } 549 | if (foundStart && end <= endCharCount) { 550 | range.setEnd(textNode, end - charCount); 551 | break; 552 | } 553 | charCount = endCharCount; 554 | } 555 | 556 | var sel = window.getSelection(); 557 | sel.removeAllRanges(); 558 | sel.addRange(range); 559 | } else if (document.selection && document.body.createTextRange) { 560 | var textRange = document.body.createTextRange(); 561 | textRange.moveToElementText(el); 562 | textRange.collapse(true); 563 | textRange.moveEnd('character', end); 564 | textRange.moveStart('character', start); 565 | textRange.select(); 566 | } 567 | } 568 | } 569 | 570 | setSelectionRange(arguments[0], %s, %s); 571 | """ 572 | try: 573 | self.get_web_driver().execute_script(script % (start, end), self) 574 | except SeleniumWebDriverException as wde: 575 | raise EasyiumException(wde.msg, self) 576 | 577 | def get_inner_html(self) -> str: 578 | """ 579 | Get the inner html of this element. 580 | """ 581 | try: 582 | return self.get_web_driver().execute_script("return arguments[0].innerHTML", self) 583 | except SeleniumWebDriverException as wde: 584 | raise EasyiumException(wde.msg, self) 585 | 586 | def is_enabled(self) -> bool: 587 | """ 588 | Returns whether the element is enabled. 589 | """ 590 | try: 591 | try: 592 | return self._selenium_element().is_enabled() 593 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 594 | self.wait_for().visible() 595 | return self._selenium_element().is_enabled() 596 | except SeleniumWebDriverException as wde: 597 | raise EasyiumException(wde.msg, self) 598 | 599 | def is_selected(self) -> bool: 600 | """ 601 | Returns whether this element is selected. 602 | Can be used to check if a checkbox or radio button is selected. 603 | """ 604 | try: 605 | try: 606 | return self._selenium_element().is_selected() 607 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 608 | self.wait_for().visible() 609 | return self._selenium_element().is_selected() 610 | except SeleniumWebDriverException as wde: 611 | raise EasyiumException(wde.msg, self) 612 | 613 | def mouse_over(self, native: bool = False): 614 | """ 615 | Do mouse over this element. 616 | 617 | :param native: use the selenium native implementation 618 | """ 619 | script = """ 620 | var mouseoverEventObj = null; 621 | if (typeof window.Event == "function") { 622 | mouseoverEventObj = new MouseEvent('mouseover', {'bubbles': true, 'cancelable': true}); 623 | } else { 624 | mouseoverEventObj = document.createEvent("MouseEvents"); 625 | mouseoverEventObj.initMouseEvent('mouseover', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 626 | } 627 | arguments[0].dispatchEvent(mouseoverEventObj); 628 | """ 629 | try: 630 | try: 631 | if native: 632 | self.get_web_driver().create_action_chains().move_to_element(self._selenium_element()).perform() 633 | else: 634 | self.get_web_driver().execute_script(script, self) 635 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 636 | self.wait_for().exists() 637 | if native: 638 | self.get_web_driver().create_action_chains().move_to_element(self._selenium_element()).perform() 639 | else: 640 | self.get_web_driver().execute_script(script, self) 641 | except SeleniumWebDriverException as wde: 642 | raise EasyiumException(wde.msg, self) 643 | 644 | def mouse_out(self, native: bool = False): 645 | """ 646 | Do mouse out this element. 647 | 648 | :param native: use the selenium native implementation 649 | """ 650 | script = """ 651 | var mouseoutEventObj = null; 652 | if (typeof window.Event == "function") { 653 | mouseoutEventObj = new MouseEvent('mouseout', {'bubbles': true, 'cancelable': true}); 654 | } else { 655 | mouseoutEventObj = document.createEvent("MouseEvents"); 656 | mouseoutEventObj.initMouseEvent('mouseout', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 657 | } 658 | arguments[0].dispatchEvent(mouseoutEventObj); 659 | """ 660 | try: 661 | try: 662 | if native: 663 | self.get_web_driver().create_action_chains().move_by_offset(-99999, -99999).perform() 664 | else: 665 | self.get_web_driver().execute_script(script, self) 666 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 667 | self.wait_for().exists() 668 | if native: 669 | self.get_web_driver().create_action_chains().move_by_offset(-99999, -99999).perform() 670 | else: 671 | self.get_web_driver().execute_script(script, self) 672 | except SeleniumWebDriverException as wde: 673 | raise EasyiumException(wde.msg, self) 674 | 675 | def drag_and_drop_by_offset(self, x_offset: float, y_offset: float): 676 | """ 677 | Drag and drop to target offset. 678 | 679 | :param x_offset: X offset to drop 680 | :param y_offset: Y offset to drop 681 | """ 682 | context = self.get_web_driver_info().context 683 | try: 684 | try: 685 | if context == WebDriverContext.NATIVE_APP: 686 | self.get_web_driver().create_touch_action().long_press(self._selenium_element()).move_to( 687 | x=x_offset, y=y_offset).release().perform() 688 | else: 689 | self.get_web_driver().create_action_chains().click_and_hold(self._selenium_element()).move_by_offset( 690 | x_offset, y_offset).release().perform() 691 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 692 | self.wait_for().visible() 693 | if context == WebDriverContext.NATIVE_APP: 694 | self.get_web_driver().create_touch_action().long_press(self._selenium_element()).move_to( 695 | x=x_offset, y=y_offset).release().perform() 696 | else: 697 | self.get_web_driver().create_action_chains().click_and_hold(self._selenium_element()).move_by_offset( 698 | x_offset, y_offset).release().perform() 699 | except SeleniumWebDriverException as wde: 700 | raise EasyiumException(wde.msg, self) 701 | 702 | def drag_and_drop_to(self, target_element: "Element"): 703 | """ 704 | Drag and drop to target element. 705 | 706 | :param target_element: the target element to drop 707 | """ 708 | context = self.get_web_driver_info().context 709 | try: 710 | try: 711 | if context == WebDriverContext.NATIVE_APP: 712 | self.get_web_driver().create_touch_action().long_press(self._selenium_element()).move_to( 713 | target_element._selenium_element()).release().perform() 714 | else: 715 | self.get_web_driver().create_action_chains().click_and_hold( 716 | self._selenium_element()).move_to_element(target_element._selenium_element()).release().perform() 717 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 718 | self.wait_for().visible() 719 | target_element.wait_for().visible() 720 | if context == WebDriverContext.NATIVE_APP: 721 | self.get_web_driver().create_touch_action().long_press(self._selenium_element()).move_to( 722 | target_element._selenium_element()).release().perform() 723 | else: 724 | self.get_web_driver().create_action_chains().click_and_hold( 725 | self._selenium_element()).move_to_element(target_element._selenium_element()).release().perform() 726 | except SeleniumWebDriverException as wde: 727 | raise EasyiumException(wde.msg, self) 728 | 729 | def drag_and_drop_to_with_offset(self, target_element: "Element", x_offset: float, y_offset: float): 730 | """ 731 | Drag and drop to target element with offset. 732 | The origin is at the top-left corner of web driver and offsets are relative to the top-left corner of the target element. 733 | 734 | :param target_element: the target element to drop 735 | :param x_offset: X offset to drop 736 | :param y_offset: Y offset to drop 737 | """ 738 | context = self.get_web_driver_info().context 739 | try: 740 | try: 741 | if context == WebDriverContext.NATIVE_APP: 742 | self.get_web_driver().create_touch_action().long_press(self._selenium_element()).move_to( 743 | target_element._selenium_element(), x_offset, y_offset).release().perform() 744 | else: 745 | self.get_web_driver().create_action_chains().click_and_hold(self._selenium_element()).move_to_element_with_offset( 746 | target_element._selenium_element(), x_offset, y_offset).release().perform() 747 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 748 | self.wait_for().visible() 749 | target_element.wait_for().visible() 750 | if context == WebDriverContext.NATIVE_APP: 751 | self.get_web_driver().create_touch_action().long_press(self._selenium_element()).move_to( 752 | target_element._selenium_element(), x_offset, y_offset).release().perform() 753 | else: 754 | self.get_web_driver().create_action_chains().click_and_hold(self._selenium_element()).move_to_element_with_offset( 755 | target_element._selenium_element(), x_offset, y_offset).release().perform() 756 | except SeleniumWebDriverException as wde: 757 | raise EasyiumException(wde.msg, self) 758 | 759 | @SupportedBy(WebDriverPlatform._MOBILE) 760 | def multiple_tap(self, count: int = 1): 761 | """ 762 | Perform a multiple-tap action on this element 763 | 764 | :param count: how many tap actions to perform on this element. 765 | """ 766 | try: 767 | try: 768 | self.get_web_driver().create_touch_action().tap(self._selenium_element(), None, None, count).perform() 769 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 770 | self.wait_for().visible() 771 | self.get_web_driver().create_touch_action().tap(self._selenium_element(), None, None, count).perform() 772 | except SeleniumWebDriverException as wde: 773 | raise EasyiumException(wde.msg, self) 774 | 775 | @SupportedBy(WebDriverPlatform._MOBILE) 776 | def tap(self): 777 | """ 778 | Perform a tap action on this element. 779 | """ 780 | self.multiple_tap(1) 781 | 782 | @SupportedBy(WebDriverPlatform._MOBILE) 783 | def double_tap(self): 784 | """ 785 | Perform a double-tap action on this element. 786 | """ 787 | self.multiple_tap(2) 788 | 789 | @SupportedBy(WebDriverPlatform._MOBILE) 790 | def long_press(self, duration: int = 1000): 791 | """ 792 | Long press on this element. 793 | 794 | :param duration: the duration of long press lasts(in ms). 795 | """ 796 | try: 797 | try: 798 | self.get_web_driver().create_touch_action().long_press(self._selenium_element(), None, None, duration).release().perform() 799 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 800 | self.wait_for().visible() 801 | self.get_web_driver().create_touch_action().long_press(self._selenium_element(), None, None, duration).release().perform() 802 | except SeleniumWebDriverException as wde: 803 | raise EasyiumException(wde.msg, self) 804 | 805 | @SupportedBy(WebDriverPlatform._MOBILE) 806 | def scroll(self, direction: str): 807 | """ 808 | Scrolls to direction in this element. 809 | 810 | :param direction: the direction to scroll, the possible values are: up, down, left, right 811 | """ 812 | try: 813 | try: 814 | scroll_params = { 815 | "direction": direction, 816 | "element": self._selenium_element().id 817 | } 818 | self.get_web_driver().execute_script("mobile: scroll", scroll_params) 819 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 820 | self.wait_for().visible() 821 | scroll_params = { 822 | "direction": direction, 823 | "element": self._selenium_element().id 824 | } 825 | self.get_web_driver().execute_script("mobile: scroll", scroll_params) 826 | except SeleniumWebDriverException as wde: 827 | raise EasyiumException(wde.msg, self) 828 | 829 | def scroll_into_view(self): 830 | """ 831 | Scrolls this element into view. 832 | """ 833 | context = self.get_web_driver_info().context 834 | try: 835 | try: 836 | if context == WebDriverContext.NATIVE_APP: 837 | scroll_params = { 838 | "element": self._selenium_element().id 839 | } 840 | self.get_web_driver().execute_script("mobile: scrollTo", scroll_params) 841 | else: 842 | self.get_web_driver().execute_script("arguments[0].scrollIntoView();", self) 843 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 844 | self.wait_for().exists() 845 | if context == WebDriverContext.NATIVE_APP: 846 | scroll_params = { 847 | "element": self._selenium_element().id 848 | } 849 | self.get_web_driver().execute_script("mobile: scrollTo", scroll_params) 850 | else: 851 | self.get_web_driver().execute_script("arguments[0].scrollIntoView();", self) 852 | except SeleniumWebDriverException as wde: 853 | raise EasyiumException(wde.msg, self) 854 | 855 | @SupportedBy(WebDriverPlatform._MOBILE) 856 | def scroll_to(self, target_element: "Element", duration: int = None): 857 | """ 858 | Scrolls from this element to another. 859 | 860 | :param target_element: the target element to be scrolled to 861 | :param duration: a duration after press and move to target element. Default is 600 ms for W3C spec. Zero for MJSONWP. 862 | """ 863 | try: 864 | try: 865 | self.get_web_driver()._selenium_web_driver().scroll(self._selenium_element(), target_element._selenium_element(), duration) 866 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 867 | self.wait_for().exists() 868 | target_element.wait_for().exists() 869 | self.get_web_driver()._selenium_web_driver().scroll(self._selenium_element(), target_element._selenium_element(), duration) 870 | except SeleniumWebDriverException as wde: 871 | raise EasyiumException(wde.msg, self) 872 | 873 | @SupportedBy(WebDriverPlatform._MOBILE) 874 | def pinch(self, percent: int = 200, steps: int = 50): 875 | """ 876 | Pinch on this element a certain amount 877 | 878 | :param percent: amount to pinch. Defaults to 200% 879 | :param steps: number of steps in the pinch action 880 | """ 881 | try: 882 | try: 883 | self.get_web_driver()._selenium_web_driver().pinch(self._selenium_element(), percent, steps) 884 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 885 | self.wait_for().visible() 886 | self.get_web_driver()._selenium_web_driver().pinch(self._selenium_element(), percent, steps) 887 | except SeleniumWebDriverException as wde: 888 | raise EasyiumException(wde.msg, self) 889 | 890 | @SupportedBy(WebDriverPlatform._MOBILE) 891 | def zoom(self, percent: int = 200, steps: int = 50): 892 | """ 893 | Zooms in on an element a certain amount. 894 | 895 | :param percent: amount to zoom. Defaults to 200% 896 | :param steps: number of steps in the zoom action 897 | """ 898 | try: 899 | try: 900 | self.get_web_driver()._selenium_web_driver().zoom(self._selenium_element(), percent, steps) 901 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 902 | self.wait_for().visible() 903 | self.get_web_driver()._selenium_web_driver().zoom(self._selenium_element(), percent, steps) 904 | except SeleniumWebDriverException as wde: 905 | raise EasyiumException(wde.msg, self) 906 | 907 | def get_screenshot_as_file(self, filename: str) -> bool: 908 | """ 909 | Gets the screenshot of the current element. Returns False if there is 910 | any IOError, else returns True. Use full paths in your filename. 911 | 912 | :param filename: The full path you wish to save your screenshot to. 913 | 914 | :Usage: 915 | element.get_screenshot_as_file('/Screenshots/foo.png') 916 | """ 917 | try: 918 | try: 919 | return self._selenium_element().screenshot(filename) 920 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 921 | self.wait_for().exists() 922 | return self._selenium_element().screenshot(filename) 923 | except SeleniumWebDriverException as wde: 924 | raise EasyiumException(wde.msg, self) 925 | 926 | def save_screenshot(self, filename: str) -> bool: 927 | """ 928 | Gets the screenshot of the current element. Returns False if there is 929 | any IOError, else returns True. Use full paths in your filename. 930 | 931 | :param filename: The full path you wish to save your screenshot to. 932 | 933 | :Usage: 934 | element.save_screenshot('/Screenshots/foo.png') 935 | """ 936 | return self.get_screenshot_as_file(filename) 937 | 938 | def get_screenshot_as_png(self) -> bytes: 939 | """ 940 | Gets the screenshot of the current element as a binary data. 941 | 942 | :Usage: 943 | element_png = element.get_screenshot_as_png() 944 | """ 945 | try: 946 | try: 947 | return self._selenium_element().screenshot_as_png 948 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 949 | self.wait_for().exists() 950 | return self._selenium_element().screenshot_as_png 951 | except SeleniumWebDriverException as wde: 952 | raise EasyiumException(wde.msg, self) 953 | 954 | def get_screenshot_as_base64(self) -> str: 955 | """ 956 | Gets the screenshot of the current element as a base64 encoded string. 957 | 958 | :Usage: 959 | img_b64 = element.get_screenshot_as_base64() 960 | """ 961 | try: 962 | try: 963 | return self._selenium_element().screenshot_as_base64 964 | except (NoSuchElementException, SeleniumStaleElementReferenceException): 965 | self.wait_for().exists() 966 | return self._selenium_element().screenshot_as_base64 967 | except SeleniumWebDriverException as wde: 968 | raise EasyiumException(wde.msg, self) 969 | 970 | def is_displayed(self) -> bool: 971 | """ 972 | Return whether this element is displayed or not. 973 | """ 974 | try: 975 | try: 976 | return self._selenium_element().is_displayed() 977 | except SeleniumStaleElementReferenceException: 978 | self._refresh() 979 | return self._selenium_element().is_displayed() 980 | except NoSuchElementException: 981 | return False 982 | except SeleniumWebDriverException as wde: 983 | raise EasyiumException(wde.msg, self) 984 | 985 | def exists(self) -> bool: 986 | """ 987 | Return whether this element is existing or not. 988 | """ 989 | try: 990 | try: 991 | self._selenium_element().is_displayed() 992 | return True 993 | except SeleniumStaleElementReferenceException: 994 | self._refresh() 995 | return True 996 | except NoSuchElementException: 997 | return False 998 | except SeleniumWebDriverException as wde: 999 | raise EasyiumException(wde.msg, self) 1000 | -------------------------------------------------------------------------------- /easyium/enumeration.py: -------------------------------------------------------------------------------- 1 | class WebDriverContext: 2 | IE = "ie" 3 | FIREFOX = "firefox" 4 | CHROME = "chrome" 5 | OPERA = "opera" 6 | SAFARI = "safari" 7 | EDGE = "edge" 8 | 9 | NATIVE_APP = "native_app" 10 | WEB_VIEW = "web_view" 11 | 12 | _WEB = [IE, FIREFOX, CHROME, OPERA, SAFARI, EDGE, WEB_VIEW] 13 | _APP = [NATIVE_APP, WEB_VIEW] 14 | 15 | 16 | class WebDriverPlatform: 17 | PC = "pc" 18 | ANDROID = "android" 19 | IOS = "ios" 20 | 21 | _MOBILE = [ANDROID, IOS] 22 | -------------------------------------------------------------------------------- /easyium/exceptions.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import TYPE_CHECKING 3 | 4 | if TYPE_CHECKING: 5 | from .context import Context 6 | 7 | filter_msg_regex = re.compile(r"\n \(Session info:.*?\)\n \(Driver info:.*?\(.*?\).*?\)") 8 | 9 | 10 | class EasyiumException(Exception): 11 | def __init__(self, msg: str = None, context: "Context" = None): 12 | # Remove Session info and Driver info of the message. 13 | self.msg = filter_msg_regex.sub("", msg) 14 | self.message = self.msg 15 | self.context = context 16 | 17 | def __str__(self): 18 | exception_msg = "" 19 | if self.msg is not None: 20 | exception_msg = self.msg 21 | if self.context is not None: 22 | exception_msg += "\n" + str(self.context) 23 | return exception_msg 24 | 25 | 26 | class TimeoutException(EasyiumException): 27 | pass 28 | 29 | 30 | class ElementTimeoutException(TimeoutException): 31 | pass 32 | 33 | 34 | class WebDriverTimeoutException(TimeoutException): 35 | pass 36 | 37 | 38 | class NoSuchElementException(EasyiumException): 39 | pass 40 | 41 | 42 | class NotPersistException(EasyiumException): 43 | pass 44 | 45 | 46 | class LatePersistException(EasyiumException): 47 | pass 48 | 49 | 50 | class InvalidLocatorException(EasyiumException): 51 | pass 52 | 53 | 54 | class UnsupportedOperationException(EasyiumException): 55 | pass 56 | -------------------------------------------------------------------------------- /easyium/identifier.py: -------------------------------------------------------------------------------- 1 | class Identifier: 2 | id = staticmethod(lambda element: "id=" + element.get_attribute("id")) 3 | 4 | class_name = staticmethod(lambda element: "class=" + element.get_attribute("class")) 5 | 6 | name = staticmethod(lambda element: "name=" + element.get_attribute("name")) 7 | 8 | text = staticmethod(lambda element: "xpath=.//*[.='%s')]" % element.get_text()) 9 | -------------------------------------------------------------------------------- /easyium/locator.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from appium.webdriver.common.mobileby import MobileBy 4 | from selenium.webdriver.common.by import By 5 | 6 | from .exceptions import InvalidLocatorException 7 | 8 | locator_to_by_map = { 9 | "id": By.ID, 10 | "xpath": By.XPATH, 11 | "link": By.LINK_TEXT, 12 | "partial_link": By.PARTIAL_LINK_TEXT, 13 | "name": By.NAME, 14 | "tag": By.TAG_NAME, 15 | "class": By.CLASS_NAME, 16 | "css": By.CSS_SELECTOR, 17 | "ios_pre": MobileBy.IOS_PREDICATE, 18 | "ios_ui": MobileBy.IOS_UIAUTOMATION, 19 | "ios_class": MobileBy.IOS_CLASS_CHAIN, 20 | "android_ui": MobileBy.ANDROID_UIAUTOMATOR, 21 | "android_tag": MobileBy.ANDROID_VIEWTAG, 22 | "android_data": MobileBy.ANDROID_DATA_MATCHER, 23 | "acc_id": MobileBy.ACCESSIBILITY_ID, 24 | "custom": MobileBy.CUSTOM 25 | } 26 | 27 | 28 | def locator_to_by_value(locator: str) -> Tuple[By, str]: 29 | separator_index = locator.find("=") 30 | if separator_index == -1: 31 | raise InvalidLocatorException("Separator '=' is not found.") 32 | by = locator[:separator_index] 33 | value = locator[separator_index + 1:] 34 | try: 35 | by = locator_to_by_map[by] 36 | except KeyError: 37 | raise InvalidLocatorException("The by <%s> of locator <%s> is not a valid By." % (by, locator)) 38 | return by, value 39 | -------------------------------------------------------------------------------- /easyium/static_element.py: -------------------------------------------------------------------------------- 1 | from .context import Context 2 | from .element import Element 3 | 4 | 5 | class StaticElement(Element): 6 | def __init__(self, parent: Context, locator: str): 7 | """ 8 | Creates a new instance of the StaticElement. 9 | 10 | :param parent: the parent context 11 | :param locator: 12 | the locator of this element (relative to parent context). 13 | The format of locator is: "by=value", the possible values of "by" are:: 14 | 15 | "id": By.ID 16 | "xpath": By.XPATH 17 | "link": By.LINK_TEXT 18 | "partial_link": By.PARTIAL_LINK_TEXT 19 | "name": By.NAME 20 | "tag": By.TAG_NAME 21 | "class": By.CLASS_NAME 22 | "css": By.CSS_SELECTOR 23 | "ios_pre": MobileBy.IOS_PREDICATE 24 | "ios_ui": MobileBy.IOS_UIAUTOMATION 25 | "ios_class": MobileBy.IOS_CLASS_CHAIN 26 | "android_ui": MobileBy.ANDROID_UIAUTOMATOR 27 | "android_tag": MobileBy.ANDROID_VIEWTAG 28 | "android_data": MobileBy.ANDROID_DATA_MATCHER 29 | "acc_id": MobileBy.ACCESSIBILITY_ID 30 | "custom": MobileBy.CUSTOM 31 | """ 32 | Element.__init__(self, parent) 33 | # from element 34 | self._inner_selenium_element = None 35 | self._locator = locator 36 | 37 | def _refresh(self): 38 | self._inner_selenium_element = None 39 | self._inner_selenium_element = self.get_parent()._find_selenium_element(self._locator) 40 | 41 | def persist(self): 42 | self.get_parent().persist() 43 | 44 | def __str__(self): 45 | if self._inner_selenium_element is None: 46 | return "%s\n|- StaticElement " % ( 47 | self.get_parent(), None, self._locator) 48 | else: 49 | return "%s\n|- StaticElement " % ( 50 | self.get_parent(), self._inner_selenium_element.id, self._locator) 51 | -------------------------------------------------------------------------------- /easyium/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from urllib.parse import urlparse 3 | 4 | from .waiter import Waiter 5 | 6 | 7 | def wait_for_server_started(server_url: str, interval: int = 1000, timeout: int = 30000): 8 | """ 9 | Wait for the remote server to be started. 10 | 11 | :param server_url: the url of the remote server, e.g.,"http://127.0.0.1:4728/wd/hub" 12 | :param interval: the wait interval (in milliseconds) 13 | :param timeout: the wait timeout (in milliseconds) 14 | """ 15 | url_components = urlparse(server_url) 16 | server_host = url_components.hostname 17 | server_port = url_components.port 18 | 19 | def server_started(): 20 | socket_ = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 21 | try: 22 | socket_.settimeout(1) 23 | socket_.connect((server_host, server_port)) 24 | return True 25 | except socket.error: 26 | return False 27 | finally: 28 | socket_.close() 29 | 30 | Waiter(interval, timeout).wait_for(server_started) 31 | -------------------------------------------------------------------------------- /easyium/waiter.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Callable, List, TYPE_CHECKING 3 | 4 | from selenium.common.exceptions import NoSuchElementException as SeleniumNoSuchElementException, StaleElementReferenceException as SeleniumStaleElementReferenceException, \ 5 | WebDriverException as SeleniumWebDriverException 6 | from selenium.webdriver.common.by import By 7 | 8 | from .decorator import SupportedBy 9 | from .enumeration import WebDriverPlatform 10 | from .exceptions import TimeoutException, ElementTimeoutException, WebDriverTimeoutException 11 | 12 | if TYPE_CHECKING: 13 | from .web_driver import WebDriver 14 | from .element import Element 15 | 16 | 17 | class Waiter: 18 | def __init__(self, interval: int = 1000, timeout: int = 30000): 19 | """ 20 | Create a Waiter instance. 21 | 22 | :param interval: the wait interval (in milliseconds) 23 | :param timeout: the wait timeout (in milliseconds) 24 | """ 25 | self.__interval = interval 26 | self.__timeout = timeout 27 | 28 | def wait_for(self, condition_function: Callable[[any], bool], *function_args, **function_kwargs): 29 | """ 30 | Wait for the condition. 31 | 32 | :param condition_function: the condition function 33 | :param function_args: the args for condition_function 34 | :param function_kwargs: the kwargs for condition_function 35 | """ 36 | start_time = time.time() * 1000.0 37 | 38 | if condition_function(*function_args, **function_kwargs): 39 | return 40 | 41 | while (time.time() * 1000.0 - start_time) <= self.__timeout: 42 | time.sleep(self.__interval / 1000.0) 43 | if condition_function(*function_args, **function_kwargs): 44 | return 45 | 46 | raise TimeoutException("Timed out waiting for <%s>." % condition_function.__name__) 47 | 48 | 49 | class ElementWaitFor: 50 | def __init__(self, element: "Element", interval: int, timeout: int): 51 | self.__element = element 52 | self.__desired_occurrence = True 53 | self.__interval = interval 54 | self.__timeout = timeout 55 | 56 | def _get_element(self) -> "Element": 57 | return self.__element 58 | 59 | def __wait_for(self, element_condition: "ElementCondition", interval: int, timeout: int) -> Waiter: 60 | def is_element_condition_occurred(): 61 | return element_condition.occurred() == self.__desired_occurrence 62 | 63 | try: 64 | Waiter(interval, timeout).wait_for(is_element_condition_occurred) 65 | except TimeoutException: 66 | raise ElementTimeoutException( 67 | "Timed out waiting for <%s> to be <%s>." % (element_condition, self.__desired_occurrence)) 68 | 69 | def not_(self) -> "ElementWaitFor": 70 | """ 71 | Wait for not. 72 | """ 73 | self.__desired_occurrence = not self.__desired_occurrence 74 | return self 75 | 76 | def exists(self): 77 | """ 78 | Wait for this element exists. 79 | """ 80 | self.__wait_for(ElementExistence(self.__element), self.__interval, self.__timeout) 81 | 82 | def visible(self): 83 | """ 84 | Wait for this element visible. 85 | """ 86 | self.__wait_for(ElementVisible(self.__element), self.__interval, self.__timeout) 87 | 88 | def text_equals(self, text: str): 89 | """ 90 | Wait for this element's text equals the expected text. 91 | 92 | :param text: the expected text 93 | 94 | :Usage: 95 | # wait for text not empty 96 | StaticElement(driver, "id=change_text").wait_for().not_().text_equals("") 97 | """ 98 | start_time = time.time() * 1000.0 99 | self.__element.wait_for(self.__interval, self.__timeout).exists() 100 | rest_timeout = start_time + self.__timeout - time.time() * 1000.0 101 | self.__wait_for(ElementTextEquals(self.__element, text), self.__interval, rest_timeout) 102 | 103 | def attribute_equals(self, attribute: str, value: str): 104 | """ 105 | Wait for this element's attribute value equals the expected value. 106 | 107 | :param attribute: the attribute of this element. 108 | :param value: the expected value. 109 | 110 | :Usage: 111 | element.wait_for().attribute_equals("class", "foo bar") 112 | """ 113 | start_time = time.time() * 1000.0 114 | self.__element.wait_for(self.__interval, self.__timeout).exists() 115 | rest_timeout = start_time + self.__timeout - time.time() * 1000.0 116 | self.__wait_for(ElementAttributeEquals(self.__element, attribute, value), self.__interval, rest_timeout) 117 | 118 | def attribute_contains_one(self, attribute: str, *values: str): 119 | """ 120 | Wait for this element's attribute value contains one of the value list. 121 | 122 | :param attribute: the attribute of this element. 123 | :param values: the expected value list. 124 | 125 | :Usage: 126 | element.wait_for().attribute_contains_one("class", "foo", "bar") 127 | element.wait_for().attribute_contains_one("class", ["foo", "bar"]) 128 | element.wait_for().attribute_contains_one("class", ("foo", "bar")) 129 | """ 130 | start_time = time.time() * 1000.0 131 | self.__element.wait_for(self.__interval, self.__timeout).exists() 132 | rest_timeout = start_time + self.__timeout - time.time() * 1000.0 133 | self.__wait_for(ElementAttributeContainsOne(self.__element, attribute, *values), self.__interval, rest_timeout) 134 | 135 | def attribute_contains_all(self, attribute: str, *values: str): 136 | """ 137 | Wait for this element's attribute value contains all of the value list. 138 | 139 | :param attribute: the attribute of this element. 140 | :param values: the expected value list. 141 | 142 | :Usage: 143 | element.wait_for().attribute_contains_all("class", "foo", "bar") 144 | element.wait_for().attribute_contains_all("class", ["foo", "bar"]) 145 | element.wait_for().attribute_contains_all("class", ("foo", "bar")) 146 | """ 147 | start_time = time.time() * 1000.0 148 | self.__element.wait_for(self.__interval, self.__timeout).exists() 149 | rest_timeout = start_time + self.__timeout - time.time() * 1000.0 150 | self.__wait_for(ElementAttributeContainsAll(self.__element, attribute, *values), self.__interval, rest_timeout) 151 | 152 | 153 | class ElementCondition: 154 | def occurred(self): 155 | pass 156 | 157 | 158 | class ElementExistence(ElementCondition): 159 | def __init__(self, element: "Element"): 160 | self.__element = element 161 | 162 | def occurred(self) -> bool: 163 | return self.__element.exists() 164 | 165 | def __str__(self): 166 | return "ElementExistence [\n%s\n]" % self.__element 167 | 168 | 169 | class ElementVisible(ElementCondition): 170 | def __init__(self, element: "Element"): 171 | self.__element = element 172 | 173 | def occurred(self) -> bool: 174 | return self.__element.is_displayed() 175 | 176 | def __str__(self): 177 | return "ElementVisible [\n%s\n]" % self.__element 178 | 179 | 180 | class ElementTextEquals(ElementCondition): 181 | def __init__(self, element: "Element", text: str): 182 | self.__element = element 183 | self.__text = text 184 | 185 | def occurred(self) -> bool: 186 | return self.__element._selenium_element().text == self.__text 187 | 188 | def __str__(self): 189 | return "ElementTextEquals [element: \n%s\n][text: %s]" % (self.__element, self.__text) 190 | 191 | 192 | class ElementAttributeEquals(ElementCondition): 193 | def __init__(self, element: "Element", attribute: str, value: str): 194 | self.__element = element 195 | self.__attribute = attribute 196 | self.__value = value 197 | 198 | def occurred(self) -> bool: 199 | return self.__element._selenium_element().get_attribute(self.__attribute) == self.__value 200 | 201 | def __str__(self): 202 | return "ElementAttributeEquals [element: \n%s\n][attribute: %s][value: %s]" % ( 203 | self.__element, self.__attribute, self.__value) 204 | 205 | 206 | class ElementAttributeContainsOne(ElementCondition): 207 | def __init__(self, element: "Element", attribute: str, *values: str): 208 | self.__element = element 209 | self.__attribute = attribute 210 | self.__values = [] 211 | for value in values: 212 | if isinstance(value, (tuple, list)): 213 | self.__values.extend(value) 214 | else: 215 | self.__values.append(value) 216 | 217 | def occurred(self) -> bool: 218 | attribute_value = self.__element._selenium_element().get_attribute(self.__attribute) 219 | for value in self.__values: 220 | if value in attribute_value: 221 | return True 222 | return False 223 | 224 | def __str__(self): 225 | return "ElementAttributeContainsOne [element: \n%s\n][attribute: %s][values: %s]" % ( 226 | self.__element, self.__attribute, self.__values) 227 | 228 | 229 | class ElementAttributeContainsAll(ElementCondition): 230 | def __init__(self, element: "Element", attribute: str, *values: str): 231 | self.__element = element 232 | self.__attribute = attribute 233 | self.__values = [] 234 | for value in values: 235 | if isinstance(value, (tuple, list)): 236 | self.__values.extend(value) 237 | else: 238 | self.__values.append(value) 239 | 240 | def occurred(self) -> bool: 241 | attribute_value = self.__element._selenium_element().get_attribute(self.__attribute) 242 | for value in self.__values: 243 | if value not in attribute_value: 244 | return False 245 | return True 246 | 247 | def __str__(self): 248 | return "ElementAttributeContainsAll [element: \n%s\n][attribute: %s][values: %s]" % ( 249 | self.__element, self.__attribute, self.__values) 250 | 251 | 252 | class WebDriverWaitFor: 253 | def __init__(self, web_driver: "WebDriver", interval: int, timeout: int): 254 | self.__web_driver = web_driver 255 | self.__desired_occurrence = True 256 | self.__waiter = Waiter(interval, timeout) 257 | 258 | def _get_web_driver(self) -> "WebDriver": 259 | return self.__web_driver 260 | 261 | def __wait_for(self, web_driver_condition: "WebDriverCondition"): 262 | def is_web_driver_condition_occurred(): 263 | return web_driver_condition.occurred() == self.__desired_occurrence 264 | 265 | try: 266 | self.__waiter.wait_for(is_web_driver_condition_occurred) 267 | except TimeoutException: 268 | raise WebDriverTimeoutException( 269 | "Timed out waiting for <%s> to be <%s>." % (web_driver_condition, self.__desired_occurrence)) 270 | 271 | def not_(self) -> "WebDriverWaitFor": 272 | """ 273 | Wait for not. 274 | """ 275 | self.__desired_occurrence = not self.__desired_occurrence 276 | return self 277 | 278 | def alert_present(self): 279 | """ 280 | Wait for the alert present. 281 | """ 282 | self.__wait_for(AlertPresent(self.__web_driver)) 283 | 284 | def text_present(self, text: str): 285 | """ 286 | Wait for the text present. 287 | 288 | :param text: the text to wait 289 | """ 290 | self.__wait_for(TextPresent(self.__web_driver, text)) 291 | 292 | def url_equals(self, url: str): 293 | """ 294 | Wait for the url equals expected url. 295 | 296 | :param url: the expected url 297 | 298 | :Usage: 299 | # wait for url changed 300 | previous_url = driver.get_current_url() 301 | StaticElement(driver, "id=change_url").click() # url changed 302 | driver.wait_for().not_().url_equals(previous_url) 303 | """ 304 | self.__wait_for(URLEquals(self.__web_driver, url)) 305 | 306 | def reloaded(self, indicator: "Element"): 307 | """ 308 | Wait for the page to be refreshed / redirected. 309 | 310 | :param indicator: the indicator element, it should be a DynamicElement 311 | 312 | :Usage: 313 | # usually we use body as indicator, the indicator should be DynamicElement 314 | indicator = driver.find_element("tag=body") 315 | StaticElement(driver, "id=reload_after_2_seconds").click() # reload after 2 seconds 316 | driver.wait_for().reloaded(indicator) 317 | """ 318 | self.__wait_for(Reloaded(self.__web_driver, indicator)) 319 | 320 | @SupportedBy(WebDriverPlatform.ANDROID) 321 | def activity_present(self, activity: str): 322 | """ 323 | Wait for the activity present. 324 | 325 | :param activity: the activity to wait 326 | """ 327 | self.__wait_for(ActivityPresent(self.__web_driver, activity)) 328 | 329 | @SupportedBy(WebDriverPlatform._MOBILE) 330 | def context_available(self, context_partial_name: str): 331 | """ 332 | Wait for the context available. 333 | 334 | :param context_partial_name: the partial name of the context 335 | """ 336 | self.__wait_for(ContextAvailable(self.__web_driver, context_partial_name)) 337 | 338 | 339 | class WebDriverCondition: 340 | def occurred(self): 341 | pass 342 | 343 | 344 | class AlertPresent(WebDriverCondition): 345 | def __init__(self, web_driver: "WebDriver"): 346 | self.__web_driver = web_driver 347 | 348 | def occurred(self) -> bool: 349 | try: 350 | alert_text = self.__web_driver._selenium_web_driver().switch_to.alert.text 351 | return True 352 | except SeleniumWebDriverException as e: 353 | return False 354 | 355 | def __str__(self): 356 | return "AlertPresent [\n%s\n]" % self.__web_driver 357 | 358 | 359 | class TextPresent(WebDriverCondition): 360 | def __init__(self, web_driver: "WebDriver", text: str): 361 | self.__web_driver = web_driver 362 | self.__text = text 363 | 364 | def occurred(self) -> bool: 365 | try: 366 | self.__web_driver._selenium_web_driver().find_element(By.XPATH, "//*[contains(., '%s')]" % self.__text) 367 | return True 368 | except SeleniumNoSuchElementException: 369 | return False 370 | 371 | def __str__(self): 372 | return "TextPresent [webdriver: \n%s\n][text: %s]" % (self.__web_driver, self.__text) 373 | 374 | 375 | class URLEquals(WebDriverCondition): 376 | def __init__(self, web_driver: "WebDriver", url: str): 377 | self.__web_driver = web_driver 378 | self.__url = url 379 | 380 | def occurred(self) -> bool: 381 | return self.__web_driver._selenium_web_driver().current_url == self.__url 382 | 383 | def __str__(self): 384 | return "URLEquals [webdriver: \n%s\n][url: %s]" % (self.__web_driver, self.__url) 385 | 386 | 387 | class Reloaded(WebDriverCondition): 388 | def __init__(self, web_driver: "WebDriver", indicator: "Element"): 389 | self.__web_driver = web_driver 390 | self.__indicator = indicator 391 | 392 | def occurred(self) -> bool: 393 | try: 394 | self.__indicator._selenium_element().is_displayed() 395 | return False 396 | except SeleniumStaleElementReferenceException: 397 | return True 398 | 399 | def __str__(self): 400 | return "Reloaded [\n%s\n]" % self.__web_driver 401 | 402 | 403 | class ActivityPresent(WebDriverCondition): 404 | def __init__(self, web_driver: "WebDriver", activity: str): 405 | self.__web_driver = web_driver 406 | self.__activity = activity 407 | 408 | def occurred(self) -> bool: 409 | return self.__web_driver._selenium_web_driver().current_activity == self.__activity 410 | 411 | def __str__(self): 412 | return "ActivityPresent [webdriver: \n%s\n][activity: %s]" % (self.__web_driver, self.__activity) 413 | 414 | 415 | class ContextAvailable(WebDriverCondition): 416 | def __init__(self, web_driver: "WebDriver", context_partial_name: str): 417 | self.__web_driver = web_driver 418 | self.__context_partial_name = context_partial_name 419 | 420 | def occurred(self) -> bool: 421 | try: 422 | contexts = self.__web_driver._selenium_web_driver().contexts 423 | return len([context for context in contexts if self.__context_partial_name in context]) > 0 424 | except SeleniumWebDriverException as e: 425 | return False 426 | 427 | def __str__(self): 428 | return "ContextAvailable [webdriver: \n%s\n][context partial name: %s]" % ( 429 | self.__web_driver, self.__context_partial_name) 430 | -------------------------------------------------------------------------------- /easyium/web_driver.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union, TYPE_CHECKING 2 | 3 | from appium.webdriver.clipboard_content_type import ClipboardContentType 4 | from appium.webdriver.common.multi_action import MultiAction 5 | from appium.webdriver.common.touch_action import TouchAction 6 | from appium.webdriver.webdriver import WebDriver as AppiumWebDriver 7 | from selenium.common.exceptions import WebDriverException 8 | from selenium.webdriver import ActionChains, Ie as SeleniumIe, Firefox as SeleniumFirefox, Chrome as SeleniumChrome, Opera as SeleniumOpera, \ 9 | Safari as SeleniumSafari, Edge as SeleniumEdge 10 | from selenium.webdriver.chrome.options import Options as ChromeOptions 11 | from selenium.webdriver.chrome.service import Service as ChromeService 12 | from selenium.webdriver.common.html5.application_cache import ApplicationCache 13 | from selenium.webdriver.edge.options import Options as EdgeOptions 14 | from selenium.webdriver.edge.service import Service as EdgeService 15 | from selenium.webdriver.firefox.options import Options as FirefoxOptions 16 | from selenium.webdriver.firefox.service import Service as FirefoxService 17 | from selenium.webdriver.ie.options import Options as IeOptions 18 | from selenium.webdriver.ie.service import Service as IeService 19 | from selenium.webdriver.opera.options import Options as OperaOptions 20 | from selenium.webdriver.safari.options import Options as SafariOptions 21 | from selenium.webdriver.safari.service import Service as SafariService 22 | 23 | from .alert import Alert 24 | from .context import Context 25 | from .decorator import SupportedBy 26 | from .enumeration import WebDriverPlatform, WebDriverContext 27 | from .waiter import WebDriverWaitFor 28 | 29 | if TYPE_CHECKING: 30 | from .element import Element 31 | 32 | 33 | class WebDriverInfo: 34 | def __init__(self, platform: WebDriverPlatform, context: WebDriverContext): 35 | self.platform = platform 36 | self.context = context 37 | 38 | 39 | class WebDriver(Context): 40 | def __init__(self, selenium_web_driver: AppiumWebDriver, web_driver_info: WebDriverInfo): 41 | """ 42 | Create a wrapper for selenium WebDriver. 43 | 44 | :param selenium_web_driver: the selenium web driver instance 45 | :param web_driver_info: the web driver info 46 | """ 47 | Context.__init__(self) 48 | self.__selenium_web_driver = selenium_web_driver 49 | self.__web_driver_info = web_driver_info 50 | 51 | # set default wait interval and timeout 52 | self.set_wait_interval(1000) 53 | self.set_wait_timeout(30000) 54 | 55 | def _selenium_context(self) -> "AppiumWebDriver": 56 | return self.__selenium_web_driver 57 | 58 | def _selenium_web_driver(self) -> AppiumWebDriver: 59 | return self.__selenium_web_driver 60 | 61 | def get_web_driver(self) -> "WebDriver": 62 | """ 63 | Get self. 64 | 65 | :return: self 66 | """ 67 | return self 68 | 69 | def get_web_driver_info(self) -> WebDriverInfo: 70 | """ 71 | Get current info of this web driver. 72 | 73 | :return: the web driver info 74 | """ 75 | return self.__web_driver_info 76 | 77 | def get_desired_capabilities(self) -> dict: 78 | """ 79 | Returns the drivers current desired capabilities being used. 80 | """ 81 | return self._selenium_web_driver().capabilities 82 | 83 | def get_application_cache(self) -> ApplicationCache: 84 | """ 85 | Returns a ApplicationCache Object to interact with the browser app cache. 86 | """ 87 | return self._selenium_web_driver().application_cache 88 | 89 | def quit(self): 90 | """ 91 | Quits the driver and closes every associated window. 92 | """ 93 | self._selenium_web_driver().quit() 94 | 95 | def create_action_chains(self) -> ActionChains: 96 | """ 97 | Create a new selenium.webdriver.common.ActionChains instance. 98 | """ 99 | return ActionChains(self._selenium_web_driver()) 100 | 101 | @SupportedBy(WebDriverPlatform._MOBILE) 102 | def create_touch_action(self) -> TouchAction: 103 | """ 104 | Create a new appium.webdriver.common.TouchAction instance. 105 | """ 106 | return TouchAction(self._selenium_web_driver()) 107 | 108 | @SupportedBy(WebDriverPlatform._MOBILE) 109 | def create_multi_action(self) -> MultiAction: 110 | """ 111 | Create a new appium.webdriver.common.MultiAction instance. 112 | """ 113 | return MultiAction(self._selenium_web_driver()) 114 | 115 | def wait_for(self, interval: int = None, timeout: int = None) -> WebDriverWaitFor: 116 | """ 117 | Get a WebDriverWaitFor instance. 118 | 119 | :param interval: the wait interval (in milliseconds). If None, use driver's wait interval. 120 | :param timeout: the wait timeout (in milliseconds). If None, use driver's wait interval. 121 | """ 122 | _interval = self.get_wait_interval() if interval is None else interval 123 | _timeout = self.get_wait_timeout() if timeout is None else timeout 124 | return WebDriverWaitFor(self, _interval, _timeout) 125 | 126 | # Timeouts 127 | 128 | def set_page_load_timeout(self, timeout: int): 129 | """ 130 | Set the amount of time to wait for a page load to complete before throwing an error. 131 | 132 | :param timeout: The amount of time to wait (in milliseconds) 133 | """ 134 | self._selenium_web_driver().set_page_load_timeout(timeout / 1000.0) 135 | 136 | def set_script_timeout(self, timeout: int): 137 | """ 138 | Set the amount of time that the script should wait during an execute_async_script call before throwing an error. 139 | 140 | :param timeout: The amount of time to wait (in milliseconds) 141 | """ 142 | self._selenium_web_driver().set_script_timeout(timeout / 1000.0) 143 | 144 | # Execute script 145 | 146 | def execute_script(self, script: str, *args) -> any: 147 | """ 148 | Synchronously Executes JavaScript in the current window/frame. 149 | 150 | :param script: The JavaScript to execute 151 | :param args: Any applicable arguments for your JavaScript 152 | :return: the return value of JavaScript 153 | """ 154 | from .element import Element 155 | 156 | converted_args = [] 157 | for arg in args: 158 | if isinstance(arg, Element): 159 | arg.wait_for().exists() 160 | converted_args.append(arg._selenium_element()) 161 | else: 162 | converted_args.append(arg) 163 | 164 | return self._selenium_web_driver().execute_script(script, *converted_args) 165 | 166 | def execute_async_script(self, script: str, *args) -> any: 167 | """ 168 | Asynchronously Executes JavaScript in the current window/frame. 169 | 170 | :param script: The JavaScript to execute 171 | :param args: Any applicable arguments for your JavaScript 172 | :return: the return value of JavaScript 173 | """ 174 | from .element import Element 175 | 176 | converted_args = [] 177 | for arg in args: 178 | if isinstance(arg, Element): 179 | arg.wait_for().exists() 180 | converted_args.append(arg._selenium_element()) 181 | else: 182 | converted_args.append(arg) 183 | 184 | return self._selenium_web_driver().execute_async_script(script, *converted_args) 185 | 186 | # Orientation 187 | 188 | @SupportedBy(WebDriverPlatform._MOBILE) 189 | def get_orientation(self) -> str: 190 | """ 191 | Gets the current orientation of the device 192 | """ 193 | return self._selenium_web_driver().orientation 194 | 195 | @SupportedBy(WebDriverPlatform._MOBILE) 196 | def set_orientation(self, value: str): 197 | """ 198 | Sets the current orientation of the device 199 | 200 | :param value: orientation to set it to, allowed_values: LANDSCAPE, PORTRAIT 201 | """ 202 | self._selenium_web_driver().orientation = value.upper() 203 | 204 | # Geolocation 205 | 206 | @SupportedBy(WebDriverPlatform._MOBILE) 207 | def get_location(self) -> dict: 208 | """ 209 | Retrieves the current location 210 | 211 | :return: A dictionary whose keys are 212 | - latitude (float) 213 | - longitude (float) 214 | - altitude (float) 215 | """ 216 | return self._selenium_web_driver().location 217 | 218 | @SupportedBy(WebDriverPlatform._MOBILE) 219 | def set_location(self, latitude, longitude, altitude): 220 | """ 221 | Set the location of the device 222 | 223 | :param latitude: String or numeric value between -90.0 and 90.00 224 | :param longitude: String or numeric value between -180.0 and 180.0 225 | :param altitude: String or numeric value 226 | """ 227 | self._selenium_web_driver().set_location(latitude, longitude, altitude) 228 | 229 | # Logs 230 | 231 | def get_log_types(self) -> List[str]: 232 | """ 233 | Gets a list of the available log types. 234 | """ 235 | return self._selenium_web_driver().log_types 236 | 237 | def get_log(self, log_type: str) -> List[dict]: 238 | """ 239 | Gets the log for a given log type 240 | 241 | :param log_type: type of log that which will be returned 242 | 243 | :Usage: 244 | driver.get_log('browser') 245 | driver.get_log('driver') 246 | driver.get_log('client') 247 | driver.get_log('server') 248 | """ 249 | return self._selenium_web_driver().get_log(log_type) 250 | 251 | # Settings 252 | 253 | @SupportedBy(WebDriverPlatform._MOBILE) 254 | def get_settings(self) -> dict: 255 | """ 256 | Returns the appium server Settings for the current session. 257 | Do not get Settings confused with Desired Capabilities, they are 258 | separate concepts. See https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md 259 | """ 260 | return self._selenium_web_driver().get_settings() 261 | 262 | @SupportedBy(WebDriverPlatform._MOBILE) 263 | def update_settings(self, settings: dict): 264 | """ 265 | Set settings for the current session. 266 | For more on settings, see: https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md 267 | 268 | :param settings: dictionary of settings to apply to the current test session 269 | """ 270 | self._selenium_web_driver().update_settings(settings) 271 | 272 | # Activity 273 | 274 | @SupportedBy(WebDriverPlatform.ANDROID) 275 | def start_activity(self, app_package: str, app_activity: str, app_wait_package: str = None, app_wait_activity: str = None, intent_action: str = None, 276 | intent_category: str = None, intent_flags: str = None, optional_intent_arguments: str = None, stop_app_on_reset: str = None): 277 | """ 278 | Opens an arbitrary activity during a test. If the activity belongs to 279 | another application, that application is started and the activity is opened. 280 | 281 | This is an Android-only method. 282 | 283 | :param app_package: The package containing the activity to start. 284 | :param app_activity: The activity to start. 285 | :param app_wait_package: Begin automation after this package starts. 286 | :param app_wait_activity: Begin automation after this activity starts. 287 | :param intent_action: Intent to start. 288 | :param intent_category: Intent category to start. 289 | :param intent_flags: Flags to send to the intent. 290 | :param optional_intent_arguments: Optional arguments to the intent. 291 | :param stop_app_on_reset: Whether the app should be stopped on reset or not 292 | """ 293 | options = { 294 | "app_wait_package": app_wait_package, 295 | "app_wait_activity": app_wait_activity, 296 | "intent_action": intent_action, 297 | "intent_category": intent_category, 298 | "intent_flags": intent_flags, 299 | "optional_intent_arguments": optional_intent_arguments, 300 | "stop_app_on_reset": stop_app_on_reset 301 | } 302 | 303 | self._selenium_web_driver().start_activity(app_package, app_activity, **options) 304 | 305 | @SupportedBy(WebDriverPlatform.ANDROID) 306 | def get_current_activity(self) -> str: 307 | """ 308 | Retrieves the current activity on the device. 309 | """ 310 | return self._selenium_web_driver().current_activity 311 | 312 | @SupportedBy(WebDriverPlatform.ANDROID) 313 | def get_current_package(self) -> str: 314 | """ 315 | Retrieves the current package running on the device. 316 | """ 317 | return self._selenium_web_driver().current_package 318 | 319 | # App 320 | 321 | @SupportedBy(WebDriverPlatform._MOBILE) 322 | def install_app(self, app_path: str, replace: bool = True, timeout: int = 60000, allow_test_packages: bool = False, usd_sd_card: bool = False, grant_permissions: bool = False): 323 | """ 324 | Install the application found at `app_path` on the device. 325 | 326 | :param app_path - the local or remote path to the application to install 327 | 328 | The following options are available for Android: 329 | :param replace: whether to reinstall/upgrade the package if it is already present on the device under test. True by default 330 | :param timeout: how much time to wait for the installation to complete. 60000ms by default. 331 | :param allow_test_packages: whether to allow installation of packages marked as test in the manifest. False by default 332 | :param usd_sd_card: whether to use the SD card to install the app. False by default 333 | :param grant_permissions: whether to automatically grant application permissions on Android 6+ after the installation completes. False by default 334 | """ 335 | options = { 336 | "replace": replace, 337 | "timeout": timeout, 338 | "allowTestPackages": allow_test_packages, 339 | "useSdcard": usd_sd_card, 340 | "grantPermissions": grant_permissions 341 | } 342 | self._selenium_web_driver().install_app(app_path, **options) 343 | 344 | @SupportedBy(WebDriverPlatform._MOBILE) 345 | def is_app_installed(self, bundle_id: str) -> bool: 346 | """ 347 | Checks whether the application specified by `bundle_id` is installed on the device. 348 | 349 | :param bundle_id: the id of the application to query 350 | """ 351 | return self._selenium_web_driver().is_app_installed(bundle_id) 352 | 353 | @SupportedBy(WebDriverPlatform._MOBILE) 354 | def launch_app(self): 355 | """ 356 | Start on the device the application specified in the desired capabilities. 357 | """ 358 | self._selenium_web_driver().launch_app() 359 | 360 | @SupportedBy(WebDriverPlatform._MOBILE) 361 | def background_app(self, duration: int): 362 | """ 363 | Puts the application in the background on the device for a certain duration. 364 | 365 | :param duration: the duration for the application to remain in the background, in ms. 366 | """ 367 | self._selenium_web_driver().background_app(int(duration / 1000.0)) 368 | 369 | @SupportedBy(WebDriverPlatform._MOBILE) 370 | def activate_app(self, app_id: str): 371 | """ 372 | Activates the application if it is not running or is running in the background. 373 | 374 | :param app_id: the application id to be activated 375 | """ 376 | self._selenium_web_driver().activate_app(app_id) 377 | 378 | @SupportedBy(WebDriverPlatform._MOBILE) 379 | def close_app(self): 380 | """ 381 | Stop the running application, specified in the desired capabilities, on the device. 382 | """ 383 | self._selenium_web_driver().close_app() 384 | 385 | @SupportedBy(WebDriverPlatform._MOBILE) 386 | def terminate_app(self, app_id: str, timeout: int = 500) -> bool: 387 | """ 388 | Terminates the application if it is running. 389 | 390 | :param app_id: the application id to be terminates 391 | 392 | The following options are available for Android: 393 | :param timeout: how much time to wait for the uninstall to complete. 500ms by default. 394 | 395 | :return: True if the app has been successfully terminated 396 | """ 397 | options = { 398 | "timeout": timeout 399 | } 400 | return self._selenium_web_driver().terminate_app(app_id, **options) 401 | 402 | @SupportedBy(WebDriverPlatform._MOBILE) 403 | def reset_app(self): 404 | """ 405 | Resets the current application on the device. 406 | """ 407 | self._selenium_web_driver().reset() 408 | 409 | @SupportedBy(WebDriverPlatform._MOBILE) 410 | def remove_app(self, app_id: str, keep_data: bool = False, timeout: int = 20000): 411 | """ 412 | Remove the specified application from the device. 413 | 414 | :param app_id: the application id to be removed 415 | 416 | The following options are available for Android: 417 | :param keep_data: whether to keep application data and caches after it is uninstalled. False by default 418 | :param timeout: how much time to wait for the uninstall to complete. 20000ms by default. 419 | """ 420 | options = { 421 | "keepData": keep_data, 422 | "timeout": timeout 423 | } 424 | self._selenium_web_driver().remove_app(app_id, **options) 425 | 426 | @SupportedBy(WebDriverPlatform._MOBILE) 427 | def get_app_state(self, app_id: str) -> int: 428 | """ 429 | Queries the state of the application. 430 | 431 | :param app_id: the application id to be queried 432 | 433 | :return: One of possible application state constants. See appium.webdriver.applicationstate.ApplicationState class for more details. 434 | """ 435 | return self._selenium_web_driver().query_app_state(app_id) 436 | 437 | @SupportedBy(WebDriverPlatform._MOBILE) 438 | def get_app_strings(self, language: str = None, string_file: str = None) -> dict: 439 | """ 440 | Returns the application strings from the device for the specified language. 441 | 442 | :param language: strings language code 443 | :param string_file: the name of the string file to query 444 | """ 445 | return self._selenium_web_driver().app_strings(language, string_file) 446 | 447 | @SupportedBy(WebDriverPlatform.ANDROID) 448 | def end_test_coverage(self, intent: str, path: str): 449 | """ 450 | Ends the coverage collection and pull the coverage.ec file from the device. 451 | Android only. 452 | 453 | See https://github.com/appium/appium/blob/master/docs/en/android_coverage.md 454 | 455 | :param intent: description of operation to be performed 456 | :param path: path to coverage.ec file to be pulled from the device 457 | """ 458 | self._selenium_web_driver().end_test_coverage(intent, path) 459 | 460 | # Files 461 | 462 | @SupportedBy(WebDriverPlatform._MOBILE) 463 | def push_file(self, destination_path: str, base64data: str = None, source_path: str = None): 464 | """ 465 | Puts the data from the file at `source_path`, encoded as Base64, in the file specified as `path`. 466 | Specify either `base64data` or `source_path`, if both specified default to `source_path` 467 | 468 | :param destination_path: the location on the device/simulator where the local file contents should be saved 469 | :param base64data: file contents, encoded as Base64, to be written to the file on the device/simulator 470 | :param source_path: local file path for the file to be loaded on device 471 | """ 472 | self._selenium_web_driver().push_file(destination_path, base64data, source_path) 473 | 474 | @SupportedBy(WebDriverPlatform._MOBILE) 475 | def pull_file(self, path: str) -> str: 476 | """ 477 | Retrieves the file at `path`. Returns the file's content encoded as Base64. 478 | 479 | :param path: the path to the file on the device 480 | """ 481 | return self._selenium_web_driver().pull_file(path) 482 | 483 | @SupportedBy(WebDriverPlatform._MOBILE) 484 | def pull_folder(self, path: str) -> str: 485 | """ 486 | Retrieves a folder at `path`. Returns the folder's contents zipped and encoded as Base64. 487 | 488 | :param path: the path to the folder on the device 489 | """ 490 | return self._selenium_web_driver().pull_folder(path) 491 | 492 | # Interactions todo: rotate 493 | 494 | @SupportedBy(WebDriverPlatform._MOBILE) 495 | def shake(self): 496 | """ 497 | Shake the device. 498 | """ 499 | self._selenium_web_driver().shake() 500 | 501 | @SupportedBy(WebDriverPlatform._MOBILE) 502 | def lock(self, duration: int = None): 503 | """ 504 | Lock the device for a certain period of time. 505 | No changes are made if the device is already locked. 506 | 507 | :param duration: (optional) the duration to lock the device, in ms. 508 | The device is going to be locked forever until `unlock` is called if it equals or is less than zero, 509 | otherwise this call blocks until the timeout expires and unlocks the screen automatically. 510 | """ 511 | self._selenium_web_driver().lock(int(duration / 1000.0)) 512 | 513 | @SupportedBy(WebDriverPlatform.ANDROID) 514 | def unlock(self): 515 | """ 516 | Unlock the device. No changes are made if the device is already unlocked. 517 | """ 518 | self._selenium_web_driver().unlock() 519 | 520 | @SupportedBy(WebDriverPlatform.ANDROID) 521 | def is_locked(self) -> bool: 522 | """ 523 | Checks whether the device is locked. 524 | 525 | :return: `True` if the device is locked 526 | """ 527 | return self._selenium_web_driver().is_locked() 528 | 529 | @SupportedBy(WebDriverPlatform.IOS) 530 | def press_button(self, button_name: str): 531 | """ 532 | Sends a physical button name to the device to simulate the user pressing. iOS only. 533 | Possible button names can be found in 534 | https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Categories/XCUIDevice%2BFBHelpers.h 535 | 536 | :param button_name: the button name to be sent to the device. volumeUp (real devices only), volumeDown (real device only), home 537 | """ 538 | self._selenium_web_driver().press_button(button_name) 539 | 540 | # Keys 541 | 542 | @SupportedBy(WebDriverPlatform.ANDROID) 543 | def press_keycode(self, keycode: int, metastate: int = None, flags: int = None): 544 | """ 545 | Sends a keycode to the device. Android only. Possible keycodes can be 546 | found in http://developer.android.com/reference/android/view/KeyEvent.html. 547 | 548 | :param keycode: the keycode to be sent to the device 549 | :param metastate: meta information about the keycode being sent 550 | :param flags: the set of key event flags 551 | """ 552 | self._selenium_web_driver().press_keycode(keycode, metastate, flags) 553 | 554 | @SupportedBy(WebDriverPlatform.ANDROID) 555 | def long_press_keycode(self, keycode: int, metastate: int = None, flags: int = None): 556 | """ 557 | Sends a long press of keycode to the device. Android only. Possible keycodes can be 558 | found in http://developer.android.com/reference/android/view/KeyEvent.html. 559 | 560 | :param keycode: the keycode to be sent to the device 561 | :param metastate: meta information about the keycode being sent 562 | :param flags: the set of key event flags 563 | """ 564 | self._selenium_web_driver().long_press_keycode(keycode, metastate, flags) 565 | 566 | @SupportedBy(WebDriverPlatform._MOBILE) 567 | def hide_keyboard(self, key_name: str = None, key: str = None, strategy: str = None): 568 | """ 569 | Hides the software keyboard on the device. In iOS, use `key_name` to press 570 | a particular key, or `strategy`. In Android, no parameters are used. 571 | 572 | :param key_name: key to press 573 | :param key: key to press 574 | :param strategy: strategy for closing the keyboard (e.g., `tapOutside`) 575 | """ 576 | self._selenium_web_driver().hide_keyboard(key_name, key, strategy) 577 | 578 | @SupportedBy(WebDriverPlatform._MOBILE) 579 | def is_keyboard_shown(self) -> bool: 580 | """ 581 | Attempts to detect whether a software keyboard is present. 582 | 583 | :return: Either True or False 584 | """ 585 | return self._selenium_web_driver().is_keyboard_shown() 586 | 587 | @SupportedBy(WebDriverPlatform.ANDROID) 588 | def key_event(self, keycode: int, metastate: int = None): 589 | """ 590 | Sends a keycode to the device. Android only. Possible keycodes can be 591 | found in http://developer.android.com/reference/android/view/KeyEvent.html. 592 | 593 | :param keycode: the keycode to be sent to the device 594 | :param metastate: meta information about the keycode being sent 595 | """ 596 | self._selenium_web_driver().keyevent(keycode, metastate) 597 | 598 | # Network 599 | 600 | @SupportedBy(WebDriverPlatform.ANDROID) 601 | def toggle_location_services(self): 602 | """ 603 | Toggle the location services on the device. Android only. 604 | """ 605 | self._selenium_web_driver().toggle_location_services() 606 | 607 | @SupportedBy(WebDriverPlatform.ANDROID) 608 | def get_network_connection(self) -> int: 609 | """ 610 | Returns an integer bitmask specifying the network connection type. 611 | Android only. 612 | Possible values are available through the enumeration `appium.webdriver.ConnectionType` 613 | """ 614 | return self._selenium_web_driver().network_connection 615 | 616 | @SupportedBy(WebDriverPlatform.ANDROID) 617 | def set_network_connection(self, connection_type: int): 618 | """ 619 | Sets the network connection type. Android only. 620 | Possible values:: 621 | Value (Alias) | Data | Wifi | Airplane Mode 622 | ------------------------------------------------- 623 | 0 (None) | 0 | 0 | 0 624 | 1 (Airplane Mode) | 0 | 0 | 1 625 | 2 (Wifi only) | 0 | 1 | 0 626 | 4 (Data only) | 1 | 0 | 0 627 | 6 (All network on) | 1 | 1 | 0 628 | These are available through the enumeration `appium.webdriver.ConnectionType` 629 | 630 | :param connection_type: a member of the enum appium.webdriver.ConnectionType 631 | """ 632 | self._selenium_web_driver().set_network_connection(connection_type) 633 | 634 | @SupportedBy(WebDriverPlatform.ANDROID) 635 | def set_network_speed(self, speed_type: str): 636 | """ 637 | Set the network speed emulation. Android Emulator only. 638 | 639 | :param speed_type: The network speed type. A member of the const appium.webdriver.extensions.android.network.NetSpeed. 640 | """ 641 | self._selenium_web_driver().set_network_speed(speed_type) 642 | 643 | # Performance Data 644 | 645 | @SupportedBy(WebDriverPlatform.ANDROID) 646 | def get_performance_data(self, package_name: str, data_type: str, data_read_timeout: int = None): 647 | """ 648 | Returns the information of the system state which is supported to read as like cpu, memory, network traffic, and battery. 649 | 650 | :param package_name: The package name of the application 651 | :param data_type: The type of system state which wants to read. 652 | It should be one of the supported performance data types. 653 | Check `get_performance_data_types` for supported types 654 | :param data_read_timeout: The number of attempts to read 655 | :return: The data along to `data_type` 656 | 657 | :Usage: 658 | self.driver.get_performance_data('my.app.package', 'cpuinfo', 5) 659 | """ 660 | return self._selenium_web_driver().get_performance_data(package_name, data_type, data_read_timeout) 661 | 662 | @SupportedBy(WebDriverPlatform.ANDROID) 663 | def get_performance_data_types(self) -> List[str]: 664 | """ 665 | Returns the information types of the system state which is supported to read as like cpu, memory, network traffic, and battery. 666 | 667 | :return: Available data types 668 | """ 669 | return self._selenium_web_driver().get_performance_data_types() 670 | 671 | # Simulator 672 | 673 | @SupportedBy(WebDriverPlatform.IOS) 674 | def perform_touch_id(self, match: bool): 675 | """ 676 | Simulate touchId on iOS Simulator 677 | 678 | :param match: Simulates a successful touch (`True`) or a failed touch (`False`) 679 | """ 680 | self._selenium_web_driver().touch_id(match) 681 | 682 | @SupportedBy(WebDriverPlatform.IOS) 683 | def toggle_touch_id_enrollment(self): 684 | """ 685 | Toggle enroll touchId on iOS Simulator 686 | """ 687 | self._selenium_web_driver().toggle_touch_id_enrollment() 688 | 689 | # System 690 | 691 | @SupportedBy(WebDriverPlatform.ANDROID) 692 | def get_system_bars(self) -> dict: 693 | """ 694 | Retrieve visibility and bounds information of the status and navigation bars. 695 | 696 | :return: A dictionary whose keys are 697 | - statusBar 698 | - visible 699 | - x 700 | - y 701 | - width 702 | - height 703 | - navigationBar 704 | - visible 705 | - x 706 | - y 707 | - width 708 | - height 709 | """ 710 | return self._selenium_web_driver().get_system_bars() 711 | 712 | @SupportedBy(WebDriverPlatform.ANDROID) 713 | def get_display_density(self) -> int: 714 | """ 715 | Get the display density, Android only 716 | 717 | :return: The display density of the Android device(dpi) 718 | """ 719 | return self._selenium_web_driver().get_display_density() 720 | 721 | @SupportedBy(WebDriverPlatform.ANDROID) 722 | def open_notifications(self): 723 | """ 724 | Open notification shade in Android (API Level 18 and above) 725 | """ 726 | self._selenium_web_driver().open_notifications() 727 | 728 | @SupportedBy(WebDriverPlatform._MOBILE) 729 | def get_device_time(self, format: str = None) -> str: 730 | """ 731 | Returns the date and time from the device 732 | 733 | :param format: The set of format specifiers. Read https://momentjs.com/docs/ to get the full list of supported datetime format specifiers. 734 | If unset, return :func:`.device_time` as default format is `YYYY-MM-DDTHH:mm:ssZ`, which complies to ISO-8601 735 | 736 | :Usage: 737 | self.driver.get_device_time() 738 | self.driver.get_device_time("YYYY-MM-DD") 739 | """ 740 | return self._selenium_web_driver().get_device_time(format) 741 | 742 | @SupportedBy(WebDriverPlatform._MOBILE) 743 | def get_battery_info(self) -> dict: 744 | """ 745 | Retrieves battery information for the device under test. 746 | 747 | :return: A dictionary containing the following entries 748 | - level: Battery level in range [0.0, 1.0], where 1.0 means 100% charge. 749 | Any value lower than 0 means the level cannot be retrieved 750 | - state: Platform-dependent battery state value. 751 | On iOS (XCUITest): 752 | - 1: Unplugged 753 | - 2: Charging 754 | - 3: Full 755 | Any other value means the state cannot be retrieved 756 | On Android (UIAutomator2): 757 | - 2: Charging 758 | - 3: Discharging 759 | - 4: Not charging 760 | - 5: Full 761 | Any other value means the state cannot be retrieved 762 | """ 763 | return self._selenium_web_driver().battery_info 764 | 765 | # Power 766 | 767 | @SupportedBy(WebDriverPlatform.ANDROID) 768 | def set_power_capacity(self, percent: int): 769 | """ 770 | Emulate power capacity change on the connected emulator. 771 | 772 | :param percent: The power capacity to be set. Can be set from 0 to 100 773 | """ 774 | self._selenium_web_driver().set_power_capacity(percent) 775 | 776 | @SupportedBy(WebDriverPlatform.ANDROID) 777 | def set_power_ac(self, ac_state: str): 778 | """ 779 | Emulate power state change on the connected emulator. 780 | 781 | :param ac_state: The power ac state to be set. A member of the const appium.webdriver.extensions.android.power.Power 782 | """ 783 | self._selenium_web_driver().set_power_ac(ac_state) 784 | 785 | # GSM & SMS 786 | 787 | @SupportedBy(WebDriverPlatform.ANDROID) 788 | def make_gsm_call(self, phone_number: str, action: str): 789 | """ 790 | Make GSM call (Emulator only) 791 | 792 | :param phone_number: The phone number to call to. 793 | :param action: The call action. A member of the const appium.webdriver.extensions.android.gsm.GsmCallActions 794 | 795 | :Usage: 796 | self.driver.make_gsm_call('5551234567', GsmCallActions.CALL) 797 | """ 798 | self._selenium_web_driver().make_gsm_call(phone_number, action) 799 | 800 | @SupportedBy(WebDriverPlatform.ANDROID) 801 | def set_gsm_signal(self, strength: int): 802 | """ 803 | Set GSM signal strength (Emulator only) 804 | 805 | :param strength: Signal strength. A member of the enum appium.webdriver.extensions.android.gsm.GsmSignalStrength 806 | 807 | :Usage: 808 | self.driver.set_gsm_signal(GsmSignalStrength.GOOD) 809 | """ 810 | self._selenium_web_driver().set_gsm_signal(strength) 811 | 812 | @SupportedBy(WebDriverPlatform.ANDROID) 813 | def set_gsm_voice(self, state: str): 814 | """ 815 | Set GSM voice state (Emulator only) 816 | 817 | :param state: State of GSM voice. A member of the const appium.webdriver.extensions.android.gsm.GsmVoiceState 818 | 819 | :Usage: 820 | self.driver.set_gsm_voice(GsmVoiceState.HOME) 821 | """ 822 | self._selenium_web_driver().set_gsm_voice(state) 823 | 824 | @SupportedBy(WebDriverPlatform.ANDROID) 825 | def send_sms(self, phone_number: str, message: str): 826 | """ 827 | Emulate send SMS event on the connected emulator. 828 | 829 | :param phone_number: The phone number of message sender 830 | :param message: The message to send 831 | 832 | :Usage: 833 | self.driver.send_sms('555-123-4567', 'Hey lol') 834 | """ 835 | self._selenium_web_driver().send_sms(phone_number, message) 836 | 837 | # Authentication 838 | 839 | @SupportedBy(WebDriverPlatform.ANDROID) 840 | def perform_finger_print(self, finger_id: int): 841 | """ 842 | Authenticate users by using their finger print scans on supported emulators. Android only. 843 | 844 | :param finger_id: Finger prints stored in Android Keystore system (from 1 to 10) 845 | """ 846 | return self._selenium_web_driver().finger_print(finger_id) 847 | 848 | # Context 849 | 850 | @SupportedBy(WebDriverPlatform._MOBILE) 851 | def get_contexts(self) -> List[str]: 852 | """ 853 | Returns the contexts within the current session. 854 | """ 855 | return self._selenium_web_driver().contexts 856 | 857 | @SupportedBy(WebDriverPlatform._MOBILE) 858 | def get_current_context(self) -> str: 859 | """ 860 | Returns the current context of the current session. 861 | """ 862 | return self._selenium_web_driver().current_context 863 | 864 | @SupportedBy(WebDriverPlatform._MOBILE) 865 | def switch_to_context(self, context_partial_name: str): 866 | """ 867 | Sets the context for the current session. 868 | 869 | :param context_partial_name: The partial name of the context to switch to 870 | 871 | :Usage: 872 | driver.switch_to_context('WEBVIEW_1') 873 | """ 874 | if context_partial_name == "NATIVE_APP": 875 | self._selenium_web_driver().switch_to.context(context_partial_name) 876 | self.__web_driver_info.context = WebDriverContext.NATIVE_APP 877 | else: 878 | contexts = {"inner": []} 879 | 880 | def get_contexts(partial_name): 881 | try: 882 | contexts["inner"] = [context for context in self.get_contexts() if partial_name in context] 883 | return contexts["inner"] 884 | except WebDriverException as e: 885 | return [] 886 | 887 | def context_available(partial_name): 888 | return len(get_contexts(partial_name)) > 0 889 | 890 | self.waiter().wait_for(context_available, partial_name=context_partial_name) 891 | self._selenium_web_driver().switch_to.context(contexts["inner"][0]) 892 | self.__web_driver_info.context = WebDriverContext.WEB_VIEW 893 | 894 | # Window 895 | 896 | def get_current_window_handle(self) -> str: 897 | """ 898 | Returns the handle of the current window. 899 | """ 900 | return self._selenium_web_driver().current_window_handle 901 | 902 | def get_window_handles(self) -> List[str]: 903 | """ 904 | Returns the handles of all windows within the current session. 905 | """ 906 | return self._selenium_web_driver().window_handles 907 | 908 | def switch_to_window(self, window_handle: str): 909 | """ 910 | Switches focus to the specified window. 911 | 912 | :param window_handle: The name or window handle of the window to switch to. 913 | 914 | :Usage: 915 | driver.switch_to_window('main') 916 | """ 917 | self._selenium_web_driver().switch_to.window(window_handle) 918 | 919 | def switch_to_new_window(self, previous_window_handles: List[str]): 920 | """ 921 | Switch to the new opened window. 922 | 923 | :param previous_window_handles: the window handles before opening new window 924 | 925 | :Usage: 926 | previous_window_handles = driver.get_window_handles() 927 | StaticElement(driver, "id=open-new-window").click() # open the new window 928 | driver.switch_to_new_window(previous_window_handles) 929 | """ 930 | new_window_handles = {"inner": []} 931 | 932 | def get_new_window_handles(): 933 | new_window_handles["inner"] = [handle for handle in self.get_window_handles() if 934 | handle not in previous_window_handles] 935 | return new_window_handles["inner"] 936 | 937 | def new_window_opened(): 938 | return len(get_new_window_handles()) > 0 939 | 940 | self.waiter().wait_for(new_window_opened) 941 | self.switch_to_window(new_window_handles["inner"][0]) 942 | 943 | def maximize_window(self): 944 | """ 945 | Maximizes the current window that webdriver is using 946 | """ 947 | self._selenium_web_driver().maximize_window() 948 | 949 | def fullscreen_window(self): 950 | """ 951 | Invokes the window manager-specific 'full screen' operation 952 | """ 953 | self._selenium_web_driver().fullscreen_window() 954 | 955 | def set_window_size(self, width: int, height: int, window_handle: str = "current"): 956 | """ 957 | Sets the width and height of the specified window. 958 | 959 | :param width: the width in pixels to set the window to 960 | :param height: the height in pixels to set the window to 961 | :param window_handle: The name or window handle of the window to set, default is current window. 962 | 963 | :Usage: 964 | driver.set_window_size(800,600) 965 | """ 966 | self._selenium_web_driver().set_window_size(width, height, window_handle) 967 | 968 | def get_window_size(self, window_handle: str = "current"): 969 | """ 970 | Gets the width and height of the specified window. 971 | 972 | :param window_handle: The name or window handle of the window to get, default is current window. 973 | """ 974 | return self._selenium_web_driver().get_window_size(window_handle) 975 | 976 | def set_window_position(self, x: int, y: int, window_handle: str = "current"): 977 | """ 978 | Sets the x, y position of the specified window. 979 | 980 | :param x: the x-coordinate in pixels to set the window position 981 | :param y: the y-coordinate in pixels to set the window position 982 | :param window_handle: The name or window handle of the window to set, default is current window. 983 | 984 | :Usage: 985 | driver.set_window_position(0,0) 986 | """ 987 | self._selenium_web_driver().set_window_position(x, y, window_handle) 988 | 989 | def get_window_position(self, window_handle: str = "current") -> dict: 990 | """ 991 | Gets the x, y position of the specified window. 992 | 993 | :param window_handle: The name or window handle of the window to get, default is current window. 994 | """ 995 | return self._selenium_web_driver().get_window_position(window_handle) 996 | 997 | def get_window_rect(self) -> dict: 998 | """ 999 | Gets the x, y coordinates of the window as well as height and width of the current window. 1000 | """ 1001 | return self._selenium_web_driver().get_window_rect() 1002 | 1003 | def set_window_rect(self, x: int = None, y: int = None, width: int = None, height: int = None): 1004 | """ 1005 | Sets the x, y coordinates of the window as well as height and width of the current window. 1006 | """ 1007 | self._selenium_web_driver().set_window_rect(x, y, width, height) 1008 | 1009 | def get_viewport_size(self) -> dict: 1010 | """ 1011 | Gets the width and height of viewport. 1012 | """ 1013 | return self._selenium_web_driver().execute_script("return {width: window.innerWidth, height: window.innerHeight};") 1014 | 1015 | def set_viewport_size(self, width: int, height: int): 1016 | """ 1017 | Sets the width and height of viewport. When changes the viewport size, the window size will be also changed. 1018 | 1019 | :param width: the width in pixels to set viewport to 1020 | :param height: the height in pixels to set viewport to 1021 | """ 1022 | window_size = self._selenium_web_driver().execute_script(""" 1023 | return [window.outerWidth - window.innerWidth + arguments[0], 1024 | window.outerHeight - window.innerHeight + arguments[1]]; 1025 | """, width, height) 1026 | self._selenium_web_driver().set_window_size(*window_size) 1027 | 1028 | def get_title(self) -> str: 1029 | """ 1030 | Returns the title of the current page. 1031 | """ 1032 | return self._selenium_web_driver().title 1033 | 1034 | def get_current_url(self) -> str: 1035 | """ 1036 | Gets the URL of the current page. 1037 | """ 1038 | return self._selenium_web_driver().current_url 1039 | 1040 | def get_page_source(self) -> str: 1041 | """ 1042 | Gets the source of the current page. 1043 | """ 1044 | return self._selenium_web_driver().page_source 1045 | 1046 | def close_window(self, window_handle: str = "current"): 1047 | """ 1048 | Close the specified window. 1049 | 1050 | :param window_handle: The name or window handle of the window to close, default is current window. 1051 | """ 1052 | if window_handle == "current" or window_handle == self.get_current_window_handle(): 1053 | self._selenium_web_driver().close() 1054 | else: 1055 | current_window_handle = self.get_current_window_handle() 1056 | self.switch_to_window(window_handle) 1057 | self._selenium_web_driver().close() 1058 | self.switch_to_window(current_window_handle) 1059 | 1060 | # Navigation 1061 | 1062 | def get(self, url: str): 1063 | """ 1064 | Loads a web page in the current browser session. 1065 | 1066 | :param url: the url to be open 1067 | """ 1068 | self._selenium_web_driver().get(url) 1069 | 1070 | def refresh(self): 1071 | """ 1072 | Refreshes the current page. 1073 | """ 1074 | self._selenium_web_driver().refresh() 1075 | 1076 | def back(self): 1077 | """ 1078 | Goes one step backward in the browser history. 1079 | """ 1080 | self._selenium_web_driver().back() 1081 | 1082 | def forward(self): 1083 | """ 1084 | Goes one step forward in the browser history. 1085 | """ 1086 | self._selenium_web_driver().forward() 1087 | 1088 | # Storage 1089 | 1090 | def get_cookie(self, name: str) -> dict: 1091 | """ 1092 | Get a single cookie by name. Returns the cookie if found, None if not. 1093 | 1094 | :param name: the cookie name 1095 | """ 1096 | return self._selenium_web_driver().get_cookie(name) 1097 | 1098 | def get_cookies(self) -> List[dict]: 1099 | """ 1100 | Returns a set of dictionaries, corresponding to cookies visible in the current session. 1101 | """ 1102 | return self._selenium_web_driver().get_cookies() 1103 | 1104 | def add_cookie(self, cookie_dict: dict): 1105 | """ 1106 | Adds a cookie to your current session. 1107 | 1108 | :param cookie_dict: A dictionary object, with required keys - "name" and "value"; 1109 | optional keys - "path", "domain", "secure", "expiry" 1110 | 1111 | Usage: 1112 | driver.add_cookie({'name' : 'foo', 'value' : 'bar'}) 1113 | driver.add_cookie({'name' : 'foo', 'value' : 'bar', 'path' : '/'}) 1114 | driver.add_cookie({'name' : 'foo', 'value' : 'bar', 'path' : '/', 'secure':True}) 1115 | """ 1116 | self._selenium_web_driver().add_cookie(cookie_dict) 1117 | 1118 | def delete_cookie(self, name: str): 1119 | """ 1120 | Deletes a single cookie with the given name. 1121 | 1122 | :param name: the cookie name 1123 | """ 1124 | self._selenium_web_driver().delete_cookie(name) 1125 | 1126 | def delete_all_cookies(self): 1127 | """ 1128 | Delete all cookies in the scope of the session. 1129 | """ 1130 | self._selenium_web_driver().delete_all_cookies() 1131 | 1132 | # Frame 1133 | 1134 | def switch_to_frame(self, frame_reference: Union[int, str, "Element"]): 1135 | """ 1136 | Switches focus to the specified frame, by index (zero-based), locator, or element. 1137 | 1138 | :param frame_reference: an integer representing the index, the locator of the frame to switch to, 1139 | or a element that is an (i)frame to switch to. 1140 | 1141 | :Usage: 1142 | driver.switch_to_frame(1) 1143 | driver.switch_to_frame("name=myIframe") 1144 | driver.switch_to_frame(StaticElement(driver, "tag=iframe")) 1145 | """ 1146 | from .element import Element 1147 | from .static_element import StaticElement 1148 | 1149 | if isinstance(frame_reference, int): 1150 | frame_element = StaticElement(self, "xpath=(.//iframe)[%s]" % (frame_reference + 1)) 1151 | elif isinstance(frame_reference, str): 1152 | frame_element = StaticElement(self, frame_reference) 1153 | elif isinstance(frame_reference, Element): 1154 | frame_element = frame_reference 1155 | else: 1156 | raise ValueError("Frame reference type %s is not supported." % type(frame_reference)) 1157 | frame_element.wait_for().exists() 1158 | self._selenium_web_driver().switch_to.frame(frame_element._selenium_element()) 1159 | 1160 | def switch_to_parent_frame(self): 1161 | """ 1162 | Switches focus to the parent context. If the current context is the top 1163 | level browsing context, the context remains unchanged. 1164 | """ 1165 | self._selenium_web_driver().switch_to.parent_frame() 1166 | 1167 | def switch_to_default_content(self): 1168 | """ 1169 | Selects either the first frame on the page, or the main document when a page contains iframes. 1170 | """ 1171 | self._selenium_web_driver().switch_to.default_content() 1172 | 1173 | # IME engine 1174 | 1175 | @SupportedBy(WebDriverPlatform.ANDROID) 1176 | def get_available_ime_engines(self) -> List[str]: 1177 | """ 1178 | Get the available input methods for an Android device. Package and 1179 | activity are returned (e.g., ['com.android.inputmethod.latin/.LatinIME']) 1180 | Android only. 1181 | """ 1182 | return self._selenium_web_driver().available_ime_engines 1183 | 1184 | @SupportedBy(WebDriverPlatform.ANDROID) 1185 | def is_ime_service_active(self) -> bool: 1186 | """ 1187 | Checks whether the device has IME service active. Returns True/False. 1188 | Android only. 1189 | """ 1190 | return self._selenium_web_driver().is_ime_active() 1191 | 1192 | @SupportedBy(WebDriverPlatform.ANDROID) 1193 | def active_ime_engine(self, engine: str): 1194 | """ 1195 | Activates the given IME engine on the device. 1196 | Android only. 1197 | 1198 | :param engine: the package and activity of the IME engine to activate (e.g., 'com.android.inputmethod.latin/.LatinIME') 1199 | """ 1200 | self._selenium_web_driver().activate_ime_engine(engine) 1201 | 1202 | @SupportedBy(WebDriverPlatform.ANDROID) 1203 | def deactivate_current_ime_engine(self): 1204 | """ 1205 | Deactivates the currently active IME engine on the device. 1206 | Android only. 1207 | """ 1208 | self._selenium_web_driver().deactivate_ime_engine() 1209 | 1210 | @SupportedBy(WebDriverPlatform.ANDROID) 1211 | def get_current_ime_engine(self) -> str: 1212 | """ 1213 | Returns the activity and package of the currently active IME engine (e.g., 'com.android.inputmethod.latin/.LatinIME'). 1214 | Android only. 1215 | """ 1216 | return self._selenium_web_driver().active_ime_engine 1217 | 1218 | # Alert 1219 | 1220 | def switch_to_alert(self) -> Alert: 1221 | """ 1222 | Switches focus to an alert on the page. 1223 | 1224 | :return: the Alert instance 1225 | """ 1226 | self.wait_for().alert_present() 1227 | return Alert(self._selenium_web_driver().switch_to.alert) 1228 | 1229 | def get_alert(self) -> Alert: 1230 | """ 1231 | Switches focus to an alert on the page. 1232 | 1233 | :return: the Alert instance 1234 | """ 1235 | return self.switch_to_alert() 1236 | 1237 | def is_alert_present(self) -> bool: 1238 | """ 1239 | Return whether the alert is present on the page or not. 1240 | """ 1241 | try: 1242 | alert_text = self._selenium_web_driver().switch_to.alert.text 1243 | return True 1244 | except WebDriverException as e: 1245 | return False 1246 | 1247 | # Screenshot and recording 1248 | 1249 | def get_screenshot_as_file(self, filename: str) -> bool: 1250 | """ 1251 | Gets the screenshot of the current window. Returns False if there is 1252 | any IOError, else returns True. Use full paths in your filename. 1253 | 1254 | :param filename: The full path you wish to save your screenshot to. 1255 | 1256 | :Usage: 1257 | driver.get_screenshot_as_file('/Screenshots/foo.png') 1258 | """ 1259 | return self._selenium_web_driver().get_screenshot_as_file(filename) 1260 | 1261 | def save_screenshot(self, filename: str) -> bool: 1262 | """ 1263 | Gets the screenshot of the current window. Returns False if there is any IOError, else returns True. Use full paths in your filename. 1264 | 1265 | :param filename: The full path you wish to save your screenshot to. 1266 | 1267 | :Usage: 1268 | driver.save_screenshot('/Screenshots/foo.png') 1269 | """ 1270 | return self.get_screenshot_as_file(filename) 1271 | 1272 | def get_screenshot_as_png(self) -> bytes: 1273 | """ 1274 | Gets the screenshot of the current window as a binary data. 1275 | 1276 | :Usage: 1277 | driver.get_screenshot_as_png() 1278 | """ 1279 | return self._selenium_web_driver().get_screenshot_as_png() 1280 | 1281 | def get_screenshot_as_base64(self) -> str: 1282 | """ 1283 | Gets the screenshot of the current window as a base64 encoded string 1284 | which is useful in embedded images in HTML. 1285 | 1286 | :Usage: 1287 | driver.get_screenshot_as_base64() 1288 | """ 1289 | return self._selenium_web_driver().get_screenshot_as_base64() 1290 | 1291 | @SupportedBy(WebDriverPlatform._MOBILE) 1292 | def start_recording_screen(self, remote_path: str = None, user: str = None, password: str = None, method: str = None, time_limit: int = None, forced_restart: bool = None, 1293 | file_field_name: str = None, form_fields: dict = None, headers: dict = None, video_quality: str = None, video_type: str = None, 1294 | video_fps: int = None, video_filters: str = None, video_scale: str = None, pixel_format: str = None, video_size: str = None, bit_rate: int = None, 1295 | bug_report: str = None, fps: int = None, capture_cursor: bool = None, capture_click: bool = None, device_id: int = None, 1296 | preset: str = None) -> bytes: 1297 | """ 1298 | Start asynchronous screen recording process. 1299 | 1300 | +--------------+-----+---------+-----+-------+ 1301 | | Keyword Args | iOS | Android | Win | macOS | 1302 | +==============+=====+=========+=====+=======+ 1303 | | remotePath | O | O | O | O | 1304 | +--------------+-----+---------+-----+-------+ 1305 | | user | O | O | O | O | 1306 | +--------------+-----+---------+-----+-------+ 1307 | | password | O | O | O | O | 1308 | +--------------+-----+---------+-----+-------+ 1309 | | method | O | O | O | O | 1310 | +--------------+-----+---------+-----+-------+ 1311 | | timeLimit | O | O | O | O | 1312 | +--------------+-----+---------+-----+-------+ 1313 | | forceRestart | O | O | O | O | 1314 | +--------------+-----+---------+-----+-------+ 1315 | | fileFieldName| O | O | O | O | 1316 | +--------------+-----+---------+-----+-------+ 1317 | | formFields | O | O | O | O | 1318 | +--------------+-----+---------+-----+-------+ 1319 | | headers | O | O | O | O | 1320 | +--------------+-----+---------+-----+-------+ 1321 | | videoQuality | O | | | | 1322 | +--------------+-----+---------+-----+-------+ 1323 | | videoType | O | | | | 1324 | +--------------+-----+---------+-----+-------+ 1325 | | videoFps | O | | | | 1326 | +--------------+-----+---------+-----+-------+ 1327 | | videoFilter | O | | O | O | 1328 | +--------------+-----+---------+-----+-------+ 1329 | | videoScale | O | | | | 1330 | +--------------+-----+---------+-----+-------+ 1331 | | pixelFormat | O | | | | 1332 | +--------------+-----+---------+-----+-------+ 1333 | | videoSize | | O | | | 1334 | +--------------+-----+---------+-----+-------+ 1335 | | bitRate | | O | | | 1336 | +--------------+-----+---------+-----+-------+ 1337 | | bugReport | | O | | | 1338 | +--------------+-----+---------+-----+-------+ 1339 | | fps | | | O | O | 1340 | +--------------+-----+---------+-----+-------+ 1341 | | captureCursor| | | O | O | 1342 | +--------------+-----+---------+-----+-------+ 1343 | | captureClicks| | | O | O | 1344 | +--------------+-----+---------+-----+-------+ 1345 | | deviceId | | | | O | 1346 | +--------------+-----+---------+-----+-------+ 1347 | | preset | | | O | O | 1348 | +--------------+-----+---------+-----+-------+ 1349 | | audioInput | | | O | | 1350 | +--------------+-----+---------+-----+-------+ 1351 | 1352 | :param remote_path: The remotePath upload option is the path to the remote location, 1353 | where the resulting video from the previous screen recording should be uploaded. 1354 | The following protocols are supported: http/https (multipart), ftp. 1355 | Missing value (the default setting) means the content of the resulting 1356 | file should be encoded as Base64 and passed as the endpoint response value, but 1357 | an exception will be thrown if the generated media file is too big to 1358 | fit into the available process memory. 1359 | This option only has an effect if there is/was an active screen recording session 1360 | and forced restart is not enabled (the default setting). 1361 | :param user: The name of the user for the remote authentication. 1362 | Only has an effect if both `remotePath` and `password` are set. 1363 | :param password: The password for the remote authentication. 1364 | Only has an effect if both `remotePath` and `user` are set. 1365 | :param method: The HTTP method name ('PUT'/'POST'). PUT method is used by default. 1366 | Only has an effect if `remotePath` is set. 1367 | :param time_limit: The actual time limit of the recorded video in seconds. 1368 | The default value for both iOS and Android is 180 seconds (3 minutes). 1369 | The default value for macOS is 600 seconds (10 minutes). 1370 | The maximum value for Android is 3 minutes. 1371 | The maximum value for iOS is 10 minutes. 1372 | The maximum value for macOS is 10000 seconds (166 minutes). 1373 | :param forced_restart: Whether to ignore the result of previous capture and start a new recording 1374 | immediately (`True` value). By default (`False`) the endpoint will try to catch and 1375 | return the result of the previous capture if it's still available. 1376 | :param file_field_name: [multipart/form-data requests] The name of the form field 1377 | containing the binary payload. "file" by default. (Since Appium 1.18.0) 1378 | :param form_fields: [multipart/form-data requests] Additional form fields mapping. If any entry has 1379 | the same key as `fileFieldName` then it is going to be ignored. (Since Appium 1.18.0) 1380 | :param headers: [multipart/form-data requests] Headers mapping (Since Appium 1.18.0) 1381 | 1382 | :param video_quality: [iOS] The video encoding quality: 'low', 'medium', 'high', 'photo'. Defaults to 'medium'. 1383 | :param video_type: [iOS] The format of the screen capture to be recorded. 1384 | Available formats: Execute `ffmpeg -codecs` in the terminal to see the list of supported video codecs. 1385 | 'mjpeg' by default. (Since Appium 1.10.0) 1386 | :param video_fps: [iOS] The Frames Per Second rate of the recorded video. Change this value if the 1387 | resulting video is too slow or too fast. Defaults to 10. This can decrease the resulting file size. 1388 | :param video_filters: [iOS, Win, macOS] The FFMPEG video filters to apply. These filters allow to scale, 1389 | flip, rotate and do many other useful transformations on the source video stream. The format of the 1390 | property must comply with https://ffmpeg.org/ffmpeg-filters.html. (Since Appium 1.15) 1391 | :param video_scale: [iOS] The scaling value to apply. Read https://trac.ffmpeg.org/wiki/Scaling for 1392 | possible values. No scale is applied by default. If videoFilters are set then the scale setting is 1393 | effectively ignored. (Since Appium 1.10.0) 1394 | :param pixel_format: [iOS] Output pixel format. Run `ffmpeg -pix_fmts` to list possible values. 1395 | For Quicktime compatibility, set to "yuv420p" along with videoType: "libx264". (Since Appium 1.12.0) 1396 | 1397 | :param video_size: [Android] The video size of the generated media file. The format is WIDTHxHEIGHT. 1398 | The default value is the device's native display resolution (if supported), 1399 | 1280x720 if not. For best results, use a size supported by your device's 1400 | Advanced Video Coding (AVC) encoder. 1401 | :param bit_rate: [Android] The video bit rate for the video, in megabits per second. 1402 | The default value is 4. You can increase the bit rate to improve video quality, 1403 | but doing so results in larger movie files. 1404 | :param bug_report: [Android] Makes the recorder to display an additional information on the video overlay, 1405 | such as a timestamp, that is helpful in videos captured to illustrate bugs. 1406 | This option is only supported since API level 27 (Android P). 1407 | 1408 | :param fps: [Win, macOS] The count of frames per second in the resulting video. 1409 | Increasing fps value also increases the size of the resulting video file and the CPU usage. 1410 | :param capture_cursor: [Win, macOS] Whether to capture the mouse cursor while recording the screen. 1411 | Disabled by default. 1412 | :param capture_click: [Win, macOS] Whether to capture the click gestures while recording the screen. 1413 | Disabled by default. 1414 | :param device_id: [macOS] Screen device index to use for the recording. 1415 | The list of available devices could be retrieved using 1416 | `ffmpeg -f avfoundation -list_devices true -i` command. 1417 | This option is mandatory and must be always provided. 1418 | :param preset: [Win, macOS] A preset is a collection of options that will provide a certain encoding 1419 | speed to compression ratio. A slower preset will provide better compression 1420 | (compression is quality per filesize). This means that, for example, if you target a certain file size 1421 | or constant bit rate, you will achieve better quality with a slower preset. 1422 | Read https://trac.ffmpeg.org/wiki/Encode/H.264 for more details. 1423 | Possible values are 'ultrafast', 'superfast', 'veryfast'(default), 'faster', 'fast', 'medium', 'slow', 1424 | 'slower', 'veryslow' 1425 | 1426 | :return: bytes: Base-64 encoded content of the recorded media if `stop_recording_screen` isn't called after previous `start_recording_screen`. 1427 | Otherwise returns an empty string. 1428 | """ 1429 | options = { 1430 | "remotePath": remote_path, 1431 | "user": user, 1432 | "password": password, 1433 | "method": method, 1434 | "timeLimit": time_limit, 1435 | "forcedRestart": forced_restart, 1436 | "fileFieldName": file_field_name, 1437 | "formFields": form_fields, 1438 | "headers": headers, 1439 | "videoQuality": video_quality, 1440 | "videoType": video_type, 1441 | "videoFps": video_fps, 1442 | "videoFilters": video_filters, 1443 | "videoScale": video_scale, 1444 | "pixelFormat": pixel_format, 1445 | "videoSize": video_size, 1446 | "bitRate": bit_rate, 1447 | "bugReport": bug_report, 1448 | "fps": fps, 1449 | "captureCursor": capture_cursor, 1450 | "captureClick": capture_click, 1451 | "deviceId": device_id, 1452 | "preset": preset 1453 | } 1454 | return self._selenium_web_driver().start_recording_screen(**options) 1455 | 1456 | @SupportedBy(WebDriverPlatform._MOBILE) 1457 | def stop_recording_screen(self, remote_path: str = None, user: str = None, password: str = None, method: str = None, file_field_name: str = None, 1458 | form_fields: dict = None, headers: dict = None) -> bytes: 1459 | """ 1460 | Gather the output from the previously started screen recording to a media file. 1461 | 1462 | :param remote_path: The remote_path upload option is the path to the remote location, 1463 | where the resulting video should be uploaded. 1464 | The following protocols are supported: http/https (multipart), ftp. 1465 | Missing value (the default setting) means the content of the resulting 1466 | file should be encoded as Base64 and passed as the endpoint response value, but 1467 | an exception will be thrown if the generated media file is too big to 1468 | fit into the available process memory. 1469 | :param user: The name of the user for the remote authentication. 1470 | Only has an effect if both `remote_path` and `password` are set. 1471 | :param password: The password for the remote authentication. 1472 | Only has an effect if both `remote_path` and `user` are set. 1473 | :param method: The HTTP method name ('PUT'/'POST'). PUT method is used by default. 1474 | Only has an effect if `remote_path` is set. 1475 | :param file_field_name: [multipart/form-data requests] The name of the form field 1476 | containing the binary payload. "file" by default. (Since Appium 1.18.0) 1477 | :param form_fields: [multipart/form-data requests] Additional form fields mapping. If any entry has 1478 | the same key as `fileFieldName` then it is going to be ignored. (Since Appium 1.18.0) 1479 | :param headers: [multipart/form-data requests] Headers mapping (Since Appium 1.18.0) 1480 | 1481 | :return: Base-64 encoded content of the recorded media file or an empty string if the file has been successfully uploaded to a remote location 1482 | (depends on the actual `remote_path` value). 1483 | """ 1484 | options = { 1485 | "remotePath": remote_path, 1486 | "user": user, 1487 | "password": password, 1488 | "method": method, 1489 | "fileFieldName": file_field_name, 1490 | "formFields": form_fields, 1491 | "headers": headers 1492 | } 1493 | return self._selenium_web_driver().stop_recording_screen(**options) 1494 | 1495 | # clipboard 1496 | 1497 | @SupportedBy(WebDriverPlatform._MOBILE) 1498 | def set_clipboard(self, content: bytes, content_type: str = ClipboardContentType.PLAINTEXT, label: str = None): 1499 | """ 1500 | Set the content of the system clipboard. 1501 | 1502 | :param content: The content to be set as bytearray string 1503 | :param content_type: One of enum appium.webdriver.clipboard_content_type.ClipboardContentType items. 1504 | Only ClipboardContentType.PLAINTEXT is supported on Android 1505 | :param label: Optional label argument, which only works for Android 1506 | """ 1507 | self._selenium_web_driver().set_clipboard(content, content_type, label) 1508 | 1509 | @SupportedBy(WebDriverPlatform._MOBILE) 1510 | def set_clipboard_text(self, text: str, label: str = None): 1511 | """ 1512 | Copies the given text to the system clipboard. 1513 | 1514 | :param text: The text to be set 1515 | :param label: Optional label argument, which only works for Android 1516 | """ 1517 | self._selenium_web_driver().set_clipboard_text(text, label) 1518 | 1519 | @SupportedBy(WebDriverPlatform._MOBILE) 1520 | def get_clipboard(self, content_type: str = ClipboardContentType.PLAINTEXT) -> bytes: 1521 | """ 1522 | Receives the content of the system clipboard. 1523 | 1524 | :param content_type: enum appium.webdriver.clipboard_content_type.ClipboardContentType items. 1525 | Only ClipboardContentType.PLAINTEXT is supported on Android 1526 | :return: Clipboard content as base64-encoded string or an empty string if the clipboard is empty 1527 | """ 1528 | return self._selenium_web_driver().get_clipboard(content_type) 1529 | 1530 | @SupportedBy(WebDriverPlatform._MOBILE) 1531 | def get_clipboard_text(self) -> str: 1532 | """ 1533 | Receives the text of the system clipboard. 1534 | 1535 | :return: The actual clipboard text or an empty string if the clipboard is empty 1536 | """ 1537 | return self._selenium_web_driver().get_clipboard_text() 1538 | 1539 | # Touch Actions 1540 | 1541 | @SupportedBy(WebDriverPlatform._MOBILE) 1542 | def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = None): 1543 | """ 1544 | Swipe from one point to another point, for an optional duration. 1545 | 1546 | :param start_x: x-coordinate at which to start 1547 | :param start_y: y-coordinate at which to start 1548 | :param end_x: x-coordinate at which to stop 1549 | :param end_y: y-coordinate at which to stop 1550 | :param duration: time to take the swipe, in ms. 1551 | 1552 | :Usage: 1553 | driver.swipe(100, 100, 100, 400) 1554 | """ 1555 | self._selenium_web_driver().swipe(start_x, start_y, end_x, end_y, duration) 1556 | 1557 | @SupportedBy(WebDriverPlatform._MOBILE) 1558 | def flick(self, start_x: int, start_y: int, end_x: int, end_y: int): 1559 | """ 1560 | Flick from one point to another point. 1561 | 1562 | :param start_x - x-coordinate at which to start 1563 | :param start_y - y-coordinate at which to start 1564 | :param end_x - x-coordinate at which to stop 1565 | :param end_y - y-coordinate at which to stop 1566 | 1567 | :Usage: 1568 | driver.flick(100, 100, 100, 400) 1569 | """ 1570 | self._selenium_web_driver().flick(start_x, start_y, end_x, end_y) 1571 | 1572 | @SupportedBy(WebDriverPlatform._MOBILE) 1573 | def scroll(self, direction: str): 1574 | """ 1575 | Scrolls the device to direction. 1576 | It will try to scroll in the first element of type scroll view, table or collection view it finds. 1577 | If you want to scroll in element, please use Element.scroll(direction) 1578 | 1579 | :param direction: the direction to scroll, the possible values are: up, down, left, right 1580 | """ 1581 | scroll_params = { 1582 | "direction": direction 1583 | } 1584 | self.execute_script("mobile: scroll", scroll_params) 1585 | 1586 | def scroll_to(self, element: "Element"): 1587 | """ 1588 | Scrolls to the given element. 1589 | 1590 | :param element: the element to be scrolled to 1591 | """ 1592 | element.scroll_into_view() 1593 | 1594 | def __str__(self): 1595 | return "WebDriver " % ( 1596 | self.get_web_driver_info().platform, self.get_web_driver_info().context, 1597 | self._selenium_web_driver().session_id) 1598 | 1599 | def __enter__(self): 1600 | return self 1601 | 1602 | def __exit__(self, exc_type, exc_val, exc_tb): 1603 | self.quit() 1604 | 1605 | 1606 | class Ie(WebDriver): 1607 | def __init__(self, service: IeService = None, options: IeOptions = None): 1608 | """ 1609 | Creates a new instance of Ie. 1610 | 1611 | :param service: IE Service instance, providing service 1612 | :param options: IE Options instance, providing additional options 1613 | """ 1614 | web_driver_info = WebDriverInfo(WebDriverPlatform.PC, WebDriverContext.IE) 1615 | selenium_web_driver = SeleniumIe(options=options, service=service) 1616 | WebDriver.__init__(self, selenium_web_driver=selenium_web_driver, web_driver_info=web_driver_info) 1617 | 1618 | 1619 | class Firefox(WebDriver): 1620 | def __init__(self, service: FirefoxService = None, options: FirefoxOptions = None): 1621 | """ 1622 | Creates a new instance of Firefox. 1623 | 1624 | :param service: Firefox Service instance, providing service 1625 | :param options: Firefox Options instance, providing additional options 1626 | """ 1627 | web_driver_info = WebDriverInfo(WebDriverPlatform.PC, WebDriverContext.FIREFOX) 1628 | selenium_web_driver = SeleniumFirefox(service=service, options=options) 1629 | WebDriver.__init__(self, selenium_web_driver=selenium_web_driver, web_driver_info=web_driver_info) 1630 | 1631 | 1632 | class Chrome(WebDriver): 1633 | def __init__(self, service: ChromeService = None, options: ChromeOptions = None): 1634 | """ 1635 | Creates a new instance of Chrome. 1636 | 1637 | :param service: Chrome Service instance, providing service 1638 | :param options: Chrome Options instance, providing additional options 1639 | """ 1640 | web_driver_info = WebDriverInfo(WebDriverPlatform.PC, WebDriverContext.CHROME) 1641 | selenium_web_driver = SeleniumChrome(service=service, options=options) 1642 | WebDriver.__init__(self, selenium_web_driver=selenium_web_driver, web_driver_info=web_driver_info) 1643 | 1644 | 1645 | class Opera(WebDriver): 1646 | def __init__(self, options: OperaOptions = None): 1647 | """ 1648 | Creates a new instance of Opera. 1649 | 1650 | :param options: Opera Options instance, providing additional options 1651 | """ 1652 | web_driver_info = WebDriverInfo(WebDriverPlatform.PC, WebDriverContext.OPERA) 1653 | selenium_web_driver = SeleniumOpera(options=options) 1654 | WebDriver.__init__(self, selenium_web_driver=selenium_web_driver, web_driver_info=web_driver_info) 1655 | 1656 | 1657 | class Safari(WebDriver): 1658 | def __init__(self, service: SafariService = None, options: SafariOptions = None): 1659 | """ 1660 | Creates a new instance of Safari. 1661 | 1662 | :param service: Safari Service instance, providing service 1663 | :param options: Safari Options instance, providing additional options 1664 | """ 1665 | web_driver_info = WebDriverInfo(WebDriverPlatform.PC, WebDriverContext.SAFARI) 1666 | selenium_web_driver = SeleniumSafari(service=service, options=options) 1667 | WebDriver.__init__(self, selenium_web_driver=selenium_web_driver, web_driver_info=web_driver_info) 1668 | 1669 | 1670 | class Edge(WebDriver): 1671 | def __init__(self, service: EdgeService = None, options: EdgeOptions = None): 1672 | """ 1673 | Creates a new instance of Edge. 1674 | 1675 | :param service: Edge Service instance, providing service 1676 | :param options: Edge Options instance, providing additional options 1677 | """ 1678 | web_driver_info = WebDriverInfo(WebDriverPlatform.PC, WebDriverContext.EDGE) 1679 | selenium_web_driver = SeleniumEdge(service=service, options=options) 1680 | WebDriver.__init__(self, selenium_web_driver=selenium_web_driver, web_driver_info=web_driver_info) 1681 | 1682 | 1683 | class Appium(WebDriver): 1684 | def __init__(self, command_executor: str = "http://127.0.0.1:4444/wd/hub", desired_capabilities: dict = None, 1685 | browser_profile: str = None, proxy: object = None, keep_alive: bool = False, 1686 | direct_connection: bool = True, extensions=[], strict_ssl: bool = True): 1687 | """ 1688 | Create a new driver that will issue commands using the wire protocol. 1689 | 1690 | :param command_executor: Either a string representing URL of the remote server or a custom remote_connection.RemoteConnection object. Defaults to 'http://127.0.0.1:4444/wd/hub'. 1691 | :param desired_capabilities: A dictionary of capabilities to request when starting the browser session. Required parameter. 1692 | :param browser_profile: A selenium.webdriver.firefox.firefox_profile.FirefoxProfile object. Only used if Firefox is requested. Optional. 1693 | :param proxy: A selenium.webdriver.common.proxy.Proxy object. The browser session will be started with given proxy settings, if possible. Optional. 1694 | :param keep_alive: Whether to configure remote_connection.RemoteConnection to use HTTP keep-alive. Defaults to False. 1695 | """ 1696 | if "platformName" in desired_capabilities: 1697 | platform = { 1698 | "ios": WebDriverPlatform.IOS, 1699 | "android": WebDriverPlatform.ANDROID 1700 | }.get(desired_capabilities["platformName"].lower(), WebDriverPlatform.IOS) 1701 | else: 1702 | platform = WebDriverPlatform.IOS 1703 | 1704 | web_driver_info = WebDriverInfo(platform, WebDriverContext.NATIVE_APP) 1705 | 1706 | selenium_web_driver = AppiumWebDriver(command_executor=command_executor, desired_capabilities=desired_capabilities, 1707 | browser_profile=browser_profile, proxy=proxy, keep_alive=keep_alive, 1708 | direct_connection=direct_connection, extensions=extensions, strict_ssl=strict_ssl) 1709 | # avoid that "autoWebview" in desired_capabilities affects the default context 1710 | if web_driver_info.context == WebDriverContext.NATIVE_APP and selenium_web_driver.current_context != "NATIVE_APP": 1711 | web_driver_info.context = WebDriverContext.WEB_VIEW 1712 | 1713 | WebDriver.__init__(self, selenium_web_driver=selenium_web_driver, web_driver_info=web_driver_info) 1714 | -------------------------------------------------------------------------------- /examples/google.py: -------------------------------------------------------------------------------- 1 | from easyium import Chrome, StaticElement 2 | 3 | 4 | # This class maps the google page in the browser. 5 | class Google: 6 | def __init__(self): 7 | # Create a WebDriver instance for chrome. 8 | self._web_driver = Chrome() 9 | 10 | # The google apps grid button is in the top-right. 11 | # This button is always in the page, so it is StaticElement. 12 | self._google_apps_grid_button = StaticElement(self._web_driver, "class=gb_b") 13 | 14 | # After clicked the google apps grid button, the google apps list will be shown. 15 | # This list is always in the page, although it is invisible, it is StaticElement. 16 | self._google_apps_list = StaticElement(self._web_driver, "class=gb_ha") 17 | 18 | # Currently the StaticElement does not refer to WebElement in Browser, 19 | # so open url here is fine. 20 | self._web_driver.get("https://www.google.com") 21 | 22 | def click_google_apps_grid_button(self): 23 | # It is StaticElement, easyium will wait it to be visible automatically. 24 | self._google_apps_grid_button.click() 25 | 26 | # Let's return the GoogleAppsList object. 27 | return GoogleAppsList(self._google_apps_list) 28 | 29 | def quit(self): 30 | self._web_driver.quit() 31 | 32 | 33 | # This class maps google apps list in the browser. 34 | class GoogleAppsList: 35 | def __init__(self, element): 36 | self._element = element 37 | 38 | def wait_until_ready(self): 39 | # Wait for the google apps list visible. 40 | self._element.wait_for().visible() 41 | 42 | # In most cases we should wait for the mask not existing here . 43 | # But in this case, no mask here. 44 | # self.__loading_mask.wait_for().not_().exists() 45 | 46 | def get_all_apps(self): 47 | # We should wait this control until ready. 48 | self.wait_until_ready() 49 | 50 | # Find the elements under google apps list. 51 | # We do not know how many apps in the list, so use find_elements(locator). 52 | # The found elements are DynamicElements. 53 | return [GoogleApp(e) for e in self._element.find_elements("class=gb_Z")] 54 | 55 | 56 | # This class maps google app in the browser. 57 | class GoogleApp: 58 | def __init__(self, element): 59 | self._element = element 60 | 61 | # This locator is relative to parent. 62 | self._name = StaticElement(self._element, "class=gb_4") 63 | 64 | def get_name(self): 65 | # get_text() doesn't work here, so use javascript 66 | # return self.name.get_text() 67 | return self._name.get_web_driver().execute_script('return arguments[0].innerText', self._name) 68 | 69 | if __name__ == '__main__': 70 | google = Google() 71 | google_apps_list = google.click_google_apps_grid_button() 72 | for app in google_apps_list.get_all_apps(): 73 | print(app.get_name()) 74 | google.quit() 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from setuptools import setup 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | # Get the long description from the relevant file 7 | with open(path.join(here, "README.rst")) as f: 8 | long_description = f.read() 9 | 10 | classifiers = ["License :: OSI Approved :: Apache Software License", 11 | "Topic :: Software Development :: Testing", 12 | "Operating System :: Microsoft :: Windows", 13 | "Operating System :: MacOS :: MacOS X", 14 | "Programming Language :: Python"] \ 15 | + [("Programming Language :: Python :: %s" % x) for x in "3.7 3.8 3.9".split()] 16 | 17 | 18 | def main(): 19 | setup( 20 | name="easyium", 21 | description="easy use of selenium and appium", 22 | long_description=long_description, 23 | install_requires=['selenium==4.1.0', 'appium-python-client==2.1.1'], 24 | version="2.0.0", 25 | keywords="selenium appium test testing framework automation", 26 | author="Karl Gong", 27 | author_email="karl.gong@outlook.com", 28 | url="https://github.com/KarlGong/easyium-python", 29 | license="Apache", 30 | classifiers=classifiers, 31 | packages=["easyium"], 32 | zip_safe=False, 33 | ) 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | --------------------------------------------------------------------------------