diff --git a/.gitignore b/.gitignore index 710b3114..911a3616 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *._* *.log *.log.* +_*/ *.pyc *.egg-info @@ -14,10 +15,12 @@ MANIFEST build dist +# Cache .cache __pycache__ .idea .pytest_cache +.mypy_cache # Virtual Environments venv* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c612326a..a4f7c00a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,3 +8,9 @@ hooks: - id: isort args: ["-rc", "."] +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v0.761' + hooks: + - id: mypy + entry: mypy appium/ test/ + pass_filenames: false diff --git a/.travis.yml b/.travis.yml index 4e51e993..84e0a624 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,8 @@ language: python dist: xenial python: - - "2.7" - - "3.4" - - "3.5" - - "3.6" - "3.7" -# - "3.8-dev" # TODO Remove comment-out when pylint and astroid upgraded to the latest and py2 support dropped + - "3.8" install: - pip install tox-travis diff --git a/Pipfile b/Pipfile index 7446b7b3..b07ffb04 100644 --- a/Pipfile +++ b/Pipfile @@ -4,14 +4,14 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -pre-commit = "~=1.13" +pre-commit = "~=2.1" [packages] selenium = "~=3.141" autopep8 = "~=1.5" -pytest = "~=4.0" +pytest = "~=5.3" pytest-cov = "~=2.8" tox = "~=3.14" @@ -19,9 +19,10 @@ tox-travis = "~=0.12" httpretty = "~=0.9" python-dateutil = "~=2.8" -mock = "~=3.0" +mock = "~=4.0" -# TODO Update to the latest ver when py2 support dropped -pylint = "~=1.9" -astroid = "~=1.6" +pylint = "~=2.4" +astroid = "~=2.3" isort = "~=4.3" + +mypy = "==0.761" diff --git a/README.md b/README.md index 34708837..94319fbb 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,18 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz). - Style Guide: https://www.python.org/dev/peps/pep-0008/ - `autopep8` helps to format code automatically - ``` + ```shell $ python -m autopep8 -r --global-config .config-pep8 -i . ``` - `isort` helps to order imports automatically - ``` + ```shell $ python -m isort -rc . ``` - When you use newly 3rd party modules, add it to [.isort.cfg](.isort.cfg) to keep import order correct + - `mypy` helps to check explicit type declarations + ```shell + $ python -m mypy appium + ``` - Docstring style: Google Style - Refer [link](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) - You can customise `CHANGELOG.rst` with commit messages following [.gitchangelog.rc](.gitchangelog.rc) diff --git a/appium/common/helper.py b/appium/common/helper.py index b694b9fa..1f5ec49a 100644 --- a/appium/common/helper.py +++ b/appium/common/helper.py @@ -12,44 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import OrderedDict +from typing import Any, Dict from appium import version as appium_version -def appium_bytes(value, encoding): - """Return a bytes-like object - - Has _appium_ prefix to avoid overriding built-in bytes. - - Args: - value (str): A value to convert - encoding (str): A encoding which will convert to - - Returns: - str: A bytes-like object - """ - - try: - return bytes(value, encoding) # Python 3 - except TypeError: - return value # Python 2 - - -def extract_const_attributes(cls): +def extract_const_attributes(cls: type) -> Dict[str, Any]: """Return dict with constants attributes and values in the class(e.g. {'VAL1': 1, 'VAL2': 2}) Args: cls (type): Class to be extracted constants Returns: - OrderedDict: dict with constants attributes and values in the class + dict: dict with constants attributes and values in the class """ - return OrderedDict( - [(attr, value) for attr, value in vars(cls).items() if not callable(getattr(cls, attr)) and attr.isupper()]) + return dict([(attr, value) for attr, value in vars(cls).items() + if not callable(getattr(cls, attr)) and attr.isupper()]) -def library_version(): +def library_version() -> str: """Return a version of this python library """ diff --git a/appium/common/logger.py b/appium/common/logger.py index 372d7fa8..014b69e4 100644 --- a/appium/common/logger.py +++ b/appium/common/logger.py @@ -16,7 +16,7 @@ import sys -def setup_logger(level=logging.NOTSET): +def setup_logger(level: int = logging.NOTSET) -> None: logger.propagate = False logger.setLevel(level) handler = logging.StreamHandler(stream=sys.stderr) diff --git a/appium/saucetestcase.py b/appium/saucetestcase.py index 8192f504..754146fb 100644 --- a/appium/saucetestcase.py +++ b/appium/saucetestcase.py @@ -19,6 +19,7 @@ import os import sys import unittest +from typing import Callable, List from sauceclient import SauceClient @@ -29,8 +30,8 @@ sauce = SauceClient(SAUCE_USERNAME, SAUCE_ACCESS_KEY) -def on_platforms(platforms): - def decorator(base_class): +def on_platforms(platforms: List[str]) -> Callable[[type], None]: + def decorator(base_class: type) -> None: module = sys.modules[base_class.__module__].__dict__ for i, platform in enumerate(platforms): name = "%s_%s" % (base_class.__name__, i + 1) @@ -40,16 +41,16 @@ def decorator(base_class): class SauceTestCase(unittest.TestCase): - def setUp(self): - self.desired_capabilities['name'] = self.id() + def setUp(self) -> None: + self.desired_capabilities['name'] = self.id() # type: ignore sauce_url = "http://%s:%s@ondemand.saucelabs.com:80/wd/hub" self.driver = webdriver.Remote( - desired_capabilities=self.desired_capabilities, + desired_capabilities=self.desired_capabilities, # type: ignore command_executor=sauce_url % (SAUCE_USERNAME, SAUCE_ACCESS_KEY) ) self.driver.implicitly_wait(30) - def tearDown(self): + def tearDown(self) -> None: print("Link to your job: https://saucelabs.com/jobs/%s" % self.driver.session_id) try: if sys.exc_info() == (None, None, None): diff --git a/appium/webdriver/appium_connection.py b/appium/webdriver/appium_connection.py index 9b7218dd..f643a168 100644 --- a/appium/webdriver/appium_connection.py +++ b/appium/webdriver/appium_connection.py @@ -13,16 +13,20 @@ # limitations under the License. import uuid +from typing import TYPE_CHECKING, Any, Dict from selenium.webdriver.remote.remote_connection import RemoteConnection from appium.common.helper import library_version +if TYPE_CHECKING: + from urllib.parse import ParseResult + class AppiumConnection(RemoteConnection): @classmethod - def get_remote_connection_headers(cls, parsed_url, keep_alive=True): + def get_remote_connection_headers(cls, parsed_url: 'ParseResult', keep_alive: bool = True) -> Dict[str, Any]: """Override get_remote_connection_headers in RemoteConnection""" headers = RemoteConnection.get_remote_connection_headers(parsed_url, keep_alive=keep_alive) headers['User-Agent'] = 'appium/python {} ({})'.format(library_version(), headers['User-Agent']) diff --git a/appium/webdriver/appium_service.py b/appium/webdriver/appium_service.py index 42527c24..071092c5 100644 --- a/appium/webdriver/appium_service.py +++ b/appium/webdriver/appium_service.py @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. - import os -import subprocess +import subprocess as sp import sys import time +from typing import Any, List, Optional, TypeVar, Union import urllib3 @@ -27,7 +27,7 @@ STATUS_URL = '/wd/hub/status' -def find_executable(executable): +def find_executable(executable: str) -> Optional[str]: path = os.environ['PATH'] paths = path.split(os.pathsep) base, ext = os.path.splitext(executable) @@ -45,13 +45,12 @@ def find_executable(executable): return None -def poll_url(host, port, path, timeout_ms): +def poll_url(host: str, port: int, path: str, timeout_ms: int) -> bool: time_started_sec = time.time() while time.time() < time_started_sec + timeout_ms / 1000.0: try: conn = urllib3.PoolManager(timeout=1.0) - resp = conn.request('HEAD', 'http://{host}:{port}{path}'.format( - host=host, port=port, path=path)) + resp = conn.request('HEAD', f'http://{host}:{port}{path}') if resp.status < 400: return True except Exception: @@ -64,12 +63,12 @@ class AppiumServiceError(RuntimeError): pass -class AppiumService(object): - def __init__(self): - self._process = None - self._cmd = None +class AppiumService: + def __init__(self) -> None: + self._process: Optional[sp.Popen] = None + self._cmd: Optional[List] = None - def _get_node(self): + def _get_node(self) -> str: if not hasattr(self, '_node_executable'): self._node_executable = find_executable('node') if self._node_executable is None: @@ -77,7 +76,7 @@ def _get_node(self): 'Make sure it is installed and present in PATH') return self._node_executable - def _get_npm(self): + def _get_npm(self) -> str: if not hasattr(self, '_npm_executable'): self._npm_executable = find_executable('npm.cmd' if sys.platform == 'win32' else 'npm') if self._npm_executable is None: @@ -85,41 +84,41 @@ def _get_npm(self): 'Make sure it is installed and present in PATH') return self._npm_executable - def _get_main_script(self): + def _get_main_script(self) -> Union[str, bytes]: if not hasattr(self, '_main_script'): for args in [['root', '-g'], ['root']]: try: - modules_root = subprocess.check_output([self._get_npm()] + args).strip().decode('utf-8') + modules_root = sp.check_output([self._get_npm()] + args).strip().decode('utf-8') if os.path.exists(os.path.join(modules_root, MAIN_SCRIPT_PATH)): - self._main_script = os.path.join(modules_root, MAIN_SCRIPT_PATH) + self._main_script: Union[str, bytes] = os.path.join(modules_root, MAIN_SCRIPT_PATH) break - except subprocess.CalledProcessError: + except sp.CalledProcessError: continue if not hasattr(self, '_main_script'): try: - self._main_script = subprocess.check_output( + self._main_script = sp.check_output( [self._get_node(), '-e', 'console.log(require.resolve("{}"))'.format(MAIN_SCRIPT_PATH)]).strip() - except subprocess.CalledProcessError as e: - raise AppiumServiceError(e.output) + except sp.CalledProcessError as e: + raise AppiumServiceError(e.output) from e return self._main_script @staticmethod - def _parse_port(args): + def _parse_port(args: List[str]) -> int: for idx, arg in enumerate(args or []): if arg in ('--port', '-p') and idx < len(args) - 1: return int(args[idx + 1]) return DEFAULT_PORT @staticmethod - def _parse_host(args): + def _parse_host(args: List[str]) -> str: for idx, arg in enumerate(args or []): if arg in ('--address', '-a') and idx < len(args) - 1: return args[idx + 1] return DEFAULT_HOST - def start(self, **kwargs): + def start(self, **kwargs: Any) -> sp.Popen: """Starts Appium service with given arguments. The service will be forcefully restarted if it is already running. @@ -153,31 +152,30 @@ def start(self, **kwargs): env = kwargs['env'] if 'env' in kwargs else None node = kwargs['node'] if 'node' in kwargs else self._get_node() - stdout = kwargs['stdout'] if 'stdout' in kwargs else subprocess.PIPE - stderr = kwargs['stderr'] if 'stderr' in kwargs else subprocess.PIPE + stdout = kwargs['stdout'] if 'stdout' in kwargs else sp.PIPE + stderr = kwargs['stderr'] if 'stderr' in kwargs else sp.PIPE timeout_ms = int(kwargs['timeout_ms']) if 'timeout_ms' in kwargs else STARTUP_TIMEOUT_MS main_script = kwargs['main_script'] if 'main_script' in kwargs else self._get_main_script() args = [node, main_script] if 'args' in kwargs: args.extend(kwargs['args']) self._cmd = args - self._process = subprocess.Popen(args=args, stdout=stdout, stderr=stderr, env=env) + self._process = sp.Popen(args=args, stdout=stdout, stderr=stderr, env=env) host = self._parse_host(args) port = self._parse_port(args) - error_msg = None + error_msg: Optional[str] = None if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, STATUS_URL, timeout_ms)): - error_msg = 'Appium has failed to start on {}:{} within {}ms timeout'\ - .format(host, port, timeout_ms) + error_msg = f'Appium has failed to start on {host}:{port} within {timeout_ms}ms timeout' if error_msg is not None: - if stderr == subprocess.PIPE: + if stderr == sp.PIPE and self._process.stderr is not None: err_output = self._process.stderr.read() if err_output: - error_msg += '\nOriginal error: {}'.format(err_output) + error_msg += f'\nOriginal error: {str(err_output)}' self.stop() raise AppiumServiceError(error_msg) return self._process - def stop(self): + def stop(self) -> bool: """Stops Appium service if it is running. The call will be ignored if the service is not running @@ -188,14 +186,14 @@ def stop(self): """ is_terminated = False if self.is_running: - self._process.terminate() + self._process.terminate() # type: ignore is_terminated = True self._process = None self._cmd = None return is_terminated @property - def is_running(self): + def is_running(self) -> bool: """Check if the service is running. Returns: @@ -204,7 +202,7 @@ def is_running(self): return self._process is not None and self._process.poll() is None @property - def is_listening(self): + def is_listening(self) -> bool: """Check if the service is listening on the given/default host/port. The fact, that the service is running, does not always mean it is listening. diff --git a/appium/webdriver/applicationstate.py b/appium/webdriver/applicationstate.py index e3c255ae..0658b264 100644 --- a/appium/webdriver/applicationstate.py +++ b/appium/webdriver/applicationstate.py @@ -13,7 +13,7 @@ # limitations under the License. -class ApplicationState(object): +class ApplicationState: NOT_INSTALLED = 0 NOT_RUNNING = 1 RUNNING_IN_BACKGROUND_SUSPENDED = 2 diff --git a/appium/webdriver/clipboard_content_type.py b/appium/webdriver/clipboard_content_type.py index 67aac351..7c1211b3 100644 --- a/appium/webdriver/clipboard_content_type.py +++ b/appium/webdriver/clipboard_content_type.py @@ -13,7 +13,7 @@ # limitations under the License. -class ClipboardContentType(object): +class ClipboardContentType: PLAINTEXT = 'plaintext' IMAGE = 'image' URL = 'url' diff --git a/appium/webdriver/common/multi_action.py b/appium/webdriver/common/multi_action.py index f80a111f..692decad 100644 --- a/appium/webdriver/common/multi_action.py +++ b/appium/webdriver/common/multi_action.py @@ -19,17 +19,25 @@ # chaining as the spec requires. import copy +from typing import TYPE_CHECKING, Dict, List, Optional, TypeVar, Union from appium.webdriver.mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + from appium.webdriver.webelement import WebElement + from appium.webdriver.common.touch_action import TouchAction -class MultiAction(object): - def __init__(self, driver, element=None): +T = TypeVar('T', bound='MultiAction') + + +class MultiAction: + def __init__(self, driver: 'WebDriver', element: Optional['WebElement'] = None) -> None: self._driver = driver self._element = element - self._touch_actions = [] + self._touch_actions: List['TouchAction'] = [] - def add(self, *touch_actions): + def add(self, *touch_actions: 'TouchAction') -> None: """Add TouchAction objects to the MultiAction, to be performed later. Args: @@ -37,8 +45,11 @@ def add(self, *touch_actions): Usage: a1 = TouchAction(driver) + a1.press(el1).move_to(el2).release() + a2 = TouchAction(driver) + a2.press(el2).move_to(el1).release() MultiAction(driver).add(a1, a2) @@ -49,13 +60,16 @@ def add(self, *touch_actions): self._touch_actions.append(copy.copy(touch_action)) - def perform(self): + def perform(self: T) -> T: """Perform the actions stored in the object. Usage: a1 = TouchAction(driver) + a1.press(el1).move_to(el2).release() + a2 = TouchAction(driver) + a2.press(el2).move_to(el1).release() MultiAction(driver).add(a1, a2).perform() @@ -68,7 +82,7 @@ def perform(self): return self @property - def json_wire_gestures(self): + def json_wire_gestures(self) -> Dict[str, Union[List, str]]: actions = [] for action in self._touch_actions: actions.append(action.json_wire_gestures) diff --git a/appium/webdriver/common/touch_action.py b/appium/webdriver/common/touch_action.py index 84668e17..7806a72f 100644 --- a/appium/webdriver/common/touch_action.py +++ b/appium/webdriver/common/touch_action.py @@ -24,16 +24,25 @@ # pylint: disable=no-self-use import copy +from typing import TYPE_CHECKING, Dict, List, Optional, TypeVar, Union from appium.webdriver.mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + from appium.webdriver.webdriver import WebDriver -class TouchAction(object): - def __init__(self, driver=None): +T = TypeVar('T', bound='TouchAction') + + +class TouchAction: + + def __init__(self, driver: Optional['WebDriver'] = None): self._driver = driver - self._actions = [] + self._actions: List = [] - def tap(self, element=None, x=None, y=None, count=1): + def tap(self: T, element: Optional['WebElement'] = None, x: Optional[int] + = None, y: Optional[int] = None, count: int = 1) -> T: """Perform a tap action on the element Args: @@ -50,7 +59,8 @@ def tap(self, element=None, x=None, y=None, count=1): return self - def press(self, el=None, x=None, y=None, pressure=None): + def press(self: T, el: Optional['WebElement'] = None, x: Optional[int] = None, + y: Optional[int] = None, pressure: Optional[float] = None) -> T: """Begin a chain with a press down action at a particular element or point Args: @@ -67,7 +77,8 @@ def press(self, el=None, x=None, y=None, pressure=None): return self - def long_press(self, el=None, x=None, y=None, duration=1000): + def long_press(self: T, el: Optional['WebElement'] = None, x: Optional[int] + = None, y: Optional[int] = None, duration: int = 1000) -> T: """Begin a chain with a press down that lasts `duration` milliseconds Args: @@ -83,7 +94,7 @@ def long_press(self, el=None, x=None, y=None, duration=1000): return self - def wait(self, ms=0): + def wait(self: T, ms: int = 0) -> T: """Pause for `ms` milliseconds. Args: @@ -101,7 +112,7 @@ def wait(self, ms=0): return self - def move_to(self, el=None, x=None, y=None): + def move_to(self: T, el: Optional['WebElement'] = None, x: Optional[int] = None, y: Optional[int] = None) -> T: """Move the pointer from the previous point to the element or point specified Args: @@ -116,7 +127,7 @@ def move_to(self, el=None, x=None, y=None): return self - def release(self): + def release(self: T) -> T: """End the action by lifting the pointer off the screen Returns: @@ -126,12 +137,14 @@ def release(self): return self - def perform(self): + def perform(self: T) -> T: """Perform the action by sending the commands to the server to be operated upon Returns: `TouchAction`: self instance """ + if self._driver is None: + raise ValueError('Set driver to constructor as a argument when to create the instance.') params = {'actions': self._actions} self._driver.execute(Command.TOUCH_ACTION, params) @@ -141,23 +154,24 @@ def perform(self): return self @property - def json_wire_gestures(self): + def json_wire_gestures(self) -> List[Dict]: gestures = [] for action in self._actions: gestures.append(copy.deepcopy(action)) return gestures - def _add_action(self, action, options): + def _add_action(self, action: str, options: Dict) -> None: gesture = { 'action': action, 'options': options, } self._actions.append(gesture) - def _get_opts(self, element, x, y, duration=None, pressure=None): + def _get_opts(self, el: Optional['WebElement'] = None, x: Optional[int] = None, y: Optional[int] = None, + duration: Optional[int] = None, pressure: Optional[float] = None) -> Dict[str, Union[int, float]]: opts = {} - if element is not None: - opts['element'] = element.id + if el is not None: + opts['element'] = el.id # it makes no sense to have x but no y, or vice versa. if x is not None and y is not None: diff --git a/appium/webdriver/connectiontype.py b/appium/webdriver/connectiontype.py index 5a408ba5..7db69ba3 100644 --- a/appium/webdriver/connectiontype.py +++ b/appium/webdriver/connectiontype.py @@ -16,17 +16,25 @@ """ Connection types are specified here: https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile#120 - Value (Alias) | Data | Wifi | Airplane Mode - ------------------------------------------------- - 0 (None) | 0 | 0 | 0 - 1 (Airplane Mode) | 0 | 0 | 1 - 2 (Wifi only) | 0 | 1 | 0 - 4 (Data only) | 1 | 0 | 0 - 6 (All network on) | 1 | 1 | 0 + + +--------------------+------+------+---------------+ + | Value (Alias) | Data | Wifi | Airplane Mode | + +====================+======+======+===============+ + | 0 (None) | 0 | 0 | 0 | + +--------------------+------+------+---------------+ + | 1 (Airplane Mode) | 0 | 0 | 1 | + +--------------------+------+------+---------------+ + | 2 (Wifi only) | 0 | 1 | 0 | + +--------------------+------+------+---------------+ + | 4 (Data only) | 1 | 0 | 0 | + +--------------------+------+------+---------------+ + | 6 (All network on) | 1 | 1 | 0 | + +--------------------+------+------+---------------+ + """ -class ConnectionType(object): +class ConnectionType: NO_CONNECTION = 0 AIRPLANE_MODE = 1 WIFI_ONLY = 2 diff --git a/appium/webdriver/errorhandler.py b/appium/webdriver/errorhandler.py index 1fa793ba..8612d86a 100644 --- a/appium/webdriver/errorhandler.py +++ b/appium/webdriver/errorhandler.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict + from selenium.common.exceptions import WebDriverException from selenium.webdriver.remote import errorhandler @@ -19,11 +21,11 @@ class MobileErrorHandler(errorhandler.ErrorHandler): - def check_response(self, response): + def check_response(self, response: Dict) -> None: try: - super(MobileErrorHandler, self).check_response(response) + super().check_response(response) except WebDriverException as wde: if wde.msg == 'No such context found.': - raise NoSuchContextException(wde.msg, wde.screen, wde.stacktrace) + raise NoSuchContextException(wde.msg, wde.screen, wde.stacktrace) from wde else: raise wde diff --git a/appium/webdriver/extensions/action_helpers.py b/appium/webdriver/extensions/action_helpers.py index 108fcfd5..6427a4b9 100644 --- a/appium/webdriver/extensions/action_helpers.py +++ b/appium/webdriver/extensions/action_helpers.py @@ -12,15 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, List, Optional, Tuple, TypeVar + from selenium import webdriver from appium.webdriver.common.multi_action import MultiAction from appium.webdriver.common.touch_action import TouchAction +from appium.webdriver.webelement import WebElement + +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') class ActionHelpers(webdriver.Remote): - def scroll(self, origin_el, destination_el, duration=None): + def scroll(self, origin_el: WebElement, destination_el: WebElement, duration: Optional[int] = None) -> T: """Scrolls from one element to another Args: @@ -47,7 +55,7 @@ def scroll(self, origin_el, destination_el, duration=None): action.press(origin_el).wait(duration).move_to(destination_el).release().perform() return self - def drag_and_drop(self, origin_el, destination_el): + def drag_and_drop(self, origin_el: WebElement, destination_el: WebElement) -> T: """Drag the origin element to the destination element Args: @@ -61,7 +69,7 @@ def drag_and_drop(self, origin_el, destination_el): action.long_press(origin_el).move_to(destination_el).release().perform() return self - def tap(self, positions, duration=None): + def tap(self, positions: List[Tuple[int, int]], duration: Optional[int] = None) -> T: """Taps on an particular place with up to five fingers, holding for a certain time @@ -100,7 +108,7 @@ def tap(self, positions, duration=None): ma.perform() return self - def swipe(self, start_x, start_y, end_x, end_y, duration=None): + def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = 0) -> T: """Swipe from one point to another point, for an optional duration. Args: @@ -127,7 +135,7 @@ def swipe(self, start_x, start_y, end_x, end_y, duration=None): action.perform() return self - def flick(self, start_x, start_y, end_x, end_y): + def flick(self, start_x: int, start_y: int, end_x: int, end_y: int) -> T: """Flick from one point to another point. Args: diff --git a/appium/webdriver/extensions/android/activities.py b/appium/webdriver/extensions/android/activities.py index 236252c3..2b2f3b11 100644 --- a/appium/webdriver/extensions/android/activities.py +++ b/appium/webdriver/extensions/android/activities.py @@ -12,15 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, TypeVar + from selenium import webdriver from selenium.common.exceptions import TimeoutException from selenium.webdriver.support.ui import WebDriverWait from appium.webdriver.mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class Activities(webdriver.Remote): - def start_activity(self, app_package, app_activity, **opts): + def start_activity(self, app_package: str, app_activity: str, **opts: str) -> T: """Opens an arbitrary activity during a test. If the activity belongs to another application, that application is started and the activity is opened. @@ -59,7 +66,7 @@ def start_activity(self, app_package, app_activity, **opts): return self @property - def current_activity(self): + def current_activity(self) -> str: """Retrieves the current activity running on the device. Returns: @@ -67,7 +74,7 @@ def current_activity(self): """ return self.execute(Command.GET_CURRENT_ACTIVITY)['value'] - def wait_activity(self, activity, timeout, interval=1): + def wait_activity(self, activity: str, timeout: int, interval: int = 1) -> bool: """Wait for an activity: block until target activity presents or time out. This is an Android-only method. @@ -76,6 +83,9 @@ def wait_activity(self, activity, timeout, interval=1): activity (str): target activity timeout (int): max wait time, in seconds interval (int): sleep interval between retries, in seconds + + Returns: + bool: `True` if the target activity is shown """ try: WebDriverWait(self, timeout, interval).until( @@ -86,7 +96,7 @@ def wait_activity(self, activity, timeout, interval=1): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_CURRENT_ACTIVITY] = \ ('GET', '/session/$sessionId/appium/device/current_activity') self.command_executor._commands[Command.START_ACTIVITY] = \ diff --git a/appium/webdriver/extensions/android/common.py b/appium/webdriver/extensions/android/common.py index 4fa3a00f..a4c32904 100644 --- a/appium/webdriver/extensions/android/common.py +++ b/appium/webdriver/extensions/android/common.py @@ -12,14 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, Any, TypeVar + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class Common(webdriver.Remote): - def end_test_coverage(self, intent, path): + def end_test_coverage(self, intent: str, path: str) -> Any: # TODO Check return type """Ends the coverage collection and pull the coverage.ec file from the device. Android only. @@ -38,7 +45,7 @@ def end_test_coverage(self, intent, path): } return self.execute(Command.END_TEST_COVERAGE, data)['value'] - def open_notifications(self): + def open_notifications(self) -> T: """Open notification shade in Android (API Level 18 and above) Returns: @@ -48,12 +55,12 @@ def open_notifications(self): return self @property - def current_package(self): + def current_package(self) -> str: """Retrieves the current package running on the device. """ return self.execute(Command.GET_CURRENT_PACKAGE)['value'] - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_CURRENT_PACKAGE] = \ ('GET', '/session/$sessionId/appium/device/current_package') self.command_executor._commands[Command.END_TEST_COVERAGE] = \ diff --git a/appium/webdriver/extensions/android/display.py b/appium/webdriver/extensions/android/display.py index c05e1a5d..f07c0875 100644 --- a/appium/webdriver/extensions/android/display.py +++ b/appium/webdriver/extensions/android/display.py @@ -19,7 +19,7 @@ class Display(webdriver.Remote): - def get_display_density(self): + def get_display_density(self) -> int: """Get the display density, Android only Returns: @@ -32,6 +32,6 @@ def get_display_density(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_DISPLAY_DENSITY] = \ ('GET', '/session/$sessionId/appium/device/display_density') diff --git a/appium/webdriver/extensions/android/gsm.py b/appium/webdriver/extensions/android/gsm.py index 4aaf4fc8..dd255446 100644 --- a/appium/webdriver/extensions/android/gsm.py +++ b/appium/webdriver/extensions/android/gsm.py @@ -12,21 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, TypeVar + from selenium import webdriver from appium.common.helper import extract_const_attributes from appium.common.logger import logger from appium.webdriver.mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + -class GsmCallActions(object): +class GsmCallActions: CALL = 'call' ACCEPT = 'accept' CANCEL = 'cancel' HOLD = 'hold' -class GsmSignalStrength(object): +class GsmSignalStrength: NONE_OR_UNKNOWN = 0 POOR = 1 MODERATE = 2 @@ -34,7 +39,7 @@ class GsmSignalStrength(object): GREAT = 4 -class GsmVoiceState(object): +class GsmVoiceState: UNREGISTERED = 'unregistered' HOME = 'home' ROAMING = 'roaming' @@ -44,9 +49,12 @@ class GsmVoiceState(object): ON = 'on' +T = TypeVar('T', bound='WebDriver') + + class Gsm(webdriver.Remote): - def make_gsm_call(self, phone_number, action): + def make_gsm_call(self, phone_number: str, action: str) -> T: """Make GSM call (Emulator only) Android only. @@ -61,12 +69,12 @@ def make_gsm_call(self, phone_number, action): """ constants = extract_const_attributes(GsmCallActions) if action not in constants.values(): - logger.warning('{} is unknown. Consider using one of {} constants. (e.g. {}.CALL)'.format( - action, list(constants.keys()), GsmCallActions.__name__)) + logger.warning( + f'{action} is unknown. Consider using one of {list(constants.keys())} constants. (e.g. {GsmCallActions.__name__}.CALL)') self.execute(Command.MAKE_GSM_CALL, {'phoneNumber': phone_number, 'action': action}) return self - def set_gsm_signal(self, strength): + def set_gsm_signal(self, strength: int) -> T: """Set GSM signal strength (Emulator only) Android only. @@ -80,12 +88,12 @@ def set_gsm_signal(self, strength): """ constants = extract_const_attributes(GsmSignalStrength) if strength not in constants.values(): - logger.warning('{} is out of range. Consider using one of {} constants. (e.g. {}.GOOD)'.format( - strength, list(constants.keys()), GsmSignalStrength.__name__)) + logger.warning( + f'{strength} is out of range. Consider using one of {list(constants.keys())} constants. (e.g. {GsmSignalStrength.__name__}.GOOD)') self.execute(Command.SET_GSM_SIGNAL, {'signalStrength': strength, 'signalStrengh': strength}) return self - def set_gsm_voice(self, state): + def set_gsm_voice(self, state: str) -> T: """Set GSM voice state (Emulator only) Android only. @@ -99,14 +107,14 @@ def set_gsm_voice(self, state): """ constants = extract_const_attributes(GsmVoiceState) if state not in constants.values(): - logger.warning('{} is unknown. Consider using one of {} constants. (e.g. {}.HOME)'.format( - state, list(constants.keys()), GsmVoiceState.__name__)) + logger.warning( + f'{state} is unknown. Consider using one of {list(constants.keys())} constants. (e.g. {GsmVoiceState.__name__}.HOME)') self.execute(Command.SET_GSM_VOICE, {'state': state}) return self # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.MAKE_GSM_CALL] = \ ('POST', '/session/$sessionId/appium/device/gsm_call') self.command_executor._commands[Command.SET_GSM_SIGNAL] = \ diff --git a/appium/webdriver/extensions/android/nativekey.py b/appium/webdriver/extensions/android/nativekey.py index aca374eb..79973dba 100644 --- a/appium/webdriver/extensions/android/nativekey.py +++ b/appium/webdriver/extensions/android/nativekey.py @@ -1,3 +1,18 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + class AndroidKey: # Key code constant: Unknown key code. UNKNOWN = 0 @@ -1005,14 +1020,14 @@ class AndroidKey: BUTTON_15, BUTTON_16] @staticmethod - def is_gamepad_button(code): + def is_gamepad_button(code: int) -> bool: """Returns true if the specified nativekey is a gamepad button.""" return code in AndroidKey.gamepad_buttons confirm_buttons = [DPAD_CENTER, ENTER, SPACE, NUMPAD_ENTER] @staticmethod - def is_confirm_key(code): + def is_confirm_key(code: int) -> bool: """Returns true if the key will, by default, trigger a click on the focused view.""" return code in AndroidKey.confirm_buttons @@ -1021,7 +1036,7 @@ def is_confirm_key(code): MEDIA_REWIND, MEDIA_RECORD, MEDIA_FAST_FORWARD] @staticmethod - def is_media_key(code): + def is_media_key(code: int) -> bool: """Returns true if this key is a media key, which can be send to apps that are interested in media key events.""" return code in AndroidKey.media_buttons @@ -1035,13 +1050,13 @@ def is_media_key(code): BRIGHTNESS_DOWN, BRIGHTNESS_UP, MEDIA_AUDIO_TRACK] @staticmethod - def is_system_key(code): + def is_system_key(code: int) -> bool: """Returns true if the key is a system key, System keys can not be used for menu shortcuts.""" return code in AndroidKey.system_buttons wake_buttons = [BACK, MENU, WAKEUP, PAIRING, STEM_1, STEM_2, STEM_3] @staticmethod - def is_wake_key(code): + def is_wake_key(code: int) -> bool: """Returns true if the key is a wake key.""" return code in AndroidKey.wake_buttons diff --git a/appium/webdriver/extensions/android/network.py b/appium/webdriver/extensions/android/network.py index 3a648154..74acea79 100644 --- a/appium/webdriver/extensions/android/network.py +++ b/appium/webdriver/extensions/android/network.py @@ -12,14 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, TypeVar + from selenium import webdriver from appium.common.helper import extract_const_attributes from appium.common.logger import logger from appium.webdriver.mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') -class NetSpeed(object): + +class NetSpeed: GSM = 'gsm' # GSM/CSD (up: 14.4(kbps), down: 14.4(kbps)) SCSD = 'scsd' # HSCSD (up: 14.4, down: 57.6) GPRS = 'gprs' # GPRS (up: 28.8, down: 57.6) @@ -34,7 +41,7 @@ class NetSpeed(object): class Network(webdriver.Remote): @property - def network_connection(self): + def network_connection(self) -> int: """Returns an integer bitmask specifying the network connection type. Android only. @@ -42,24 +49,32 @@ def network_connection(self): """ return self.execute(Command.GET_NETWORK_CONNECTION, {})['value'] - def set_network_connection(self, connection_type): + def set_network_connection(self, connection_type: int) -> int: """Sets the network connection type. Android only. Possible values: - Value (Alias) | Data | Wifi | Airplane Mode - ------------------------------------------------- - 0 (None) | 0 | 0 | 0 - 1 (Airplane Mode) | 0 | 0 | 1 - 2 (Wifi only) | 0 | 1 | 0 - 4 (Data only) | 1 | 0 | 0 - 6 (All network on) | 1 | 1 | 0 + + +--------------------+------+------+---------------+ + | Value (Alias) | Data | Wifi | Airplane Mode | + +====================+======+======+===============+ + | 0 (None) | 0 | 0 | 0 | + +--------------------+------+------+---------------+ + | 1 (Airplane Mode) | 0 | 0 | 1 | + +--------------------+------+------+---------------+ + | 2 (Wifi only) | 0 | 1 | 0 | + +--------------------+------+------+---------------+ + | 4 (Data only) | 1 | 0 | 0 | + +--------------------+------+------+---------------+ + | 6 (All network on) | 1 | 1 | 0 | + +--------------------+------+------+---------------+ + These are available through the enumeration `appium.webdriver.ConnectionType` Args: connection_type (int): a member of the enum appium.webdriver.ConnectionType - Returns: - `appium.webdriver.webdriver.WebDriver` + Return: + int: Set network connection type """ data = { 'parameters': { @@ -68,7 +83,7 @@ def set_network_connection(self, connection_type): } return self.execute(Command.SET_NETWORK_CONNECTION, data)['value'] - def toggle_wifi(self): + def toggle_wifi(self) -> T: """Toggle the wifi on the device, Android only. Returns: @@ -77,7 +92,7 @@ def toggle_wifi(self): self.execute(Command.TOGGLE_WIFI, {}) return self - def set_network_speed(self, speed_type): + def set_network_speed(self, speed_type: str) -> T: """Set the network speed emulation. Android Emulator only. @@ -94,15 +109,15 @@ def set_network_speed(self, speed_type): """ constants = extract_const_attributes(NetSpeed) if speed_type not in constants.values(): - logger.warning('{} is unknown. Consider using one of {} constants. (e.g. {}.LTE)'.format( - speed_type, list(constants.keys()), NetSpeed.__name__)) + logger.warning( + f'{speed_type} is unknown. Consider using one of {list(constants.keys())} constants. (e.g. {NetSpeed.__name__}.LTE)') self.execute(Command.SET_NETWORK_SPEED, {'netspeed': speed_type}) return self # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.TOGGLE_WIFI] = \ ('POST', '/session/$sessionId/appium/device/toggle_wifi') self.command_executor._commands[Command.GET_NETWORK_CONNECTION] = \ diff --git a/appium/webdriver/extensions/android/performance.py b/appium/webdriver/extensions/android/performance.py index 7b77b74e..3d661d77 100644 --- a/appium/webdriver/extensions/android/performance.py +++ b/appium/webdriver/extensions/android/performance.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, List, Union + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class Performance(webdriver.Remote): - def get_performance_data(self, package_name, data_type, data_read_timeout=None): + def get_performance_data(self, package_name: str, data_type: str, data_read_timeout: int = None) -> List[List[str]]: """Returns the information of the system state which is supported to read as like cpu, memory, network traffic, and battery. @@ -36,14 +38,14 @@ def get_performance_data(self, package_name, data_type, data_read_timeout=None): self.driver.get_performance_data('my.app.package', 'cpuinfo', 5) Returns: - dict: The data along to `data_type` + list: The data along to `data_type` """ - data = {'packageName': package_name, 'dataType': data_type} + data: Dict[str, Union[str, int]] = {'packageName': package_name, 'dataType': data_type} if data_read_timeout is not None: data['dataReadTimeout'] = data_read_timeout return self.execute(Command.GET_PERFORMANCE_DATA, data)['value'] - def get_performance_data_types(self): + def get_performance_data_types(self) -> List: """Returns the information types of the system state which is supported to read as like cpu, memory, network traffic, and battery. Android only. @@ -58,7 +60,7 @@ def get_performance_data_types(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_PERFORMANCE_DATA] = \ ('POST', '/session/$sessionId/appium/getPerformanceData') self.command_executor._commands[Command.GET_PERFORMANCE_DATA_TYPES] = \ diff --git a/appium/webdriver/extensions/android/power.py b/appium/webdriver/extensions/android/power.py index bfa6a824..0f82ceb2 100644 --- a/appium/webdriver/extensions/android/power.py +++ b/appium/webdriver/extensions/android/power.py @@ -12,16 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, TypeVar + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class Power(webdriver.Remote): AC_OFF, AC_ON = 'off', 'on' - def set_power_capacity(self, percent): + def set_power_capacity(self, percent: int) -> T: """Emulate power capacity change on the connected emulator. Android only. @@ -38,7 +45,7 @@ def set_power_capacity(self, percent): self.execute(Command.SET_POWER_CAPACITY, {'percent': percent}) return self - def set_power_ac(self, ac_state): + def set_power_ac(self, ac_state: str) -> T: """Emulate power state change on the connected emulator. Android only. @@ -48,6 +55,7 @@ def set_power_ac(self, ac_state): Usage: self.driver.set_power_ac(Power.AC_OFF) + self.driver.set_power_ac(Power.AC_ON) Returns: @@ -58,7 +66,7 @@ def set_power_ac(self, ac_state): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.SET_POWER_CAPACITY] = \ ('POST', '/session/$sessionId/appium/device/power_capacity') self.command_executor._commands[Command.SET_POWER_AC] = \ diff --git a/appium/webdriver/extensions/android/sms.py b/appium/webdriver/extensions/android/sms.py index 4c0675a0..072c1867 100644 --- a/appium/webdriver/extensions/android/sms.py +++ b/appium/webdriver/extensions/android/sms.py @@ -12,14 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, TypeVar + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class Sms(webdriver.Remote): - def send_sms(self, phone_number, message): + def send_sms(self, phone_number: str, message: str) -> T: """Emulate send SMS event on the connected emulator. Android only. @@ -39,6 +46,6 @@ def send_sms(self, phone_number, message): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.SEND_SMS] = \ ('POST', '/session/$sessionId/appium/device/send_sms') diff --git a/appium/webdriver/extensions/android/system_bars.py b/appium/webdriver/extensions/android/system_bars.py index edc98a5e..085296d9 100644 --- a/appium/webdriver/extensions/android/system_bars.py +++ b/appium/webdriver/extensions/android/system_bars.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, Union + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class SystemBars(webdriver.Remote): - def get_system_bars(self): + def get_system_bars(self) -> Dict[str, Dict[str, Union[int, bool]]]: """Retrieve visibility and bounds information of the status and navigation bars. Android only. @@ -43,6 +45,6 @@ def get_system_bars(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_SYSTEM_BARS] = \ ('GET', '/session/$sessionId/appium/device/system_bars') diff --git a/appium/webdriver/extensions/applications.py b/appium/webdriver/extensions/applications.py index e89b097d..6f767c9b 100644 --- a/appium/webdriver/extensions/applications.py +++ b/appium/webdriver/extensions/applications.py @@ -12,13 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, Any, Dict, TypeVar + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class Applications(webdriver.Remote): - def background_app(self, seconds): + def background_app(self, seconds: int) -> T: """Puts the application in the background on the device for a certain duration. Args: @@ -33,7 +40,7 @@ def background_app(self, seconds): self.execute(Command.BACKGROUND, data) return self - def is_app_installed(self, bundle_id): + def is_app_installed(self, bundle_id: str) -> bool: """Checks whether the application specified by `bundle_id` is installed on the device. Args: @@ -47,7 +54,7 @@ def is_app_installed(self, bundle_id): } return self.execute(Command.IS_APP_INSTALLED, data)['value'] - def install_app(self, app_path, **options): + def install_app(self, app_path: str, **options: Any) -> T: """Install the application found at `app_path` on the device. Args: @@ -67,7 +74,7 @@ def install_app(self, app_path, **options): Returns: `appium.webdriver.webdriver.WebDriver` """ - data = { + data: Dict[str, Any] = { 'appPath': app_path, } if options: @@ -75,7 +82,7 @@ def install_app(self, app_path, **options): self.execute(Command.INSTALL_APP, data) return self - def remove_app(self, app_id, **options): + def remove_app(self, app_id: str, **options: Any) -> T: """Remove the specified application from the device. Args: @@ -90,7 +97,7 @@ def remove_app(self, app_id, **options): Returns: `appium.webdriver.webdriver.WebDriver` """ - data = { + data: Dict[str, Any] = { 'appId': app_id, } if options: @@ -98,7 +105,7 @@ def remove_app(self, app_id, **options): self.execute(Command.REMOVE_APP, data) return self - def launch_app(self): + def launch_app(self) -> T: """Start on the device the application specified in the desired capabilities. Returns: @@ -107,7 +114,7 @@ def launch_app(self): self.execute(Command.LAUNCH_APP) return self - def close_app(self): + def close_app(self) -> T: """Stop the running application, specified in the desired capabilities, on the device. @@ -117,7 +124,7 @@ def close_app(self): self.execute(Command.CLOSE_APP) return self - def terminate_app(self, app_id, **options): + def terminate_app(self, app_id: str, **options: Any) -> bool: """Terminates the application if it is running. Args: @@ -130,14 +137,14 @@ def terminate_app(self, app_id, **options): Returns: bool: True if the app has been successfully terminated """ - data = { + data: Dict[str, Any] = { 'appId': app_id, } if options: data.update({'options': options}) return self.execute(Command.TERMINATE_APP, data)['value'] - def activate_app(self, app_id): + def activate_app(self, app_id: str) -> T: """Activates the application if it is not running or is running in the background. @@ -153,7 +160,7 @@ def activate_app(self, app_id): self.execute(Command.ACTIVATE_APP, data) return self - def query_app_state(self, app_id): + def query_app_state(self, app_id: str) -> int: """Queries the state of the application. Args: @@ -168,13 +175,16 @@ class for more details. } return self.execute(Command.QUERY_APP_STATE, data)['value'] - def app_strings(self, language=None, string_file=None): + def app_strings(self, language: str = None, string_file: str = None) -> Dict[str, str]: """Returns the application strings from the device for the specified language. Args: language (str): strings language code string_file (str): the name of the string file to query + + Returns: + Dict[str, str]: The key is string id and the value is the content. """ data = {} if language is not None: @@ -183,7 +193,7 @@ def app_strings(self, language=None, string_file=None): data['stringFile'] = string_file return self.execute(Command.GET_APP_STRINGS, data)['value'] - def reset(self): + def reset(self) -> T: """Resets the current application on the device. """ self.execute(Command.RESET) @@ -191,7 +201,7 @@ def reset(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.BACKGROUND] = \ ('POST', '/session/$sessionId/appium/app/background') self.command_executor._commands[Command.IS_APP_INSTALLED] = \ diff --git a/appium/webdriver/extensions/clipboard.py b/appium/webdriver/extensions/clipboard.py index e05e6b86..0c7b33da 100644 --- a/appium/webdriver/extensions/clipboard.py +++ b/appium/webdriver/extensions/clipboard.py @@ -13,25 +13,34 @@ # limitations under the License. import base64 +from typing import TYPE_CHECKING, Optional, TypeVar from selenium import webdriver -from appium.common.helper import appium_bytes from appium.webdriver.clipboard_content_type import ClipboardContentType from ..mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class Clipboard(webdriver.Remote): - def set_clipboard(self, content, content_type=ClipboardContentType.PLAINTEXT, label=None): + def set_clipboard(self, content: bytes, content_type: str = ClipboardContentType.PLAINTEXT, + label: Optional[str] = None) -> T: """Set the content of the system clipboard Args: - content (str): The content to be set as bytearray string + content (bytes): The content to be set as bytearray string content_type (str): One of ClipboardContentType items. Only ClipboardContentType.PLAINTEXT is supported on Android label (:obj:`str`, optional): label argument, which only works for Android + + Returns: + `appium.webdriver.webdriver.WebDriver` """ options = { 'content': base64.b64encode(content).decode('UTF-8'), @@ -40,18 +49,23 @@ def set_clipboard(self, content, content_type=ClipboardContentType.PLAINTEXT, la if label: options['label'] = label self.execute(Command.SET_CLIPBOARD, options) + return self - def set_clipboard_text(self, text, label=None): + def set_clipboard_text(self, text: str, label: Optional[str] = None) -> T: """Copies the given text to the system clipboard Args: text (str): The text to be set - label (:obj:`int`, optional):label argument, which only works for Android + label (:obj:`str`, optional):label argument, which only works for Android + + Returns: + `appium.webdriver.webdriver.WebDriver` """ - self.set_clipboard(appium_bytes(str(text), 'UTF-8'), ClipboardContentType.PLAINTEXT, label) + self.set_clipboard(bytes(str(text), 'UTF-8'), ClipboardContentType.PLAINTEXT, label) + return self - def get_clipboard(self, content_type=ClipboardContentType.PLAINTEXT): + def get_clipboard(self, content_type: str = ClipboardContentType.PLAINTEXT) -> bytes: """Receives the content of the system clipboard Args: @@ -66,7 +80,7 @@ def get_clipboard(self, content_type=ClipboardContentType.PLAINTEXT): })['value'] return base64.b64decode(base64_str) - def get_clipboard_text(self): + def get_clipboard_text(self) -> str: """Receives the text of the system clipboard Return: @@ -76,7 +90,7 @@ def get_clipboard_text(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.SET_CLIPBOARD] = \ ('POST', '/session/$sessionId/appium/device/set_clipboard') self.command_executor._commands[Command.GET_CLIPBOARD] = \ diff --git a/appium/webdriver/extensions/context.py b/appium/webdriver/extensions/context.py index 8581cc3f..9ff5ec62 100644 --- a/appium/webdriver/extensions/context.py +++ b/appium/webdriver/extensions/context.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import List + from selenium import webdriver from ..mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class Context(webdriver.Remote): @property - def contexts(self): + def contexts(self) -> List[str]: """Returns the contexts within the current session. Usage: @@ -32,7 +34,7 @@ def contexts(self): return self.execute(Command.CONTEXTS)['value'] @property - def current_context(self): + def current_context(self) -> str: """Returns the current context of the current session. Usage: @@ -44,7 +46,7 @@ def current_context(self): return self.execute(Command.GET_CURRENT_CONTEXT)['value'] @property - def context(self): + def context(self) -> str: """Returns the current context of the current session. Usage: @@ -57,7 +59,7 @@ def context(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.CONTEXTS] = \ ('GET', '/session/$sessionId/contexts') self.command_executor._commands[Command.GET_CURRENT_CONTEXT] = \ diff --git a/appium/webdriver/extensions/device_time.py b/appium/webdriver/extensions/device_time.py index e40b36d2..db57f19c 100644 --- a/appium/webdriver/extensions/device_time.py +++ b/appium/webdriver/extensions/device_time.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional + from selenium import webdriver from ..mobilecommand import MobileCommand as Command @@ -20,7 +22,7 @@ class DeviceTime(webdriver.Remote): @property - def device_time(self): + def device_time(self) -> str: """Returns the date and time from the device. Return: @@ -28,17 +30,18 @@ def device_time(self): """ return self.execute(Command.GET_DEVICE_TIME_GET, {})['value'] - def get_device_time(self, format=None): + def get_device_time(self, format: Optional[str] = None) -> str: """Returns the date and time from the device. Args: - format (optional): The set of format specifiers. Read https://momentjs.com/docs/ + format (:obj:`str`, optional): The set of format specifiers. Read https://momentjs.com/docs/ to get the full list of supported datetime format specifiers. If unset, return :func:`.device_time` as default format is `YYYY-MM-DDTHH:mm:ssZ`, which complies to ISO-8601 Usage: self.driver.get_device_time() + self.driver.get_device_time("YYYY-MM-DD") Return: @@ -50,7 +53,7 @@ def get_device_time(self, format=None): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_DEVICE_TIME_GET] = \ ('GET', '/session/$sessionId/appium/device/system_time') self.command_executor._commands[Command.GET_DEVICE_TIME_POST] = \ diff --git a/appium/webdriver/extensions/execute_driver.py b/appium/webdriver/extensions/execute_driver.py index 5c866a39..4e96b8fe 100644 --- a/appium/webdriver/extensions/execute_driver.py +++ b/appium/webdriver/extensions/execute_driver.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict, Optional, Union + from selenium import webdriver from ..mobilecommand import MobileCommand as Command @@ -19,19 +21,22 @@ class ExecuteDriver(webdriver.Remote): - def execute_driver(self, script, script_type='webdriverio', timeout_ms=None): + # TODO Inner class case + def execute_driver(self, script: str, script_type: str = 'webdriverio', timeout_ms: Optional[int] = None) -> Any: """Run a set of script against the current session, allowing execution of many commands in one Appium request. Please read http://appium.io/docs/en/commands/session/execute-driver for more details about the acceptable scripts and the output format. Args: - script (string): The string consisting of the script itself - script_type (string): The name of the script type. Defaults to 'webdriverio'. - timeout_ms (optional): The number of `ms` Appium should wait for the script to finish before killing it due to timeout_ms. + script (str): The string consisting of the script itself + script_type (str): The name of the script type. Defaults to 'webdriverio'. + timeout_ms (:obj:`int`, optional): The number of `ms` Appium should wait for the script to finish before killing it due to timeout_ms. Usage: self.driver.execute_driver(script='return [];') + self.driver.execute_driver(script='return [];', script_type='webdriverio') + self.driver.execute_driver(script='return [];', script_type='webdriverio', timeout_ms=10000) Returns: @@ -41,13 +46,13 @@ def execute_driver(self, script, script_type='webdriverio', timeout_ms=None): WebDriverException: If something error happenes in the script. The message has the original error message. """ - class Result(object): + class Result: - def __init__(self, response): + def __init__(self, response: Dict): self.result = response['result'] self.logs = response['logs'] - option = {'script': script, 'type': script_type} + option: Dict[str, Union[str, int]] = {'script': script, 'type': script_type} if timeout_ms is not None: option['timeout'] = timeout_ms @@ -56,6 +61,6 @@ def __init__(self, response): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.EXECUTE_DRIVER] = \ ('POST', '/session/$sessionId/appium/execute_driver') diff --git a/appium/webdriver/extensions/execute_mobile_command.py b/appium/webdriver/extensions/execute_mobile_command.py index 789dcfef..0df53e0f 100644 --- a/appium/webdriver/extensions/execute_mobile_command.py +++ b/appium/webdriver/extensions/execute_mobile_command.py @@ -12,12 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, Any, Dict, TypeVar + from selenium import webdriver +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class ExecuteMobileCommand(webdriver.Remote): - def press_button(self, button_name): + def press_button(self, button_name: str) -> T: """Sends a physical button name to the device to simulate the user pressing. iOS only. @@ -38,7 +45,7 @@ def press_button(self, button_name): return self @property - def battery_info(self): + def battery_info(self) -> Dict[str, Any]: """Retrieves battery information for the device under test. Returns: diff --git a/appium/webdriver/extensions/hw_actions.py b/appium/webdriver/extensions/hw_actions.py index a3d6babb..07643fa6 100644 --- a/appium/webdriver/extensions/hw_actions.py +++ b/appium/webdriver/extensions/hw_actions.py @@ -12,18 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, Any, Optional, TypeVar + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class HardwareActions(webdriver.Remote): - def lock(self, seconds=None): + def lock(self, seconds: Optional[int] = None) -> T: """Lock the device. No changes are made if the device is already unlocked. Args: - seconds (optional): The duration to lock the device, in seconds. + seconds (:obj:`int`, optional): The duration to lock the device, in seconds. The device is going to be locked forever until `unlock` is called if it equals or is less than zero, otherwise this call blocks until the timeout expires and unlocks the screen automatically. @@ -38,7 +45,7 @@ def lock(self, seconds=None): return self - def unlock(self): + def unlock(self) -> T: """Unlock the device. No changes are made if the device is already locked. Returns: @@ -47,7 +54,7 @@ def unlock(self): self.execute(Command.UNLOCK) return self - def is_locked(self): + def is_locked(self) -> bool: """Checks whether the device is locked. Returns: @@ -55,7 +62,7 @@ def is_locked(self): """ return self.execute(Command.IS_LOCKED)['value'] - def shake(self): + def shake(self) -> T: """Shake the device. Returns: @@ -64,7 +71,7 @@ def shake(self): self.execute(Command.SHAKE) return self - def touch_id(self, match): + def touch_id(self, match: bool) -> T: """Simulate touchId on iOS Simulator Args: @@ -79,7 +86,7 @@ def touch_id(self, match): self.execute(Command.TOUCH_ID, data) return self - def toggle_touch_id_enrollment(self): + def toggle_touch_id_enrollment(self) -> T: """Toggle enroll touchId on iOS Simulator Returns: @@ -88,7 +95,7 @@ def toggle_touch_id_enrollment(self): self.execute(Command.TOGGLE_TOUCH_ID_ENROLLMENT) return self - def finger_print(self, finger_id): + def finger_print(self, finger_id: int) -> Any: """Authenticate users by using their finger print scans on supported Android emulators. Args: @@ -101,7 +108,7 @@ def finger_print(self, finger_id): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.LOCK] = \ ('POST', '/session/$sessionId/appium/device/lock') self.command_executor._commands[Command.UNLOCK] = \ diff --git a/appium/webdriver/extensions/images_comparison.py b/appium/webdriver/extensions/images_comparison.py index c0f16a90..77289fe8 100644 --- a/appium/webdriver/extensions/images_comparison.py +++ b/appium/webdriver/extensions/images_comparison.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict, Union + from selenium import webdriver from ..mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class ImagesComparison(webdriver.Remote): - def match_images_features(self, base64_image1, base64_image2, **opts): + def match_images_features(self, base64_image1: bytes, base64_image2: bytes, **opts: Any) -> Dict[str, Any]: """Performs images matching by features. Read @@ -73,7 +75,8 @@ def match_images_features(self, base64_image1, base64_image2, **opts): } return self.execute(Command.COMPARE_IMAGES, options)['value'] - def find_image_occurrence(self, base64_full_image, base64_partial_image, **opts): + def find_image_occurrence(self, base64_full_image: bytes, base64_partial_image: bytes, + **opts: Any) -> Dict[str, Union[bytes, Dict]]: """Performs images matching by template to find possible occurrence of the partial image in the full image. @@ -91,10 +94,11 @@ def find_image_occurrence(self, base64_full_image, base64_partial_image, **opts) False by default Returns: - visualization (bytes): base64-encoded content of PNG visualization of the current comparison - operation. This entry is only present if `visualize` option is enabled - rect (dict): The region of the partial image occurrence on the full image. - The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys + The dictionary containing the following entries: + visualization (bytes): base64-encoded content of PNG visualization of the current comparison + operation. This entry is only present if `visualize` option is enabled + rect (dict): The region of the partial image occurrence on the full image. + The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys """ options = { 'mode': 'matchTemplate', @@ -104,7 +108,8 @@ def find_image_occurrence(self, base64_full_image, base64_partial_image, **opts) } return self.execute(Command.COMPARE_IMAGES, options)['value'] - def get_images_similarity(self, base64_image1, base64_image2, **opts): + def get_images_similarity(self, base64_image1: bytes, base64_image2: bytes, + **opts: Any) -> Dict[str, Union[bytes, Dict]]: """Performs images matching to calculate the similarity score between them. The flow there is similar to the one used in @@ -120,10 +125,11 @@ def get_images_similarity(self, base64_image1, base64_image2, **opts): False by default Returns: - visualization (bytes): base64-encoded content of PNG visualization of the current comparison - operation. This entry is only present if `visualize` option is enabled - score (float): The similarity score as a float number in range [0.0, 1.0]. - 1.0 is the highest score (means both images are totally equal). + The dictionary containing the following entries: + visualization (bytes): base64-encoded content of PNG visualization of the current comparison + operation. This entry is only present if `visualize` option is enabled + score (float): The similarity score as a float number in range [0.0, 1.0]. + 1.0 is the highest score (means both images are totally equal). """ options = { 'mode': 'getSimilarity', @@ -135,6 +141,6 @@ def get_images_similarity(self, base64_image1, base64_image2, **opts): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.COMPARE_IMAGES] = \ ('POST', '/session/$sessionId/appium/compare_images') diff --git a/appium/webdriver/extensions/ime.py b/appium/webdriver/extensions/ime.py index 9c3230e3..046bc538 100644 --- a/appium/webdriver/extensions/ime.py +++ b/appium/webdriver/extensions/ime.py @@ -12,15 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, List, TypeVar + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class IME(webdriver.Remote): @property - def available_ime_engines(self): + def available_ime_engines(self) -> List[str]: """Get the available input methods for an Android device. Package and activity are returned (e.g., ['com.android.inputmethod.latin/.LatinIME']) @@ -31,7 +38,7 @@ def available_ime_engines(self): """ return self.execute(Command.GET_AVAILABLE_IME_ENGINES, {})['value'] - def is_ime_active(self): + def is_ime_active(self) -> bool: """Checks whether the device has IME service active. Android only. @@ -40,7 +47,7 @@ def is_ime_active(self): """ return self.execute(Command.IS_IME_ACTIVE, {})['value'] - def activate_ime_engine(self, engine): + def activate_ime_engine(self, engine: str) -> T: """Activates the given IME engine on the device. Android only. @@ -58,7 +65,7 @@ def activate_ime_engine(self, engine): self.execute(Command.ACTIVATE_IME_ENGINE, data) return self - def deactivate_ime_engine(self): + def deactivate_ime_engine(self) -> T: """Deactivates the currently active IME engine on the device. Android only. @@ -70,7 +77,7 @@ def deactivate_ime_engine(self): return self @property - def active_ime_engine(self): + def active_ime_engine(self) -> str: """Returns the activity and package of the currently active IME engine(e.g., 'com.android.inputmethod.latin/.LatinIME'). Android only. @@ -82,7 +89,7 @@ def active_ime_engine(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_AVAILABLE_IME_ENGINES] = \ ('GET', '/session/$sessionId/ime/available_engines') self.command_executor._commands[Command.IS_IME_ACTIVE] = \ diff --git a/appium/webdriver/extensions/keyboard.py b/appium/webdriver/extensions/keyboard.py index 612a99b6..9142f721 100644 --- a/appium/webdriver/extensions/keyboard.py +++ b/appium/webdriver/extensions/keyboard.py @@ -12,14 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, Dict, Optional, TypeVar + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class Keyboard(webdriver.Remote): - def hide_keyboard(self, key_name=None, key=None, strategy=None): + def hide_keyboard(self, key_name: Optional[str] = None, key: Optional[str] + = None, strategy: Optional[str] = None) -> T: """Hides the software keyboard on the device. In iOS, use `key_name` to press @@ -30,7 +38,7 @@ def hide_keyboard(self, key_name=None, key=None, strategy=None): key (:obj:`str`, optional): strategy (:obj:`str`, optional): strategy for closing the keyboard (e.g., `tapOutside`) """ - data = {} + data: Dict[str, Optional[str]] = {} if key_name is not None: data['keyName'] = key_name elif key is not None: @@ -41,7 +49,7 @@ def hide_keyboard(self, key_name=None, key=None, strategy=None): self.execute(Command.HIDE_KEYBOARD, data) return self - def is_keyboard_shown(self): + def is_keyboard_shown(self) -> bool: """Attempts to detect whether a software keyboard is present Returns: @@ -49,7 +57,7 @@ def is_keyboard_shown(self): """ return self.execute(Command.IS_KEYBOARD_SHOWN)['value'] - def keyevent(self, keycode, metastate=None): + def keyevent(self, keycode: int, metastate: Optional[int] = None) -> T: """Sends a keycode to the device. Android only. @@ -70,7 +78,7 @@ def keyevent(self, keycode, metastate=None): self.execute(Command.KEY_EVENT, data) return self - def press_keycode(self, keycode, metastate=None, flags=None): + def press_keycode(self, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> T: """Sends a keycode to the device. Android only. Possible keycodes can be found in http://developer.android.com/reference/android/view/KeyEvent.html. @@ -93,7 +101,7 @@ def press_keycode(self, keycode, metastate=None, flags=None): self.execute(Command.PRESS_KEYCODE, data) return self - def long_press_keycode(self, keycode, metastate=None, flags=None): + def long_press_keycode(self, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> T: """Sends a long press of keycode to the device. Android only. Possible keycodes can be found in http://developer.android.com/reference/android/view/KeyEvent.html. @@ -118,7 +126,7 @@ def long_press_keycode(self, keycode, metastate=None, flags=None): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.HIDE_KEYBOARD] = \ ('POST', '/session/$sessionId/appium/device/hide_keyboard') self.command_executor._commands[Command.IS_KEYBOARD_SHOWN] = \ diff --git a/appium/webdriver/extensions/location.py b/appium/webdriver/extensions/location.py index 7dffcb0e..744035af 100644 --- a/appium/webdriver/extensions/location.py +++ b/appium/webdriver/extensions/location.py @@ -12,13 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, Dict, TypeVar, Union + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class Location(webdriver.Remote): - def toggle_location_services(self): + def toggle_location_services(self) -> T: """Toggle the location services on the device. Android only. @@ -29,7 +36,10 @@ def toggle_location_services(self): self.execute(Command.TOGGLE_LOCATION_SERVICES, {}) return self - def set_location(self, latitude, longitude, altitude=None): + def set_location(self, + latitude: Union[float, str], + longitude: Union[float, str], + altitude: Union[float, str] = None) -> T: """Set the location of the device Args: @@ -52,7 +62,7 @@ def set_location(self, latitude, longitude, altitude=None): return self @property - def location(self): + def location(self) -> Dict[str, float]: """Retrieves the current location Returns: @@ -65,7 +75,7 @@ def location(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.TOGGLE_LOCATION_SERVICES] = \ ('POST', '/session/$sessionId/appium/device/toggle_location_services') self.command_executor._commands[Command.GET_LOCATION] = \ diff --git a/appium/webdriver/extensions/log_event.py b/appium/webdriver/extensions/log_event.py index 383664d6..7e62672b 100644 --- a/appium/webdriver/extensions/log_event.py +++ b/appium/webdriver/extensions/log_event.py @@ -12,14 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, Dict, List, TypeVar, Union + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class LogEvent(webdriver.Remote): - def get_events(self, type=None): + def get_events(self, type: List[str] = None) -> Dict[str, Union[str, int]]: """ Retrieves events information from the current session (Since Appium 1.16.0) @@ -28,6 +35,7 @@ def get_events(self, type=None): Usage: events = driver.get_events() + events = driver.get_events(['appium:funEvent']) Returns: @@ -35,14 +43,14 @@ def get_events(self, type=None): commands: (`list` of `dict`) List of dictionaries containing the following entries cmd: (str) The command name that has been sent to the appium server startTime: (int) Received time - endTime: (init) Response time + endTime: (int) Response time """ data = {} if type is not None: data['type'] = type return self.execute(Command.GET_EVENTS, data)['value'] - def log_event(self, vendor, event): + def log_event(self, vendor: str, event: str) -> T: """Log a custom event on the Appium server. (Since Appium 1.16.0) @@ -65,7 +73,7 @@ def log_event(self, vendor, event): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_EVENTS] = \ ('POST', '/session/$sessionId/appium/events') self.command_executor._commands[Command.LOG_EVENT] = \ diff --git a/appium/webdriver/extensions/remote_fs.py b/appium/webdriver/extensions/remote_fs.py index 10fbff23..62b3cfcb 100644 --- a/appium/webdriver/extensions/remote_fs.py +++ b/appium/webdriver/extensions/remote_fs.py @@ -13,50 +13,57 @@ # limitations under the License. import base64 +from typing import TYPE_CHECKING, Optional, TypeVar from selenium import webdriver from selenium.common.exceptions import InvalidArgumentException from ..mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class RemoteFS(webdriver.Remote): - def pull_file(self, path): + def pull_file(self, path: str) -> str: """Retrieves the file at `path`. Args: path (str): the path to the file on the device Returns: - bytes: The file's contents as base64. + str: The file's contents encoded as Base64. """ data = { 'path': path, } return self.execute(Command.PULL_FILE, data)['value'] - def pull_folder(self, path): + def pull_folder(self, path: str) -> str: """Retrieves a folder at `path`. Args: path (str): the path to the folder on the device Returns: - bytes: The folder's contents zipped and encoded as Base64. + str: The folder's contents zipped and encoded as Base64. """ data = { 'path': path, } return self.execute(Command.PULL_FOLDER, data)['value'] - def push_file(self, destination_path, base64data=None, source_path=None): + def push_file(self, destination_path: str, + base64data: Optional[str] = None, source_path: Optional[str] = None) -> T: """Puts the data from the file at `source_path`, encoded as Base64, in the file specified as `path`. Specify either `base64data` or `source_path`, if both specified default to `source_path` Args: destination_path (str): the location on the device/simulator where the local file contents should be saved - base64data (:obj:`bytes`, optional): file contents, encoded as Base64, to be written to the file on the device/simulator + base64data (:obj:`str`, optional): file contents, encoded as Base64, to be written to the file on the device/simulator source_path (:obj:`str`, optional): local file path for the file to be loaded on device Returns: @@ -68,11 +75,11 @@ def push_file(self, destination_path, base64data=None, source_path=None): if source_path is not None: try: with open(source_path, 'rb') as f: - data = f.read() - except IOError: - message = 'source_path {} could not be found. Are you sure the file exists?'.format(source_path) - raise InvalidArgumentException(message) - base64data = base64.b64encode(data).decode('utf-8') + file_data = f.read() + except IOError as e: + message = f'source_path "{source_path}" could not be found. Are you sure the file exists?' + raise InvalidArgumentException(message) from e + base64data = base64.b64encode(file_data).decode('utf-8') data = { 'path': destination_path, @@ -83,7 +90,7 @@ def push_file(self, destination_path, base64data=None, source_path=None): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.PULL_FILE] = \ ('POST', '/session/$sessionId/appium/device/pull_file') self.command_executor._commands[Command.PULL_FOLDER] = \ diff --git a/appium/webdriver/extensions/screen_record.py b/appium/webdriver/extensions/screen_record.py index 0b7f1bc7..8d2abbf0 100644 --- a/appium/webdriver/extensions/screen_record.py +++ b/appium/webdriver/extensions/screen_record.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Union + from selenium import webdriver from ..mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class ScreenRecord(webdriver.Remote): - def start_recording_screen(self, **options): + def start_recording_screen(self, **options: Any) -> Union[bytes, str]: """Start asynchronous screen recording process. Keyword Args: @@ -82,7 +84,7 @@ def start_recording_screen(self, **options): del options['password'] return self.execute(Command.START_RECORDING_SCREEN, {'options': options})['value'] - def stop_recording_screen(self, **options): + def stop_recording_screen(self, **options: Any) -> bytes: """Gather the output from the previously started screen recording to a media file. Keyword Args: @@ -112,7 +114,7 @@ def stop_recording_screen(self, **options): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.START_RECORDING_SCREEN] = \ ('POST', '/session/$sessionId/appium/start_recording_screen') self.command_executor._commands[Command.STOP_RECORDING_SCREEN] = \ diff --git a/appium/webdriver/extensions/search_context/android.py b/appium/webdriver/extensions/search_context/android.py index 942675c7..a4f3a958 100644 --- a/appium/webdriver/extensions/search_context/android.py +++ b/appium/webdriver/extensions/search_context/android.py @@ -15,16 +15,21 @@ # pylint: disable=abstract-method import json +from typing import TYPE_CHECKING, Any, List, Optional from appium.webdriver.common.mobileby import MobileBy from .base_search_context import BaseSearchContext +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class AndroidSearchContext(BaseSearchContext): """Define search context for Android""" - def find_element_by_android_view_matcher(self, name=None, args=None, className=None): + def find_element_by_android_view_matcher( + self, name: Optional[str] = None, args: Optional[Any] = None, className: Optional[str] = None) -> 'WebElement': """Finds element by [onView](https://developer.android.com/training/testing/espresso/basics) in Android It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). @@ -33,7 +38,7 @@ def find_element_by_android_view_matcher(self, name=None, args=None, className=N name (:obj:`str`, optional): The name of a method to invoke. The method must return a Hamcrest [Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html) - args (:obj:`str`, optional): The args provided to the method + args (:obj:`Any`, optional): The args provided to the method className (:obj:`str`, optional): The class name that the method is part of (defaults to `org.hamcrest.Matchers`). Can be fully qualified by having the androidx.test.espresso.matcher. prefix. If the prefix is not provided then it is going to be added implicitly. @@ -57,7 +62,8 @@ def find_element_by_android_view_matcher(self, name=None, args=None, className=N value=self._build_data_matcher(name=name, args=args, className=className) ) - def find_element_by_android_data_matcher(self, name=None, args=None, className=None): + def find_element_by_android_data_matcher( + self, name: Optional[str] = None, args: Optional[Any] = None, className: Optional[str] = None) -> 'WebElement': """Finds element by [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). @@ -66,7 +72,7 @@ def find_element_by_android_data_matcher(self, name=None, args=None, className=N name (:obj:`str`, optional): The name of a method to invoke. The method must return a Hamcrest [Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html) - args (:obj:`str`, optional): The args provided to the method + args (:obj:`Any`, optional): The args provided to the method className (:obj:`str`, optional): The class name that the method is part of (defaults to `org.hamcrest.Matchers`). Can be fully qualified, or simple, and simple defaults to `androidx.test.espresso.matcher` package (e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers` @@ -89,16 +95,16 @@ def find_element_by_android_data_matcher(self, name=None, args=None, className=N value=self._build_data_matcher(name=name, args=args, className=className) ) - def find_elements_by_android_data_matcher(self, name=None, args=None, className=None): + def find_elements_by_android_data_matcher( + self, name: Optional[str] = None, args: Optional[Any] = None, className: Optional[str] = None) -> List['WebElement']: """Finds elements by [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android - It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). Args: name (:obj:`str`, optional): The name of a method to invoke. The method must return a Hamcrest [Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html) - args (:obj:`str`, optional): The args provided to the method + args (:obj:`Any`, optional): The args provided to the method className (:obj:`str`, optional): The class name that the method is part of (defaults to `org.hamcrest.Matchers`). Can be fully qualified, or simple, and simple defaults to `androidx.test.espresso.matcher` package (e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers` @@ -117,7 +123,8 @@ def find_elements_by_android_data_matcher(self, name=None, args=None, className= value=self._build_data_matcher(name=name, args=args, className=className) ) - def _build_data_matcher(self, name=None, args=None, className=None): + def _build_data_matcher(self, name: Optional[str] = None, args: Optional[Any] + = None, className: Optional[str] = None) -> str: result = {} for key, value in {'name': name, 'args': args, 'class': className}.items(): @@ -126,7 +133,7 @@ def _build_data_matcher(self, name=None, args=None, className=None): return json.dumps(result) - def find_element_by_android_uiautomator(self, uia_string): + def find_element_by_android_uiautomator(self, uia_string: str) -> 'WebElement': """Finds element by uiautomator in Android. Args: @@ -142,7 +149,7 @@ def find_element_by_android_uiautomator(self, uia_string): """ return self.find_element(by=MobileBy.ANDROID_UIAUTOMATOR, value=uia_string) - def find_elements_by_android_uiautomator(self, uia_string): + def find_elements_by_android_uiautomator(self, uia_string: str) -> List['WebElement']: """Finds elements by uiautomator in Android. Args: @@ -158,7 +165,7 @@ def find_elements_by_android_uiautomator(self, uia_string): """ return self.find_elements(by=MobileBy.ANDROID_UIAUTOMATOR, value=uia_string) - def find_element_by_android_viewtag(self, tag): + def find_element_by_android_viewtag(self, tag: str) -> 'WebElement': """Finds element by [View#tags](https://developer.android.com/reference/android/view/View#tags) in Android. It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). @@ -176,7 +183,7 @@ def find_element_by_android_viewtag(self, tag): """ return self.find_element(by=MobileBy.ANDROID_VIEWTAG, value=tag) - def find_elements_by_android_viewtag(self, tag): + def find_elements_by_android_viewtag(self, tag: str) -> List['WebElement']: """Finds element by [View#tags](https://developer.android.com/reference/android/view/View#tags) in Android. It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). diff --git a/appium/webdriver/extensions/search_context/base_search_context.py b/appium/webdriver/extensions/search_context/base_search_context.py index 500a6e3a..317069f0 100644 --- a/appium/webdriver/extensions/search_context/base_search_context.py +++ b/appium/webdriver/extensions/search_context/base_search_context.py @@ -14,12 +14,17 @@ # pylint: disable=abstract-method +from typing import TYPE_CHECKING, Dict, List, Union -class BaseSearchContext(object): +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + + +class BaseSearchContext: """Used by each search context. Dummy find_element/s are for preventing pylint error""" - def find_element(self, by=None, value=None): + def find_element(self, by: str, value: Union[str, Dict] = None) -> 'WebElement': raise NotImplementedError - def find_elements(self, by=None, value=None): + def find_elements(self, by: str, value: Union[str, Dict] = None) -> List['WebElement']: raise NotImplementedError diff --git a/appium/webdriver/extensions/search_context/custom.py b/appium/webdriver/extensions/search_context/custom.py index dabcc6fd..a790016b 100644 --- a/appium/webdriver/extensions/search_context/custom.py +++ b/appium/webdriver/extensions/search_context/custom.py @@ -14,15 +14,20 @@ # pylint: disable=abstract-method +from typing import TYPE_CHECKING, List + from appium.webdriver.common.mobileby import MobileBy from .base_search_context import BaseSearchContext +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class CustomSearchContext(BaseSearchContext): """Define search context for custom plugin""" - def find_element_by_custom(self, selector): + def find_element_by_custom(self, selector: str) -> 'WebElement': """Finds an element in conjunction with a custom element finding plugin Args: @@ -42,7 +47,7 @@ def find_element_by_custom(self, selector): """ return self.find_element(by=MobileBy.CUSTOM, value=selector) - def find_elements_by_custom(self, selector): + def find_elements_by_custom(self, selector: str) -> List['WebElement']: """Finds elements in conjunction with a custom element finding plugin Args: diff --git a/appium/webdriver/extensions/search_context/ios.py b/appium/webdriver/extensions/search_context/ios.py index ad42ce91..3066ad9e 100644 --- a/appium/webdriver/extensions/search_context/ios.py +++ b/appium/webdriver/extensions/search_context/ios.py @@ -14,15 +14,20 @@ # pylint: disable=abstract-method +from typing import TYPE_CHECKING, List + from appium.webdriver.common.mobileby import MobileBy from .base_search_context import BaseSearchContext +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class iOSSearchContext(BaseSearchContext): """Define search context for iOS""" - def find_element_by_ios_uiautomation(self, uia_string): + def find_element_by_ios_uiautomation(self, uia_string: str) -> 'WebElement': """Finds an element by uiautomation in iOS. Args: @@ -39,7 +44,7 @@ def find_element_by_ios_uiautomation(self, uia_string): """ return self.find_element(by=MobileBy.IOS_UIAUTOMATION, value=uia_string) - def find_elements_by_ios_uiautomation(self, uia_string): + def find_elements_by_ios_uiautomation(self, uia_string: str) -> List['WebElement']: """Finds elements by uiautomation in iOS. Args: @@ -55,7 +60,7 @@ def find_elements_by_ios_uiautomation(self, uia_string): """ return self.find_elements(by=MobileBy.IOS_UIAUTOMATION, value=uia_string) - def find_element_by_ios_predicate(self, predicate_string): + def find_element_by_ios_predicate(self, predicate_string: str) -> 'WebElement': """Find an element by ios predicate string. Args: @@ -71,7 +76,7 @@ def find_element_by_ios_predicate(self, predicate_string): """ return self.find_element(by=MobileBy.IOS_PREDICATE, value=predicate_string) - def find_elements_by_ios_predicate(self, predicate_string): + def find_elements_by_ios_predicate(self, predicate_string: str) -> List['WebElement']: """Finds elements by ios predicate string. Args: @@ -87,7 +92,7 @@ def find_elements_by_ios_predicate(self, predicate_string): """ return self.find_elements(by=MobileBy.IOS_PREDICATE, value=predicate_string) - def find_element_by_ios_class_chain(self, class_chain_string): + def find_element_by_ios_class_chain(self, class_chain_string: str) -> 'WebElement': """Find an element by ios class chain string. Args: @@ -103,7 +108,7 @@ def find_element_by_ios_class_chain(self, class_chain_string): """ return self.find_element(by=MobileBy.IOS_CLASS_CHAIN, value=class_chain_string) - def find_elements_by_ios_class_chain(self, class_chain_string): + def find_elements_by_ios_class_chain(self, class_chain_string: str) -> List['WebElement']: """Finds elements by ios class chain string. Args: diff --git a/appium/webdriver/extensions/search_context/mobile.py b/appium/webdriver/extensions/search_context/mobile.py index c9d68632..a58f4d23 100644 --- a/appium/webdriver/extensions/search_context/mobile.py +++ b/appium/webdriver/extensions/search_context/mobile.py @@ -15,16 +15,20 @@ # pylint: disable=abstract-method import base64 +from typing import TYPE_CHECKING, List from appium.webdriver.common.mobileby import MobileBy from .base_search_context import BaseSearchContext +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class MobileSearchContext(BaseSearchContext): """Define search context for Mobile(Android, iOS)""" - def find_element_by_accessibility_id(self, accessibility_id): + def find_element_by_accessibility_id(self, accessibility_id: str) -> 'WebElement': """Finds an element by accessibility id. Args: @@ -42,7 +46,7 @@ def find_element_by_accessibility_id(self, accessibility_id): """ return self.find_element(by=MobileBy.ACCESSIBILITY_ID, value=accessibility_id) - def find_elements_by_accessibility_id(self, accessibility_id): + def find_elements_by_accessibility_id(self, accessibility_id: str) -> List['WebElement']: """Finds elements by accessibility id. Args: @@ -59,7 +63,7 @@ def find_elements_by_accessibility_id(self, accessibility_id): """ return self.find_elements(by=MobileBy.ACCESSIBILITY_ID, value=accessibility_id) - def find_element_by_image(self, img_path): + def find_element_by_image(self, img_path: str) -> 'WebElement': """Finds a portion of a screenshot by an image. Uses driver.find_image_occurrence under the hood. @@ -77,7 +81,7 @@ def find_element_by_image(self, img_path): return self.find_element(by=MobileBy.IMAGE, value=b64_data) - def find_elements_by_image(self, img_path): + def find_elements_by_image(self, img_path: str) -> List['WebElement']: """Finds a portion of a screenshot by an image. Uses driver.find_image_occurrence under the hood. Note that this will diff --git a/appium/webdriver/extensions/search_context/windows.py b/appium/webdriver/extensions/search_context/windows.py index 66ca23a2..692aebce 100644 --- a/appium/webdriver/extensions/search_context/windows.py +++ b/appium/webdriver/extensions/search_context/windows.py @@ -14,15 +14,20 @@ # pylint: disable=abstract-method +from typing import TYPE_CHECKING, List + from appium.webdriver.common.mobileby import MobileBy from .base_search_context import BaseSearchContext +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class WindowsSearchContext(BaseSearchContext): """Define search context for Windows""" - def find_element_by_windows_uiautomation(self, win_uiautomation): + def find_element_by_windows_uiautomation(self, win_uiautomation: str) -> 'WebElement': """Finds an element by windows uiautomation Args: @@ -39,7 +44,7 @@ def find_element_by_windows_uiautomation(self, win_uiautomation): """ return self.find_element(by=MobileBy.WINDOWS_UI_AUTOMATION, value=win_uiautomation) - def find_elements_by_windows_uiautomation(self, win_uiautomation): + def find_elements_by_windows_uiautomation(self, win_uiautomation: str) -> List['WebElement']: """Finds elements by windows uiautomation Args: diff --git a/appium/webdriver/extensions/session.py b/appium/webdriver/extensions/session.py index 64d49370..a8c097a6 100644 --- a/appium/webdriver/extensions/session.py +++ b/appium/webdriver/extensions/session.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict, List + from selenium import webdriver from appium.common.logger import logger @@ -21,7 +23,7 @@ class Session(webdriver.Remote): @property - def session(self): + def session(self) -> Dict[str, Any]: """ Retrieves session information from the current session Usage: @@ -33,19 +35,19 @@ def session(self): return self.execute(Command.GET_SESSION)['value'] @property - def all_sessions(self): + def all_sessions(self) -> List[Dict[str, Any]]: """ Retrieves all sessions that are open Usage: sessions = driver.all_sessions Returns: - `dict`: containing all open sessions + :obj:`list` of :obj:`dict`: containing all open sessions """ return self.execute(Command.GET_ALL_SESSIONS)['value'] @property - def events(self): + def events(self) -> Dict: """ Retrieves events information from the current session Usage: @@ -63,7 +65,7 @@ def events(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_SESSION] = \ ('GET', '/session/$sessionId') self.command_executor._commands[Command.GET_ALL_SESSIONS] = \ diff --git a/appium/webdriver/extensions/settings.py b/appium/webdriver/extensions/settings.py index 2a0f4235..d3501c19 100644 --- a/appium/webdriver/extensions/settings.py +++ b/appium/webdriver/extensions/settings.py @@ -12,13 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TYPE_CHECKING, Any, Dict, TypeVar + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='WebDriver') + class Settings(webdriver.Remote): - def get_settings(self): + def get_settings(self) -> Dict[str, Any]: """Returns the appium server Settings for the current session. Do not get Settings confused with Desired Capabilities, they are @@ -29,7 +36,7 @@ def get_settings(self): """ return self.execute(Command.GET_SETTINGS, {})['value'] - def update_settings(self, settings): + def update_settings(self, settings: Dict[str, Any]) -> T: """Set settings for the current session. For more on settings, see: https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md @@ -44,7 +51,7 @@ def update_settings(self, settings): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_SETTINGS] = \ ('GET', '/session/$sessionId/appium/settings') self.command_executor._commands[Command.UPDATE_SETTINGS] = \ diff --git a/appium/webdriver/mobilecommand.py b/appium/webdriver/mobilecommand.py index f6d90679..b0bd8c4d 100644 --- a/appium/webdriver/mobilecommand.py +++ b/appium/webdriver/mobilecommand.py @@ -13,7 +13,7 @@ # limitations under the License. -class MobileCommand(object): +class MobileCommand: # Common GET_SESSION = 'getSession' GET_ALL_SESSIONS = 'getAllSessions' diff --git a/appium/webdriver/py.typed b/appium/webdriver/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/appium/webdriver/switch_to.py b/appium/webdriver/switch_to.py index d496b7a3..e0266510 100644 --- a/appium/webdriver/switch_to.py +++ b/appium/webdriver/switch_to.py @@ -12,13 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TypeVar + from selenium.webdriver.remote.switch_to import SwitchTo from .mobilecommand import MobileCommand +T = TypeVar('T', bound='MobileSwitchTo') + class MobileSwitchTo(SwitchTo): - def context(self, context_name): + def context(self, context_name: str) -> T: """Sets the context for the current session. Args: @@ -28,3 +32,4 @@ def context(self, context_name): driver.switch_to.context('WEBVIEW_1') """ self._driver.execute(MobileCommand.SWITCH_TO_CONTEXT, {'name': context_name}) + return self diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 03e7e3fd..35cf0cad 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -15,6 +15,7 @@ # pylint: disable=too-many-lines,too-many-public-methods,too-many-statements,no-self-use import copy +from typing import Any, Dict, List, Optional, TypeVar, Union from selenium.common.exceptions import InvalidArgumentException from selenium.webdriver.common.by import By @@ -84,7 +85,7 @@ # Add appium prefix for the non-W3C capabilities -def _make_w3c_caps(caps): +def _make_w3c_caps(caps: Dict) -> Dict[str, List[Dict[str, Any]]]: appium_prefix = 'appium:' caps = copy.deepcopy(caps) @@ -111,6 +112,9 @@ def _make_w3c_caps(caps): return {'firstMatch': [first_match]} +T = TypeVar('T', bound='WebDriver') + + class WebDriver( AppiumSearchContext, ActionHelpers, @@ -141,10 +145,10 @@ class WebDriver( SystemBars ): - def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', - desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=True, direct_connection=False): + def __init__(self, command_executor: str = 'http://127.0.0.1:4444/wd/hub', + desired_capabilities: Optional[Dict] = None, browser_profile: str = None, proxy: str = None, keep_alive: bool = True, direct_connection: bool = False): - super(WebDriver, self).__init__( + super().__init__( AppiumConnection(command_executor, keep_alive=keep_alive), desired_capabilities, browser_profile, @@ -171,7 +175,7 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', By.IMAGE = MobileBy.IMAGE By.CUSTOM = MobileBy.CUSTOM - def _update_command_executor(self, keep_alive): + def _update_command_executor(self, keep_alive: bool) -> None: """Update command executor following directConnect feature""" direct_protocol = 'directConnectProtocol' direct_host = 'directConnectHost' @@ -189,19 +193,14 @@ def _update_command_executor(self, keep_alive): hostname = self.capabilities[direct_host] port = self.capabilities[direct_port] path = self.capabilities[direct_path] - executor = '{scheme}://{hostname}:{port}{path}'.format( - scheme=protocol, - hostname=hostname, - port=port, - path=path - ) + executor = f'{protocol}://{hostname}:{port}{path}' logger.info('Updated request endpoint to %s', executor) # Override command executor self.command_executor = RemoteConnection(executor, keep_alive=keep_alive) self._addCommands() - def start_session(self, capabilities, browser_profile=None): + def start_session(self, capabilities: Dict, browser_profile: Optional[str] = None) -> None: """Creates a new session with the desired capabilities. Override for Appium @@ -218,9 +217,11 @@ def start_session(self, capabilities, browser_profile=None): raise InvalidArgumentException('Capabilities must be a dictionary') if browser_profile: if 'moz:firefoxOptions' in capabilities: - capabilities['moz:firefoxOptions']['profile'] = browser_profile.encoded + # encoded is defined in selenium's original codes + capabilities['moz:firefoxOptions']['profile'] = browser_profile.encoded # type: ignore else: - capabilities.update({'firefox_profile': browser_profile.encoded}) + # encoded is defined in selenium's original codes + capabilities.update({'firefox_profile': browser_profile.encoded}) # type: ignore parameters = self._merge_capabilities(capabilities) @@ -239,7 +240,7 @@ def start_session(self, capabilities, browser_profile=None): self.w3c = response.get('status') is None self.command_executor.w3c = self.w3c - def _merge_capabilities(self, capabilities): + def _merge_capabilities(self, capabilities: Dict) -> Dict[str, Any]: """Manage capabilities whether W3C format or MJSONWP format """ if _FORCE_MJSONWP in capabilities: @@ -252,7 +253,7 @@ def _merge_capabilities(self, capabilities): w3c_caps = _make_w3c_caps(capabilities) return {'capabilities': w3c_caps, 'desiredCapabilities': capabilities} - def find_element(self, by=By.ID, value=None): + def find_element(self, by: str = By.ID, value: Union[str, Dict] = None) -> MobileWebElement: """'Private' method used by the find_element_by_* methods. Override for Appium @@ -283,7 +284,8 @@ def find_element(self, by=By.ID, value=None): 'using': by, 'value': value})['value'] - def find_elements(self, by=By.ID, value=None): + def find_elements(self, by: str = By.ID, value: Union[str, Dict] + = None) -> Union[List[MobileWebElement], List]: """'Private' method used by the find_elements_by_* methods. Override for Appium @@ -317,7 +319,7 @@ def find_elements(self, by=By.ID, value=None): 'using': by, 'value': value})['value'] or [] - def create_web_element(self, element_id, w3c=False): + def create_web_element(self, element_id: Union[int, str], w3c: bool = False) -> MobileWebElement: """Creates a web element with the specified element_id. Overrides method in Selenium WebDriver in order to always give them @@ -332,7 +334,7 @@ def create_web_element(self, element_id, w3c=False): """ return MobileWebElement(self, element_id, w3c) - def set_value(self, element, value): + def set_value(self, element: MobileWebElement, value: str) -> T: """Set the value on an element in the application. Args: @@ -351,7 +353,7 @@ def set_value(self, element, value): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: # call the overridden command binders from all mixin classes except for # appium.webdriver.webdriver.WebDriver and its sub-classes # https://github.com/appium/python-client/issues/342 diff --git a/appium/webdriver/webelement.py b/appium/webdriver/webelement.py index a90080c4..42af922b 100644 --- a/appium/webdriver/webelement.py +++ b/appium/webdriver/webelement.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, List, Optional, TypeVar, Union + from selenium.webdriver.common.by import By from selenium.webdriver.common.utils import keys_to_typing from selenium.webdriver.remote.command import Command as RemoteCommand @@ -19,15 +21,11 @@ from .extensions.search_context import AppiumWebElementSearchContext from .mobilecommand import MobileCommand as Command -# Python 3 imports -try: - str = basestring -except NameError: - pass +T = TypeVar('T', bound='WebElement') class WebElement(AppiumWebElementSearchContext): - def get_attribute(self, name): + def get_attribute(self, name: str) -> Optional[Union[str, Dict]]: """Gets the given attribute or property of the element. Override for Appium @@ -62,25 +60,23 @@ def get_attribute(self, name): if isinstance(attributeValue, dict): return attributeValue + # Convert to str along to the spec if not isinstance(attributeValue, str): - try: - attributeValue = unicode(attributeValue) - except NameError: - attributeValue = str(attributeValue) + attributeValue = str(attributeValue) if name != 'value' and attributeValue.lower() in ('true', 'false'): return attributeValue.lower() return attributeValue - def is_displayed(self): + def is_displayed(self) -> bool: """Whether the element is visible to a user. Override for Appium """ return self._execute(RemoteCommand.IS_ELEMENT_DISPLAYED)['value'] - def find_element(self, by=By.ID, value=None): + def find_element(self, by: str = By.ID, value: Union[str, Dict] = None) -> T: """Find an element given a By strategy and locator Override for Appium @@ -114,7 +110,7 @@ def find_element(self, by=By.ID, value=None): return self._execute(RemoteCommand.FIND_CHILD_ELEMENT, {"using": by, "value": value})['value'] - def find_elements(self, by=By.ID, value=None): + def find_elements(self, by: str = By.ID, value: Union[str, Dict] = None) -> List[T]: """Find elements given a By strategy and locator Override for Appium @@ -148,7 +144,7 @@ def find_elements(self, by=By.ID, value=None): return self._execute(RemoteCommand.FIND_CHILD_ELEMENTS, {"using": by, "value": value})['value'] - def clear(self): + def clear(self) -> T: """Clears text. Override for Appium @@ -160,7 +156,7 @@ def clear(self): self._execute(Command.CLEAR, data) return self - def set_text(self, keys=''): + def set_text(self, keys: str = '') -> T: """Sends text to the element. Previous text is removed. @@ -183,12 +179,14 @@ def set_text(self, keys=''): return self @property - def location_in_view(self): + def location_in_view(self) -> Dict[str, int]: """Gets the location of an element relative to the view. Usage: location = element.location_in_view + x = location['x'] + y = location['y'] Returns: @@ -196,7 +194,7 @@ def location_in_view(self): """ return self._execute(Command.LOCATION_IN_VIEW)['value'] - def set_value(self, value): + def set_value(self, value: str) -> T: """Set the value on this element in the application Args: @@ -213,7 +211,7 @@ def set_value(self, value): return self # Override - def send_keys(self, *value): + def send_keys(self, *value: str) -> T: """Simulates typing into the element. Args: diff --git a/ci.sh b/ci.sh index 0fb53862..1303ff84 100755 --- a/ci.sh +++ b/ci.sh @@ -34,4 +34,11 @@ if [[ $? -ne 0 ]] ; then EXIT_STATUS=1 fi +( + python -m mypy appium test +) +if [[ $? -ne 0 ]] ; then + EXIT_STATUS=1 +fi + exit $EXIT_STATUS diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..9e0f7e68 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +### How to generate doc + +```bash +$cd python-client/docs +$pip install -r requirements.txt +$bash generate.sh +``` + +### How to check generated doc + +```bash +$cd python-client/docs +$bash generate.sh +$cd python-client/docs/_build/html +$python -m http.server 1234 +``` + +Access to http://localhost:1234 on web browser + + +### How to deploy generated doc +See https://github.com/ki4070ma/python-client-sphinx#how-to-deploy for now diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..9c323427 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,59 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +sys.path.insert(0, os.path.abspath('../appium')) + + +# -- Project information ----------------------------------------------------- + +project = 'Python client 1.0' +copyright = '2020, Appium' +author = 'Appium' + +# The full version, including alpha/beta/rc tags +release = '1.0' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.githubpages' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/docs/generate.sh b/docs/generate.sh new file mode 100644 index 00000000..219b7325 --- /dev/null +++ b/docs/generate.sh @@ -0,0 +1,4 @@ +#!/bin/sh +rm -rf *rst _build +sphinx-apidoc -F -H 'Appium python client' -o . ../appium/webdriver +make html diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..b5db08cc --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,21 @@ +.. Appium python client documentation master file, created by + sphinx-quickstart on Sun Mar 1 17:05:15 2020. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Appium python client's documentation! +================================================ + +.. toctree:: + :maxdepth: 4 + :caption: Contents: + + webdriver + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..6247f7e2 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..d108218f --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +sphinx >= 3.0, <4.0 \ No newline at end of file diff --git a/docs/webdriver.common.rst b/docs/webdriver.common.rst new file mode 100644 index 00000000..1da461fb --- /dev/null +++ b/docs/webdriver.common.rst @@ -0,0 +1,38 @@ +webdriver.common package +======================== + +Submodules +---------- + +webdriver.common.mobileby module +-------------------------------- + +.. automodule:: webdriver.common.mobileby + :members: + :undoc-members: + :show-inheritance: + +webdriver.common.multi\_action module +------------------------------------- + +.. automodule:: webdriver.common.multi_action + :members: + :undoc-members: + :show-inheritance: + +webdriver.common.touch\_action module +------------------------------------- + +.. automodule:: webdriver.common.touch_action + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: webdriver.common + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/webdriver.extensions.android.rst b/docs/webdriver.extensions.android.rst new file mode 100644 index 00000000..55a32a1f --- /dev/null +++ b/docs/webdriver.extensions.android.rst @@ -0,0 +1,94 @@ +webdriver.extensions.android package +==================================== + +Submodules +---------- + +webdriver.extensions.android.activities module +---------------------------------------------- + +.. automodule:: webdriver.extensions.android.activities + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.android.common module +------------------------------------------ + +.. automodule:: webdriver.extensions.android.common + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.android.display module +------------------------------------------- + +.. automodule:: webdriver.extensions.android.display + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.android.gsm module +--------------------------------------- + +.. automodule:: webdriver.extensions.android.gsm + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.android.nativekey module +--------------------------------------------- + +.. automodule:: webdriver.extensions.android.nativekey + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.android.network module +------------------------------------------- + +.. automodule:: webdriver.extensions.android.network + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.android.performance module +----------------------------------------------- + +.. automodule:: webdriver.extensions.android.performance + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.android.power module +----------------------------------------- + +.. automodule:: webdriver.extensions.android.power + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.android.sms module +--------------------------------------- + +.. automodule:: webdriver.extensions.android.sms + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.android.system\_bars module +------------------------------------------------ + +.. automodule:: webdriver.extensions.android.system_bars + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: webdriver.extensions.android + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/webdriver.extensions.rst b/docs/webdriver.extensions.rst new file mode 100644 index 00000000..53002b3a --- /dev/null +++ b/docs/webdriver.extensions.rst @@ -0,0 +1,158 @@ +webdriver.extensions package +============================ + +Subpackages +----------- + +.. toctree:: + + webdriver.extensions.android + webdriver.extensions.search_context + +Submodules +---------- + +webdriver.extensions.action\_helpers module +------------------------------------------- + +.. automodule:: webdriver.extensions.action_helpers + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.applications module +---------------------------------------- + +.. automodule:: webdriver.extensions.applications + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.clipboard module +------------------------------------- + +.. automodule:: webdriver.extensions.clipboard + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.context module +----------------------------------- + +.. automodule:: webdriver.extensions.context + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.device\_time module +---------------------------------------- + +.. automodule:: webdriver.extensions.device_time + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.execute\_driver module +------------------------------------------- + +.. automodule:: webdriver.extensions.execute_driver + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.execute\_mobile\_command module +---------------------------------------------------- + +.. automodule:: webdriver.extensions.execute_mobile_command + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.hw\_actions module +--------------------------------------- + +.. automodule:: webdriver.extensions.hw_actions + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.images\_comparison module +---------------------------------------------- + +.. automodule:: webdriver.extensions.images_comparison + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.ime module +------------------------------- + +.. automodule:: webdriver.extensions.ime + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.keyboard module +------------------------------------ + +.. automodule:: webdriver.extensions.keyboard + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.location module +------------------------------------ + +.. automodule:: webdriver.extensions.location + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.log\_event module +-------------------------------------- + +.. automodule:: webdriver.extensions.log_event + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.remote\_fs module +-------------------------------------- + +.. automodule:: webdriver.extensions.remote_fs + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.screen\_record module +------------------------------------------ + +.. automodule:: webdriver.extensions.screen_record + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.session module +----------------------------------- + +.. automodule:: webdriver.extensions.session + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.settings module +------------------------------------ + +.. automodule:: webdriver.extensions.settings + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: webdriver.extensions + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/webdriver.extensions.search_context.rst b/docs/webdriver.extensions.search_context.rst new file mode 100644 index 00000000..cff8b87e --- /dev/null +++ b/docs/webdriver.extensions.search_context.rst @@ -0,0 +1,62 @@ +webdriver.extensions.search\_context package +============================================ + +Submodules +---------- + +webdriver.extensions.search\_context.android module +--------------------------------------------------- + +.. automodule:: webdriver.extensions.search_context.android + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.search\_context.base\_search\_context module +----------------------------------------------------------------- + +.. automodule:: webdriver.extensions.search_context.base_search_context + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.search\_context.custom module +-------------------------------------------------- + +.. automodule:: webdriver.extensions.search_context.custom + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.search\_context.ios module +----------------------------------------------- + +.. automodule:: webdriver.extensions.search_context.ios + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.search\_context.mobile module +-------------------------------------------------- + +.. automodule:: webdriver.extensions.search_context.mobile + :members: + :undoc-members: + :show-inheritance: + +webdriver.extensions.search\_context.windows module +--------------------------------------------------- + +.. automodule:: webdriver.extensions.search_context.windows + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: webdriver.extensions.search_context + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/webdriver.rst b/docs/webdriver.rst new file mode 100644 index 00000000..2b20f6b2 --- /dev/null +++ b/docs/webdriver.rst @@ -0,0 +1,102 @@ +webdriver package +================= + +Subpackages +----------- + +.. toctree:: + + webdriver.common + webdriver.extensions + +Submodules +---------- + +webdriver.appium\_connection module +----------------------------------- + +.. automodule:: webdriver.appium_connection + :members: + :undoc-members: + :show-inheritance: + +webdriver.appium\_service module +-------------------------------- + +.. automodule:: webdriver.appium_service + :members: + :undoc-members: + :show-inheritance: + +webdriver.applicationstate module +--------------------------------- + +.. automodule:: webdriver.applicationstate + :members: + :undoc-members: + :show-inheritance: + +webdriver.clipboard\_content\_type module +----------------------------------------- + +.. automodule:: webdriver.clipboard_content_type + :members: + :undoc-members: + :show-inheritance: + +webdriver.connectiontype module +------------------------------- + +.. automodule:: webdriver.connectiontype + :members: + :undoc-members: + :show-inheritance: + +webdriver.errorhandler module +----------------------------- + +.. automodule:: webdriver.errorhandler + :members: + :undoc-members: + :show-inheritance: + +webdriver.mobilecommand module +------------------------------ + +.. automodule:: webdriver.mobilecommand + :members: + :undoc-members: + :show-inheritance: + +webdriver.switch\_to module +--------------------------- + +.. automodule:: webdriver.switch_to + :members: + :undoc-members: + :show-inheritance: + +webdriver.webdriver module +-------------------------- + +.. automodule:: webdriver.webdriver + :members: + :undoc-members: + :show-inheritance: + +webdriver.webelement module +--------------------------- + +.. automodule:: webdriver.webelement + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: webdriver + :members: + :undoc-members: + :show-inheritance: diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..42606f97 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,9 @@ +[mypy] +check_untyped_defs = True +disallow_untyped_calls = True +disallow_untyped_defs = True +follow_imports = skip +ignore_missing_imports = True +strict_optional = True +warn_redundant_casts = True +warn_unused_ignores = True diff --git a/setup.py b/setup.py index 9912e7eb..774b06c2 100644 --- a/setup.py +++ b/setup.py @@ -37,13 +37,10 @@ packages=find_packages(include=['appium*']), license='Apache 2.0', classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Environment :: Console', 'Environment :: MacOS X', 'Environment :: Win32 (MS Windows)', diff --git a/test/functional/android/activities_tests.py b/test/functional/android/activities_tests.py index 7b1894ff..7f9978be 100644 --- a/test/functional/android/activities_tests.py +++ b/test/functional/android/activities_tests.py @@ -13,35 +13,28 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase -class ActivitiesTests(BaseTestCase): - def test_current_activity(self): +class TestActivities(BaseTestCase): + def test_current_activity(self) -> None: activity = self.driver.current_activity - self.assertEqual('.ApiDemos', activity) + assert '.ApiDemos' == activity - def test_start_activity_this_app(self): + def test_start_activity_this_app(self) -> None: self.driver.start_activity(APIDEMO_PKG_NAME, ".ApiDemos") self._assert_activity_contains('Demos') self.driver.start_activity(APIDEMO_PKG_NAME, ".accessibility.AccessibilityNodeProviderActivity") self._assert_activity_contains('Node') - def test_start_activity_other_app(self): + def test_start_activity_other_app(self) -> None: self.driver.start_activity(APIDEMO_PKG_NAME, ".ApiDemos") self._assert_activity_contains('Demos') self.driver.start_activity("com.android.calculator2", ".Calculator") self._assert_activity_contains('Calculator') - def _assert_activity_contains(self, activity): + def _assert_activity_contains(self, activity: str) -> None: current = self.driver.current_activity - self.assertTrue(activity in current) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(ActivitiesTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert activity in current diff --git a/test/functional/android/applications_tests.py b/test/functional/android/applications_tests.py index 9703fbc7..ea2216a8 100644 --- a/test/functional/android/applications_tests.py +++ b/test/functional/android/applications_tests.py @@ -14,70 +14,64 @@ # limitations under the License. import os -import unittest from time import sleep +import pytest + from appium.webdriver.applicationstate import ApplicationState from .helper.desired_capabilities import PATH from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase -class ApplicationsTests(BaseTestCase): +class TestApplications(BaseTestCase): - def test_background_app(self): + def test_background_app(self) -> None: self.driver.background_app(1) sleep(3) self.driver.launch_app() - def test_is_app_installed(self): - self.assertFalse(self.driver.is_app_installed('sdfsdf')) - self.assertTrue(self.driver.is_app_installed(APIDEMO_PKG_NAME)) + def test_is_app_installed(self) -> None: + assert not self.driver.is_app_installed('sdfsdf') + assert self.driver.is_app_installed(APIDEMO_PKG_NAME) - def test_install_app(self): - self.assertFalse(self.driver.is_app_installed('io.selendroid.testapp')) + @pytest.mark.skip('This causes the server to crash. no idea why') + def test_install_app(self) -> None: + assert not self.driver.is_app_installed('io.selendroid.testapp') self.driver.install_app(PATH(os.path.join('../..', 'apps', 'selendroid-test-app.apk'))) - self.assertTrue(self.driver.is_app_installed('io.selendroid.testapp')) + assert self.driver.is_app_installed('io.selendroid.testapp') - def test_remove_app(self): - self.assertTrue(self.driver.is_app_installed(APIDEMO_PKG_NAME)) + def test_remove_app(self) -> None: + assert self.driver.is_app_installed(APIDEMO_PKG_NAME) self.driver.remove_app(APIDEMO_PKG_NAME) - self.assertFalse(self.driver.is_app_installed(APIDEMO_PKG_NAME)) + assert not self.driver.is_app_installed(APIDEMO_PKG_NAME) - def test_close_and_launch_app(self): + def test_close_and_launch_app(self) -> None: self.driver.close_app() self.driver.launch_app() activity = self.driver.current_activity - self.assertEqual('.ApiDemos', activity) + assert '.ApiDemos' == activity - def test_app_management(self): + def test_app_management(self) -> None: app_id = self.driver.current_package - self.assertEqual(self.driver.query_app_state(app_id), - ApplicationState.RUNNING_IN_FOREGROUND) + assert self.driver.query_app_state(app_id) == ApplicationState.RUNNING_IN_FOREGROUND self.driver.background_app(-1) - self.assertTrue(self.driver.query_app_state(app_id) < - ApplicationState.RUNNING_IN_FOREGROUND) + assert self.driver.query_app_state(app_id) < ApplicationState.RUNNING_IN_FOREGROUND self.driver.activate_app(app_id) - self.assertEqual(self.driver.query_app_state(app_id), - ApplicationState.RUNNING_IN_FOREGROUND) + assert self.driver.query_app_state(app_id) == ApplicationState.RUNNING_IN_FOREGROUND - def test_app_strings(self): + def test_app_strings(self) -> None: strings = self.driver.app_strings() - self.assertEqual(u'You can\'t wipe my data, you are a monkey!', strings[u'monkey_wipe_data']) + assert u'You can\'t wipe my data, you are a monkey!' == strings[u'monkey_wipe_data'] - def test_app_strings_with_language(self): + def test_app_strings_with_language(self) -> None: strings = self.driver.app_strings('en') - self.assertEqual(u'You can\'t wipe my data, you are a monkey!', strings[u'monkey_wipe_data']) + assert u'You can\'t wipe my data, you are a monkey!' == strings[u'monkey_wipe_data'] - def test_app_strings_with_language_and_file(self): + def test_app_strings_with_language_and_file(self) -> None: strings = self.driver.app_strings('en', 'some_file') - self.assertEqual(u'You can\'t wipe my data, you are a monkey!', strings[u'monkey_wipe_data']) + assert u'You can\'t wipe my data, you are a monkey!' == strings[u'monkey_wipe_data'] - def test_reset(self): + def test_reset(self) -> None: self.driver.reset() - self.assertTrue(self.driver.is_app_installed(APIDEMO_PKG_NAME)) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(ApplicationsTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert self.driver.is_app_installed(APIDEMO_PKG_NAME) diff --git a/test/functional/android/chrome_tests.py b/test/functional/android/chrome_tests.py index 32f74216..08767349 100644 --- a/test/functional/android/chrome_tests.py +++ b/test/functional/android/chrome_tests.py @@ -12,29 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from appium import webdriver from .helper.desired_capabilities import get_desired_capabilities -class ChromeTests(unittest.TestCase): - def setUp(self): +class TestChrome(object): + def setup_method(self) -> None: caps = get_desired_capabilities() caps['browserName'] = 'Chrome' self.driver = webdriver.Remote('http://localhost:4723/wd/hub', caps) - def tearDown(self): + def teardown_method(self) -> None: self.driver.quit() - def test_find_single_element(self): + def test_find_single_element(self) -> None: self.driver.get('http://10.0.2.2:4723/test/guinea-pig') self.driver.find_element_by_link_text('i am a link').click() - self.assertTrue('I am some other page content' in self.driver.page_source) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(ChromeTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert 'I am some other page content' in self.driver.page_source diff --git a/test/functional/android/common_tests.py b/test/functional/android/common_tests.py index 7c50150c..7768c376 100644 --- a/test/functional/android/common_tests.py +++ b/test/functional/android/common_tests.py @@ -13,9 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from time import sleep +import pytest from selenium.common.exceptions import NoSuchElementException from appium.webdriver.common.mobileby import MobileBy @@ -28,25 +28,27 @@ ) -class CommonTests(BaseTestCase): +class TestCommon(BaseTestCase): - def test_current_package(self): - self.assertEqual(APIDEMO_PKG_NAME, self.driver.current_package) + def test_current_package(self) -> None: + assert APIDEMO_PKG_NAME == self.driver.current_package - def test_end_test_coverage(self): - self.skipTest('Not sure how to set this up to run') + @pytest.mark.skip('Not sure how to set this up to run') + def test_end_test_coverage(self) -> None: self.driver.end_test_coverage(intent='android.intent.action.MAIN', path='') sleep(5) - def test_open_notifications(self): + # TODO Due to unexpected dialog, "System UI isn't responding" + @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI.') + def test_open_notifications(self) -> None: for word in ['App', 'Notification', 'Status Bar', ':-|']: wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, - 'new UiSelector().text("{}")'.format(word)).click() + f'new UiSelector().text("{word}")').click() self.driver.open_notifications() sleep(1) - self.assertRaises(NoSuchElementException, - self.driver.find_element_by_android_uiautomator, 'new UiSelector().text(":-|")') + with pytest.raises(NoSuchElementException): + self.driver.find_element_by_android_uiautomator, 'new UiSelector().text(":-|")' els = self.driver.find_elements_by_class_name('android.widget.TextView') # sometimes numbers shift @@ -58,14 +60,9 @@ def test_open_notifications(self): title = True elif text == 'I am ok': body = True - self.assertTrue(title) - self.assertTrue(body) + assert title + assert body self.driver.keyevent(4) sleep(1) self.driver.find_element_by_android_uiautomator('new UiSelector().text(":-|")') - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(CommonTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/context_switching_tests.py b/test/functional/android/context_switching_tests.py index 2bc6d092..c358866a 100644 --- a/test/functional/android/context_switching_tests.py +++ b/test/functional/android/context_switching_tests.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - import pytest from appium import webdriver @@ -22,47 +20,40 @@ from .helper import desired_capabilities -class ContextSwitchingTests(unittest.TestCase): - def setUp(self): +@pytest.mark.skip(reason="Need to fix broken test") +class TestContextSwitching(object): + def setup_method(self) -> None: desired_caps = desired_capabilities.get_desired_capabilities('selendroid-test-app.apk') self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - def test_contexts_list(self): - self.skipTest('Need to replace with apk which has WEBVIEW as context') + def teardown_method(self) -> None: + self.driver.quit() + + def test_contexts_list(self) -> None: self._enter_webview() contexts = self.driver.contexts - self.assertEqual(2, len(contexts)) + assert 2 == len(contexts) - def test_move_to_correct_context(self): - self.skipTest('Need to replace with apk which has WEBVIEW as context') + def test_move_to_correct_context(self) -> None: self._enter_webview() - self.assertEqual('WEBVIEW_io.selendroid.testapp', self.driver.current_context) + assert 'WEBVIEW_io.selendroid.testapp' == self.driver.current_context - def test_actually_in_webview(self): - self.skipTest('Need to replace with apk which has WEBVIEW as context') + def test_actually_in_webview(self) -> None: self._enter_webview() self.driver.find_element_by_css_selector('input[type=submit]').click() el = self.driver.find_element_by_xpath("//h1[contains(., 'This is my way')]") - self.assertIsNot(None, el) + assert el is not None - def test_move_back_to_native_context(self): - self.skipTest('Need to replace with apk which has WEBVIEW as context') + def test_move_back_to_native_context(self) -> None: self._enter_webview() self.driver.switch_to.context(None) - self.assertEqual('NATIVE_APP', self.driver.current_context) + assert 'NATIVE_APP' == self.driver.current_context - def test_set_invalid_context(self): - self.assertRaises(NoSuchContextException, self.driver.switch_to.context, 'invalid name') + def test_set_invalid_context(self) -> None: + with pytest.raises(NoSuchContextException): + self.driver.switch_to.context('invalid name') - def tearDown(self): - self.driver.quit() - - def _enter_webview(self): - btn = self.driver.find_element_by_accessibility_id('buttonStartWebviewCD') + def _enter_webview(self) -> None: + btn = self.driver.find_element_by_name('buttonStartWebviewCD') btn.click() self.driver.switch_to.context('WEBVIEW') - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(ContextSwitchingTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/device_time_tests.py b/test/functional/android/device_time_tests.py index deefdd36..b5d576fb 100644 --- a/test/functional/android/device_time_tests.py +++ b/test/functional/android/device_time_tests.py @@ -13,20 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from dateutil.parser import parse from .helper.test_helper import BaseTestCase -class DeviceTimeTests(BaseTestCase): - def test_device_time(self): +class TestDeviceTime(BaseTestCase): + def test_device_time(self) -> None: date_time = self.driver.device_time # convert to date ought to work parse(date_time) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(DeviceTimeTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/finger_print_tests.py b/test/functional/android/finger_print_tests.py index e77e72a4..9b8871d9 100644 --- a/test/functional/android/finger_print_tests.py +++ b/test/functional/android/finger_print_tests.py @@ -13,17 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from .helper.test_helper import BaseTestCase -class FingerPrintTests(BaseTestCase): - def test_finger_print(self): +class TestFingerPrint(BaseTestCase): + def test_finger_print(self) -> None: result = self.driver.finger_print(1) - self.assertEqual(None, result) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(FingerPrintTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert result is None diff --git a/test/functional/android/helper/desired_capabilities.py b/test/functional/android/helper/desired_capabilities.py index eccb091a..7ddcd80a 100644 --- a/test/functional/android/helper/desired_capabilities.py +++ b/test/functional/android/helper/desired_capabilities.py @@ -13,17 +13,19 @@ # limitations under the License. import os - +from typing import Any, Dict, Optional # Returns abs path relative to this file and not cwd -def PATH(p): + + +def PATH(p: str) -> str: return os.path.abspath( os.path.join(os.path.dirname(__file__), '..', p) ) -def get_desired_capabilities(app=None): - desired_caps = { +def get_desired_capabilities(app: Optional[str] = None) -> Dict[str, Any]: + desired_caps: Dict[str, Any] = { 'platformName': 'Android', 'deviceName': 'Android Emulator', 'newCommandTimeout': 240, diff --git a/test/functional/android/helper/test_helper.py b/test/functional/android/helper/test_helper.py index 5813abc6..026ea9e3 100644 --- a/test/functional/android/helper/test_helper.py +++ b/test/functional/android/helper/test_helper.py @@ -15,7 +15,7 @@ import base64 import os -import unittest +from typing import TYPE_CHECKING from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait @@ -25,6 +25,10 @@ from . import desired_capabilities +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + from appium.webdriver.webdriver import WebDriver + # the emulator is sometimes slow and needs time to think SLEEPY_TIME = 10 @@ -32,7 +36,7 @@ APIDEMO_PKG_NAME = 'io.appium.android.apis' -def wait_for_element(driver, locator, value, timeout=SLEEPY_TIME): +def wait_for_element(driver: 'WebDriver', locator: str, value: str, timeout: int = SLEEPY_TIME) -> 'WebElement': """Wait until the element located Args: @@ -53,18 +57,18 @@ def wait_for_element(driver, locator, value, timeout=SLEEPY_TIME): ) -class BaseTestCase(unittest.TestCase): +class BaseTestCase(): - def setUp(self): + def setup_method(self, method) -> None: # type: ignore desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) if is_ci(): self.driver.start_recording_screen() - def tearDown(self): + def teardown_method(self, method) -> None: # type: ignore if is_ci(): payload = self.driver.stop_recording_screen() - video_path = os.path.join(os.getcwd(), self._testMethodName + '.mp4') + video_path = os.path.join(os.getcwd(), method.__name__ + '.mp4') with open(video_path, "wb") as fd: fd.write(base64.b64decode(payload)) self.driver.quit() diff --git a/test/functional/android/hw_actions_tests.py b/test/functional/android/hw_actions_tests.py index 3bb022f4..4646d254 100644 --- a/test/functional/android/hw_actions_tests.py +++ b/test/functional/android/hw_actions_tests.py @@ -13,23 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from time import sleep from .helper.test_helper import BaseTestCase -class HwActionsTests(BaseTestCase): - def test_lock(self): +class TestHwActions(BaseTestCase): + def test_lock(self) -> None: self.driver.lock(-1) sleep(10) try: - self.assertTrue(self.driver.is_locked()) + assert self.driver.is_locked() finally: self.driver.unlock() - self.assertFalse(self.driver.is_locked()) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(HwActionsTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert not self.driver.is_locked() diff --git a/test/functional/android/ime_tests.py b/test/functional/android/ime_tests.py index 86bf7449..ea0f6ea0 100644 --- a/test/functional/android/ime_tests.py +++ b/test/functional/android/ime_tests.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from time import sleep from .helper.test_helper import BaseTestCase @@ -22,37 +21,31 @@ GOOGLE_LATIN = 'com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME' # Android O/P -class IMETests(BaseTestCase): - def test_available_ime_engines(self): +class TestIME(BaseTestCase): + def test_available_ime_engines(self) -> None: engines = self.driver.available_ime_engines - self.assertIsInstance(engines, list) - self.assertTrue(ANDROID_LATIN in engines or GOOGLE_LATIN in engines) + assert isinstance(engines, list) + assert ANDROID_LATIN in engines or GOOGLE_LATIN in engines - def test_is_ime_active(self): - self.assertTrue(self.driver.is_ime_active()) + def test_is_ime_active(self) -> None: + assert self.driver.is_ime_active() - def test_active_ime_engine(self): + def test_active_ime_engine(self) -> None: engines = self.driver.available_ime_engines - self.assertTrue(self.driver.active_ime_engine in engines) + assert self.driver.active_ime_engine in engines - def test_activate_ime_engine(self): + def test_activate_ime_engine(self) -> None: engines = self.driver.available_ime_engines - active_engine = self.driver.active_ime_engine self.driver.activate_ime_engine(engines[-1]) - self.assertEqual(self.driver.active_ime_engine, engines[-1]) + assert self.driver.active_ime_engine == engines[-1] - def test_deactivate_ime_engine(self): + def test_deactivate_ime_engine(self) -> None: engines = self.driver.available_ime_engines self.driver.activate_ime_engine(engines[-1]) - self.assertEqual(self.driver.active_ime_engine, engines[-1]) + assert self.driver.active_ime_engine == engines[-1] self.driver.deactivate_ime_engine() sleep(1) - self.assertNotEqual(self.driver.active_ime_engine, engines[-1]) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(IMETests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert self.driver.active_ime_engine != engines[-1] diff --git a/test/functional/android/keyboard_tests.py b/test/functional/android/keyboard_tests.py index 6c634da3..5e1ebe6a 100644 --- a/test/functional/android/keyboard_tests.py +++ b/test/functional/android/keyboard_tests.py @@ -13,21 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from .helper.test_helper import BaseTestCase -class KeyboardTests(BaseTestCase): - def test_press_keycode(self): - # not sure how to test this. +class TestKeyboard(BaseTestCase): + def test_press_keycode(self) -> None: + # TODO not sure how to test this. self.driver.press_keycode(176) - def test_long_press_keycode(self): - # not sure how to test this. + def test_long_press_keycode(self) -> None: + # TODO not sure how to test this. self.driver.long_press_keycode(176) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(KeyboardTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/location_tests.py b/test/functional/android/location_tests.py index 1584fd5a..30cd7880 100644 --- a/test/functional/android/location_tests.py +++ b/test/functional/android/location_tests.py @@ -13,16 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from .helper.test_helper import BaseTestCase -class LocationTests(BaseTestCase): - def test_toggle_location_services(self): - self.driver.toggle_location_services() - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(LocationTests) - unittest.TextTestRunner(verbosity=2).run(suite) +class TestLocation(BaseTestCase): + def test_toggle_location_services(self) -> None: + self.driver.toggle_location_services() # TODO Add assert diff --git a/test/functional/android/log_event_tests.py b/test/functional/android/log_event_tests.py index 8067e9f7..9ad0b53d 100644 --- a/test/functional/android/log_event_tests.py +++ b/test/functional/android/log_event_tests.py @@ -13,19 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from .helper.test_helper import BaseTestCase -class LogEventTests(BaseTestCase): - def test_log_event(self): +class TestLogEvent(BaseTestCase): + def test_log_event(self) -> None: vendor = 'appium' event = 'funEvent' self.driver.log_event(vendor, event) - assert '{}:{}'.format(vendor, event) in self.driver.get_events().keys() - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(LogEventTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert f'{vendor}:{event}' in self.driver.get_events().keys() diff --git a/test/functional/android/multi_action_tests.py b/test/functional/android/multi_action_tests.py index 7ed47708..6739bf59 100644 --- a/test/functional/android/multi_action_tests.py +++ b/test/functional/android/multi_action_tests.py @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from time import sleep +import pytest + from appium.webdriver.common.mobileby import MobileBy from appium.webdriver.common.multi_action import MultiAction from appium.webdriver.common.touch_action import TouchAction @@ -22,8 +23,8 @@ from .helper.test_helper import BaseTestCase, is_ci, wait_for_element -class MultiActionTests(BaseTestCase): - def test_parallel_actions(self): +class TestMultiAction(BaseTestCase): + def test_parallel_actions(self) -> None: self._move_to_splitting_touches_accros_views() els = self.driver.find_elements_by_class_name('android.widget.ListView') @@ -39,7 +40,7 @@ def test_parallel_actions(self): ma.add(a1, a2) ma.perform() - def test_actions_with_waits(self): + def test_actions_with_waits(self) -> None: self._move_to_splitting_touches_accros_views() els = self.driver.find_elements_by_class_name('android.widget.ListView') @@ -63,7 +64,7 @@ def test_actions_with_waits(self): ma.add(a1, a2) ma.perform() - def _move_to_splitting_touches_accros_views(self): + def _move_to_splitting_touches_accros_views(self) -> None: el1 = self.driver.find_element_by_accessibility_id('Content') el2 = self.driver.find_element_by_accessibility_id('Animation') self.driver.scroll(el1, el2) @@ -83,9 +84,8 @@ def _move_to_splitting_touches_accros_views(self): wait_for_element(self.driver, MobileBy.ID, 'io.appium.android.apis:id/list1') - def test_driver_multi_tap(self): - if is_ci(): - self.skipTest('Skip since the test must be watched to check if it works') + @pytest.mark.skipif(condition=is_ci(), reason='Skip since the test must be watched to check if it works') + def test_driver_multi_tap(self) -> None: el = self.driver.find_element_by_accessibility_id('Graphics') action = TouchAction(self.driver) action.tap(el).perform() @@ -107,8 +107,3 @@ def test_driver_multi_tap(self): # THE TEST MUST BE WATCHED TO CHECK IF IT WORKS self.driver.tap(positions) sleep(10) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(MultiActionTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/network_connection_tests.py b/test/functional/android/network_connection_tests.py index 34267345..e0a5fb7d 100644 --- a/test/functional/android/network_connection_tests.py +++ b/test/functional/android/network_connection_tests.py @@ -13,24 +13,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest +import pytest from appium.webdriver.connectiontype import ConnectionType +from ..test_helper import is_ci from .helper.test_helper import BaseTestCase -class NetworkConnectionTests(BaseTestCase): - def test_get_network_connection(self): +class TestNetworkConnection(BaseTestCase): + def test_get_network_connection(self) -> None: nc = self.driver.network_connection - self.assertIsInstance(nc, int) + assert isinstance(nc, int) - def test_set_network_connection(self): + @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI') + def test_set_network_connection(self) -> None: nc = self.driver.set_network_connection(ConnectionType.DATA_ONLY) - self.assertIsInstance(nc, int) - self.assertEqual(nc, ConnectionType.DATA_ONLY) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(NetworkConnectionTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert isinstance(nc, int) + assert nc == ConnectionType.DATA_ONLY diff --git a/test/functional/android/remote_fs_tests.py b/test/functional/android/remote_fs_tests.py index fefe0839..a71fe30c 100644 --- a/test/functional/android/remote_fs_tests.py +++ b/test/functional/android/remote_fs_tests.py @@ -15,27 +15,24 @@ import base64 import os import random -import unittest from io import BytesIO from zipfile import ZipFile -from appium.common.helper import appium_bytes - from .helper.test_helper import BaseTestCase -class RemoteFsTests(BaseTestCase): - def test_push_pull_file(self): +class TestRemoteFs(BaseTestCase): + def test_push_pull_file(self) -> None: dest_path = '/data/local/tmp/test_push_file.txt' - data = appium_bytes('This is the contents of the file to push to the device.', 'utf-8') + data = bytes('This is the contents of the file to push to the device.', 'utf-8') self.driver.push_file(dest_path, base64.b64encode(data).decode('utf-8')) data_ret = base64.b64decode(self.driver.pull_file(dest_path)) - self.assertEqual(data, data_ret) + assert data == data_ret - def test_pull_folder(self): - data = appium_bytes('random string data {}'.format(random.randint(0, 1000)), 'utf-8') + def test_pull_folder(self) -> None: + data = bytes('random string data {}'.format(random.randint(0, 1000)), 'utf-8') dest_dir = '/data/local/tmp/' for filename in ['1.txt', '2.txt']: @@ -45,9 +42,9 @@ def test_pull_folder(self): with ZipFile(BytesIO(base64.b64decode(folder))) as fzip: for filename in ['1.txt', '2.txt']: - self.assertTrue(filename in fzip.namelist()) + assert filename in fzip.namelist() - def test_push_file_with_src_path(self): + def test_push_file_with_src_path(self) -> None: test_files = ['test_image.jpg', 'test_file.txt'] for file_name in test_files: src_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', file_name) @@ -58,9 +55,4 @@ def test_push_file_with_src_path(self): self.driver.push_file(dest_path, source_path=src_path) new_data = base64.b64decode(self.driver.pull_file(dest_path)) - self.assertEqual(original_data, new_data) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(RemoteFsTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert original_data == new_data diff --git a/test/functional/android/screen_record_tests.py b/test/functional/android/screen_record_tests.py index 26f5f764..49618a89 100644 --- a/test/functional/android/screen_record_tests.py +++ b/test/functional/android/screen_record_tests.py @@ -13,20 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from time import sleep from .helper.test_helper import BaseTestCase -class ScreenRecordTests(BaseTestCase): - def test_screen_record(self): +class TestScreenRecord(BaseTestCase): + def test_screen_record(self) -> None: self.driver.start_recording_screen(timeLimit=10, forcedRestart=True) sleep(10) result = self.driver.stop_recording_screen() - self.assertTrue(len(result) > 0) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(ScreenRecordTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert len(result) > 0 diff --git a/test/functional/android/search_context/find_by_accessibility_id_tests.py b/test/functional/android/search_context/find_by_accessibility_id_tests.py index b5e8a878..6ef5cf23 100644 --- a/test/functional/android/search_context/find_by_accessibility_id_tests.py +++ b/test/functional/android/search_context/find_by_accessibility_id_tests.py @@ -12,44 +12,41 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest +import pytest from appium.webdriver.common.mobileby import MobileBy from test.functional.android.helper.test_helper import ( BaseTestCase, + is_ci, wait_for_element ) -class FindByAccessibilityIDTests(BaseTestCase): - def test_find_single_element(self): +class TestFindByAccessibilityID(BaseTestCase): + def test_find_single_element(self) -> None: wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility")').click() wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility Node Querying")').click() el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Task Take out Trash') - self.assertIsNotNone(el) + assert el is not None - def test_find_multiple_elements(self): + def test_find_multiple_elements(self) -> None: els = self.driver.find_elements_by_accessibility_id('Accessibility') - self.assertIsInstance(els, list) + assert isinstance(els, list) - def test_element_find_single_element(self): + @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI') + def test_element_find_single_element(self) -> None: wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility")').click() wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility Node Querying")').click() - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Task Take out Trash') + el = wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.ListView') sub_el = el.find_element_by_accessibility_id('Task Take out Trash') - self.assertIsNotNone(sub_el) + assert sub_el is not None - def test_element_find_multiple_elements(self): + def test_element_find_multiple_elements(self) -> None: wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.ListView') el = self.driver.find_element_by_class_name('android.widget.ListView') sub_els = el.find_elements_by_accessibility_id('Animation') - self.assertIsInstance(sub_els, list) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(FindByAccessibilityIDTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert isinstance(sub_els, list) diff --git a/test/functional/android/search_context/find_by_image_tests.py b/test/functional/android/search_context/find_by_image_tests.py index 0ba6f8a9..86dec00e 100644 --- a/test/functional/android/search_context/find_by_image_tests.py +++ b/test/functional/android/search_context/find_by_image_tests.py @@ -13,22 +13,19 @@ # limitations under the License. import base64 -import unittest +import pytest from selenium.common.exceptions import NoSuchElementException, TimeoutException -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.support.ui import WebDriverWait from appium import webdriver from appium.webdriver.common.mobileby import MobileBy from test.functional.android.helper import desired_capabilities +from test.functional.android.helper.test_helper import wait_for_element -from ..helper.test_helper import wait_for_element +class TestFindByImage(object): -class FindByImageTests(unittest.TestCase): - - def setUp(self): + def setup_method(self) -> None: desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) @@ -37,50 +34,44 @@ def setUp(self): "fixImageTemplateSize": True, "autoUpdateImageElementPosition": True}) - def tearDown(self): + def teardown_method(self) -> None: self.driver.quit() - def test_find_based_on_image_template(self): + def test_find_based_on_image_template(self) -> None: image_path = desired_capabilities.PATH('file/find_by_image_success.png') with open(image_path, 'rb') as png_file: b64_data = base64.b64encode(png_file.read()).decode('UTF-8') el = wait_for_element(self.driver, MobileBy.IMAGE, b64_data) size = el.size - self.assertIsNotNone(size['width']) - self.assertIsNotNone(size['height']) + assert size['width'] is not None + assert size['height'] is not None loc = el.location - self.assertIsNotNone(loc['x']) - self.assertIsNotNone(loc['y']) + assert loc['x'] is not None + assert loc['y'] is not None rect = el.rect - self.assertIsNotNone(rect['width']) - self.assertIsNotNone(rect['height']) - self.assertIsNotNone(rect['x']) - self.assertIsNotNone(rect['y']) - self.assertTrue(el.is_displayed()) + assert rect['width'] is not None + assert rect['height'] is not None + assert rect['x'] is not None + assert rect['y'] is not None + assert el.is_displayed() el.click() wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "Alarm") - def test_find_multiple_elements_by_image_just_returns_one(self): + def test_find_multiple_elements_by_image_just_returns_one(self) -> None: wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "App") image_path = desired_capabilities.PATH('file/find_by_image_success.png') els = self.driver.find_elements_by_image(image_path) els[0].click() wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "Alarm") - def test_find_throws_no_such_element(self): + def test_find_throws_no_such_element(self) -> None: image_path = desired_capabilities.PATH('file/find_by_image_failure.png') with open(image_path, 'rb') as png_file: b64_data = base64.b64encode(png_file.read()).decode('UTF-8') - with self.assertRaises(TimeoutException): - WebDriverWait(self.driver, 3).until( - EC.presence_of_element_located((MobileBy.IMAGE, b64_data)) - ) - with self.assertRaises(NoSuchElementException): - self.driver.find_element_by_image(image_path) - + with pytest.raises(TimeoutException): + wait_for_element(self.driver, MobileBy.IMAGE, b64_data, timeout=3) -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(FindByImageTests) - unittest.TextTestRunner(verbosity=2).run(suite) + with pytest.raises(NoSuchElementException): + self.driver.find_element_by_image(image_path) diff --git a/test/functional/android/search_context/find_by_uiautomator_tests.py b/test/functional/android/search_context/find_by_uiautomator_tests.py index b602a591..2c23cfee 100644 --- a/test/functional/android/search_context/find_by_uiautomator_tests.py +++ b/test/functional/android/search_context/find_by_uiautomator_tests.py @@ -12,40 +12,35 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - import pytest from test.functional.android.helper.test_helper import BaseTestCase -class FindByUIAutomatorTests(BaseTestCase): - def test_find_single_element(self): +@pytest.mark.skip(reason="Need to fix flaky test") +class TestFindByUIAutomator(BaseTestCase): + def test_find_single_element(self) -> None: el = self.driver.find_element_by_android_uiautomator('new UiSelector().text("Animation")') - self.assertIsNotNone(el) + assert el is not None - def test_find_multiple_elements(self): + def test_find_multiple_elements(self) -> None: els = self.driver.find_elements_by_android_uiautomator('new UiSelector().clickable(true)') - self.assertIsInstance(els, list) + assert isinstance(els, list) - def test_element_find_single_element(self): + def test_element_find_single_element(self) -> None: el = self.driver.find_element_by_class_name('android.widget.ListView') sub_el = el.find_element_by_android_uiautomator('new UiSelector().description("Animation")') - self.assertIsNotNone(sub_el) + assert sub_el is not None - def test_element_find_multiple_elements(self): + def test_element_find_multiple_elements(self) -> None: el = self.driver.find_element_by_class_name('android.widget.ListView') sub_els = el.find_elements_by_android_uiautomator('new UiSelector().clickable(true)') - self.assertIsInstance(sub_els, list) + assert isinstance(sub_els, list) - def test_scroll_into_view(self): + def test_scroll_into_view(self) -> None: el = self.driver.find_element_by_android_uiautomator( 'new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));') el.click() - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(FindByUIAutomatorTests) - unittest.TextTestRunner(verbosity=2).run(suite) + # TODO Add assert diff --git a/test/functional/android/search_context/find_by_view_matcher_tests.py b/test/functional/android/search_context/find_by_view_matcher_tests.py index 0f8ed0d0..5bb0a47d 100644 --- a/test/functional/android/search_context/find_by_view_matcher_tests.py +++ b/test/functional/android/search_context/find_by_view_matcher_tests.py @@ -24,36 +24,33 @@ AndroidSearchContext ) from test.functional.android.helper.test_helper import ( + BaseTestCase, desired_capabilities, is_ci ) -class FindByViewMatcherTests(unittest.TestCase): +class TestFindByViewMatcher(BaseTestCase): - def setUp(self): + # Override + def setup_method(self, method) -> None: # type: ignore desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') desired_caps['automationName'] = 'Espresso' self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): if is_ci(): - # Take the screenshot to investigate when tests failed only on CI - img_path = os.path.join(os.getcwd(), self._testMethodName + '.png') - self.driver.get_screenshot_as_file(img_path) - self.driver.quit() + self.driver.start_recording_screen() - def test_find_single_element(self): + def test_find_single_element(self) -> None: el = self.driver.find_element_by_android_view_matcher( name='withText', args=['Accessibility'], className='ViewMatchers') assert el.text == 'Accessibility' - def test_find_single_element_ful_class_name(self): + def test_find_single_element_ful_class_name(self) -> None: el = self.driver.find_element_by_android_view_matcher( name='withText', args=['Accessibility'], className='androidx.test.espresso.matcher.ViewMatchers') assert el.text == 'Accessibility' - def test_find_single_element_using_hamcrest_matcher(self): + def test_find_single_element_using_hamcrest_matcher(self) -> None: el = self.driver.find_element_by_android_view_matcher( name='withText', args={ @@ -65,13 +62,8 @@ def test_find_single_element_using_hamcrest_matcher(self): # androidx.test.espresso.AmbiguousViewMatcherException: # 'with text: a string containing "Access"' matches multiple views in the hierarchy. - def test_find_multiple_elements(self): + def test_find_multiple_elements(self) -> None: value = AndroidSearchContext()._build_data_matcher( name='withSubstring', args=['Access'], className='ViewMatchers') with pytest.raises(WebDriverException): self.driver.find_elements(by=MobileBy.ANDROID_VIEW_MATCHER, value=value) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(FindByViewMatcherTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/settings_tests.py b/test/functional/android/settings_tests.py index a1ffe10b..ac600a3c 100644 --- a/test/functional/android/settings_tests.py +++ b/test/functional/android/settings_tests.py @@ -13,22 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from .helper.test_helper import BaseTestCase -class SettingsTests(BaseTestCase): - def test_get_settings(self): +class TestSettings(BaseTestCase): + def test_get_settings(self) -> None: settings = self.driver.get_settings() - self.assertIsNotNone(settings) + assert settings is not None - def test_update_settings(self): + def test_update_settings(self) -> None: self.driver.update_settings({"waitForIdleTimeout": 10001}) settings = self.driver.get_settings() - self.assertEqual(settings["waitForIdleTimeout"], 10001) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(SettingsTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert settings["waitForIdleTimeout"] == 10001 diff --git a/test/functional/android/touch_action_tests.py b/test/functional/android/touch_action_tests.py index 5fb5dca5..42f22fb6 100644 --- a/test/functional/android/touch_action_tests.py +++ b/test/functional/android/touch_action_tests.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - +import pytest from selenium.common.exceptions import NoSuchElementException from appium.webdriver.common.mobileby import MobileBy @@ -27,23 +26,24 @@ ) -class TouchActionTests(BaseTestCase): - def test_tap(self): +class TestTouchAction(BaseTestCase): + def test_tap(self) -> None: el = self.driver.find_element_by_accessibility_id('Animation') action = TouchAction(self.driver) action.tap(el).perform() el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls') - self.assertIsNotNone(el) + assert el is not None - def test_tap_x_y(self): + def test_tap_x_y(self) -> None: el = self.driver.find_element_by_accessibility_id('Animation') action = TouchAction(self.driver) action.tap(el, 100, 10).perform() el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls') - self.assertIsNotNone(el) + assert el is not None - def test_tap_twice(self): + @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI.') + def test_tap_twice(self) -> None: el = self.driver.find_element_by_accessibility_id('Text') action = TouchAction(self.driver) action.tap(el).perform() @@ -55,25 +55,25 @@ def test_tap_twice(self): action.tap(el, count=2).perform() els = self.driver.find_elements_by_class_name('android.widget.TextView') - self.assertEqual('This is a test\nThis is a test\n', els[1].get_attribute("text")) + assert 'This is a test\nThis is a test\n' == els[1].get_attribute("text") - def test_press_and_immediately_release(self): + def test_press_and_immediately_release(self) -> None: el = self.driver.find_element_by_accessibility_id('Animation') action = TouchAction(self.driver) action.press(el).release().perform() el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls') - self.assertIsNotNone(el) + assert el is not None - def test_press_and_immediately_release_x_y(self): + def test_press_and_immediately_release_x_y(self) -> None: el = self.driver.find_element_by_accessibility_id('Animation') action = TouchAction(self.driver) action.press(el, 100, 10).release().perform() el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls') - self.assertIsNotNone(el) + assert el is not None - def test_press_and_wait(self): + def test_press_and_wait(self) -> None: self._move_to_custom_adapter() action = TouchAction(self.driver) @@ -84,9 +84,9 @@ def test_press_and_wait(self): # 'Sample menu' only comes up with a long press, not a press el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Sample menu")') - self.assertIsNotNone(el) + assert el is not None - def test_press_and_moveto(self): + def test_press_and_moveto(self) -> None: el1 = self.driver.find_element_by_accessibility_id('Content') el2 = self.driver.find_element_by_accessibility_id('Animation') @@ -94,9 +94,9 @@ def test_press_and_moveto(self): action.press(el1).move_to(el2).release().perform() el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views') - self.assertIsNotNone(el) + assert el is not None - def test_press_and_moveto_x_y(self): + def test_press_and_moveto_x_y(self) -> None: el1 = self.driver.find_element_by_accessibility_id('Content') el2 = self.driver.find_element_by_accessibility_id('App') @@ -104,9 +104,9 @@ def test_press_and_moveto_x_y(self): action.press(el1).move_to(el2, 100, 100).release().perform() el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views') - self.assertIsNotNone(el) + assert el is not None - def test_long_press(self): + def test_long_press(self) -> None: self._move_to_custom_adapter() action = TouchAction(self.driver) @@ -117,11 +117,10 @@ def test_long_press(self): # 'Sample menu' only comes up with a long press, not a tap el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Sample menu")') - self.assertIsNotNone(el) + assert el is not None - def test_long_press_x_y(self): - if is_ci(): - self.skipTest("Skip since this check is low robust due to hard-coded position.") + @pytest.mark.skipif(condition=is_ci(), reason='Skip since this check is low robust due to hard-coded position.') + def test_long_press_x_y(self) -> None: self._move_to_custom_adapter() action = TouchAction(self.driver) @@ -132,9 +131,9 @@ def test_long_press_x_y(self): # 'Sample menu' only comes up with a long press, not a tap el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Sample menu")') - self.assertIsNotNone(el) + assert el is not None - def test_drag_and_drop(self): + def test_drag_and_drop(self) -> None: self._move_to_views() action = TouchAction(self.driver) @@ -148,9 +147,9 @@ def test_drag_and_drop(self): action.long_press(dd3).move_to(dd2).release().perform() el = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_result_text'.format(APIDEMO_PKG_NAME)) - self.assertTrue('Dropped!' in el.text) + assert 'Dropped!' in el.text - def test_driver_drag_and_drop(self): + def test_driver_drag_and_drop(self) -> None: self._move_to_views() action = TouchAction(self.driver) @@ -163,21 +162,21 @@ def test_driver_drag_and_drop(self): self.driver.drag_and_drop(dd3, dd2) el = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_result_text'.format(APIDEMO_PKG_NAME)) - self.assertTrue('Dropped!' in el.text) + assert 'Dropped!' in el.text - def test_driver_swipe(self): + def test_driver_swipe(self) -> None: el = self.driver.find_element_by_accessibility_id('Views') action = TouchAction(self.driver) action.tap(el).perform() - wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Animation') - self.assertRaises(NoSuchElementException, self.driver.find_element_by_accessibility_id, 'ImageView') + with pytest.raises(NoSuchElementException): + self.driver.find_element_by_accessibility_id('ImageView') self.driver.swipe(100, 1000, 100, 100, 800) el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'ImageView') - self.assertIsNotNone(el) + assert el is not None - def _move_to_views(self): + def _move_to_views(self) -> None: el1 = self.driver.find_element_by_accessibility_id('Content') el2 = self.driver.find_element_by_accessibility_id('Animation') self.driver.scroll(el1, el2) @@ -186,7 +185,7 @@ def _move_to_views(self): action = TouchAction(self.driver) action.tap(el).perform() - def _move_to_custom_adapter(self): + def _move_to_custom_adapter(self) -> None: self._move_to_views() action = TouchAction(self.driver) @@ -195,8 +194,3 @@ def _move_to_custom_adapter(self): el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, '1. Custom Adapter') action.tap(el).perform() - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(TouchActionTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/webelement_tests.py b/test/functional/android/webelement_tests.py index 1a16473b..d9c484c4 100644 --- a/test/functional/android/webelement_tests.py +++ b/test/functional/android/webelement_tests.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from appium.webdriver.common.mobileby import MobileBy from .helper.test_helper import ( @@ -24,14 +22,14 @@ ) -class WebelementTests(BaseTestCase): - def test_element_location_in_view(self): +class TestWebelement(BaseTestCase): + def test_element_location_in_view(self) -> None: el = self.driver.find_element_by_accessibility_id('Content') loc = el.location_in_view - self.assertIsNotNone(loc['x']) - self.assertIsNotNone(loc['y']) + assert loc['x'] is not None + assert loc['y'] is not None - def test_set_text(self): + def test_set_text(self) -> None: self.driver.find_element_by_android_uiautomator( 'new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));').click() @@ -42,19 +40,14 @@ def test_set_text(self): el.send_keys('original text') el.set_text('new text') - self.assertEqual('new text', el.text) + assert 'new text' == el.text - def test_send_keys(self): + def test_send_keys(self) -> None: for text in ['App', 'Activity', 'Custom Title']: wait_for_element(self.driver, MobileBy.XPATH, - "//android.widget.TextView[@text='{}']".format(text)).click() + f"//android.widget.TextView[@text='{text}']").click() el = wait_for_element(self.driver, MobileBy.ID, '{}:id/left_text_edit'.format(APIDEMO_PKG_NAME)) el.send_keys(' text') - self.assertEqual('Left is best text', el.text) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(WebelementTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert 'Left is best text' == el.text diff --git a/test/functional/ios/applications_tests.py b/test/functional/ios/applications_tests.py index 067aec18..c3b3ef1c 100644 --- a/test/functional/ios/applications_tests.py +++ b/test/functional/ios/applications_tests.py @@ -12,31 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from appium.webdriver.applicationstate import ApplicationState from test.functional.ios.helper.test_helper import BaseTestCase from .helper import desired_capabilities -class WebDriverTests(BaseTestCase): +class TestWebDriver(BaseTestCase): - def test_app_management(self): + def test_app_management(self) -> None: # this only works in Xcode9+ if float(desired_capabilities.get_desired_capabilities( desired_capabilities.BUNDLE_ID)['platformVersion']) < 11: return - self.assertEqual(self.driver.query_app_state(desired_capabilities.BUNDLE_ID), - ApplicationState.RUNNING_IN_FOREGROUND) + assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND self.driver.background_app(-1) - self.assertTrue(self.driver.query_app_state(desired_capabilities.BUNDLE_ID) < - ApplicationState.RUNNING_IN_FOREGROUND) + assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) < ApplicationState.RUNNING_IN_FOREGROUND self.driver.activate_app(desired_capabilities.BUNDLE_ID) - self.assertEqual(self.driver.query_app_state(desired_capabilities.BUNDLE_ID), - ApplicationState.RUNNING_IN_FOREGROUND) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(WebDriverTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND diff --git a/test/functional/ios/execute_driver_tests.py b/test/functional/ios/execute_driver_tests.py index 4f2251ec..8c3a64e8 100644 --- a/test/functional/ios/execute_driver_tests.py +++ b/test/functional/ios/execute_driver_tests.py @@ -13,13 +13,12 @@ # limitations under the License. import textwrap -import unittest from test.functional.ios.helper.test_helper import BaseTestCase -class ExecuteDriverTests(BaseTestCase): - def test_batch(self): +class TestExecuteDriver(BaseTestCase): + def test_batch(self) -> None: script = """ const status = await driver.status(); console.warn('warning message'); @@ -30,7 +29,7 @@ def test_batch(self): assert(response.result['build']) assert(response.logs['warn'] == ['warning message']) - def test_batch_combination_python_script(self): + def test_batch_combination_python_script(self) -> None: script = """ console.warn('warning message'); const element = await driver.findElement('accessibility id', 'Buttons'); @@ -42,8 +41,3 @@ def test_batch_combination_python_script(self): r = response.result[0].rect assert(r == response.result[1]) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(ExecuteDriverTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/ios/helper/desired_capabilities.py b/test/functional/ios/helper/desired_capabilities.py index 2fc9b25c..50810667 100644 --- a/test/functional/ios/helper/desired_capabilities.py +++ b/test/functional/ios/helper/desired_capabilities.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. import os +from typing import Any, Dict, Optional # Returns abs path relative to this file and not cwd -def PATH(p): return os.path.abspath( +def PATH(p: str) -> str: return os.path.abspath( os.path.join(os.path.dirname(__file__), p) ) @@ -24,8 +25,8 @@ def PATH(p): return os.path.abspath( BUNDLE_ID = 'com.example.apple-samplecode.UICatalog' -def get_desired_capabilities(app=None): - desired_caps = { +def get_desired_capabilities(app: Optional[str] = None) -> Dict[str, Any]: + desired_caps: Dict[str, Any] = { 'deviceName': iphone_device_name(), 'platformName': 'iOS', 'platformVersion': '13.3', @@ -41,24 +42,24 @@ def get_desired_capabilities(app=None): return desired_caps -class PytestXdistWorker(object): - NUMBER = os.getenv('PYTEST_XDIST_WORKER') - COUNT = os.getenv('PYTEST_XDIST_WORKER_COUNT') # Return 2 if `-n 2` is passed +class PytestXdistWorker: + NUMBER: Optional[str] = os.getenv('PYTEST_XDIST_WORKER') + COUNT: Optional[str] = os.getenv('PYTEST_XDIST_WORKER_COUNT') # Return 2 if `-n 2` is passed @staticmethod - def gw(number): + def gw(number: int) -> str: if PytestXdistWorker.COUNT is None: return '0' if number >= int(PytestXdistWorker.COUNT): return 'gw0' - return 'gw{}'.format(number) + return f'gw{number}' # If you run tests with pytest-xdist, you can run tests in parallel. -def wda_port(): +def wda_port() -> int: if PytestXdistWorker.NUMBER == PytestXdistWorker.gw(1): return 8101 @@ -68,7 +69,7 @@ def wda_port(): # Before running tests, you must have iOS simulators named 'iPhone 8 - 8100' and 'iPhone 8 - 8101' -def iphone_device_name(port=None): +def iphone_device_name() -> str: if PytestXdistWorker.NUMBER == PytestXdistWorker.gw(0): return 'iPhone 8 - 8100' elif PytestXdistWorker.NUMBER == PytestXdistWorker.gw(1): diff --git a/test/functional/ios/helper/test_helper.py b/test/functional/ios/helper/test_helper.py index 3329bbd6..f78912f6 100644 --- a/test/functional/ios/helper/test_helper.py +++ b/test/functional/ios/helper/test_helper.py @@ -15,7 +15,6 @@ import base64 import os -import unittest from appium import webdriver from test.functional.test_helper import is_ci @@ -23,18 +22,18 @@ from . import desired_capabilities -class BaseTestCase(unittest.TestCase): +class BaseTestCase(object): - def setUp(self): + def setup_method(self) -> None: desired_caps = desired_capabilities.get_desired_capabilities('UICatalog.app.zip') self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) if is_ci(): self.driver.start_recording_screen() - def tearDown(self): + def teardown_method(self, method) -> None: # type: ignore if is_ci(): payload = self.driver.stop_recording_screen() - video_path = os.path.join(os.getcwd(), self._testMethodName + '.mp4') + video_path = os.path.join(os.getcwd(), method.__name__ + '.mp4') with open(video_path, "wb") as fd: fd.write(base64.b64decode(payload)) self.driver.quit() diff --git a/test/functional/ios/hw_actions_tests.py b/test/functional/ios/hw_actions_tests.py index f37d268d..86264b37 100644 --- a/test/functional/ios/hw_actions_tests.py +++ b/test/functional/ios/hw_actions_tests.py @@ -12,34 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from test.functional.ios.helper.test_helper import BaseTestCase -class HwActionsTests(BaseTestCase): - def test_lock(self): +class TestHwActions(BaseTestCase): + def test_lock(self) -> None: self.driver.lock(-1) try: - self.assertTrue(self.driver.is_locked()) + assert self.driver.is_locked() finally: self.driver.unlock() - self.assertFalse(self.driver.is_locked()) + assert not self.driver.is_locked() - def test_shake(self): - # what can we assert about this? + def test_shake(self) -> None: + # TODO what can we assert about this? self.driver.shake() - def test_touch_id(self): + def test_touch_id(self) -> None: # nothing to assert, just verify that it doesn't blow up self.driver.touch_id(True) self.driver.touch_id(False) - def test_toggle_touch_id_enrollment(self): + def test_toggle_touch_id_enrollment(self) -> None: # nothing to assert, just verify that it doesn't blow up self.driver.toggle_touch_id_enrollment() - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(HwActionsTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/ios/keyboard_tests.py b/test/functional/ios/keyboard_tests.py index 7ae62347..0ee303e0 100644 --- a/test/functional/ios/keyboard_tests.py +++ b/test/functional/ios/keyboard_tests.py @@ -12,14 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from time import sleep from test.functional.ios.helper.test_helper import BaseTestCase -class KeyboardTests(BaseTestCase): - def test_hide_keyboard(self): +class TestKeyboard(BaseTestCase): + def test_hide_keyboard(self) -> None: self._move_to_textbox() el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] @@ -27,13 +26,13 @@ def test_hide_keyboard(self): el.set_value('Testing') el = self.driver.find_element_by_class_name('UIAKeyboard') - self.assertTrue(el.is_displayed()) + assert el.is_displayed() self.driver.hide_keyboard(key_name='Done') - self.assertFalse(el.is_displayed()) + assert not el.is_displayed() - def test_hide_keyboard_presskey_strategy(self): + def test_hide_keyboard_presskey_strategy(self) -> None: self._move_to_textbox() el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] @@ -41,13 +40,13 @@ def test_hide_keyboard_presskey_strategy(self): el.set_value('Testing') el = self.driver.find_element_by_class_name('UIAKeyboard') - self.assertTrue(el.is_displayed()) + assert el.is_displayed() self.driver.hide_keyboard(strategy='pressKey', key='Done') - self.assertFalse(el.is_displayed()) + assert not el.is_displayed() - def test_hide_keyboard_no_key_name(self): + def test_hide_keyboard_no_key_name(self) -> None: self._move_to_textbox() el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] @@ -55,31 +54,26 @@ def test_hide_keyboard_no_key_name(self): el.set_value('Testing') el = self.driver.find_element_by_class_name('UIAKeyboard') - self.assertTrue(el.is_displayed()) + assert el.is_displayed() self.driver.hide_keyboard() sleep(10) # currently fails. - self.assertFalse(el.is_displayed()) + assert not el.is_displayed() - def test_is_keyboard_shown(self): + def test_is_keyboard_shown(self) -> None: self._move_to_textbox() el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] el.click() el.set_value('Testing') - self.assertTrue(self.driver.is_keyboard_shown()) + assert self.driver.is_keyboard_shown() - def _move_to_textbox(self): + def _move_to_textbox(self) -> None: el1 = self.driver.find_element_by_accessibility_id('Sliders') el2 = self.driver.find_element_by_accessibility_id('Buttons') self.driver.scroll(el1, el2) # Click text fields self.driver.find_element_by_accessibility_id('Text Fields').click() - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(KeyboardTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/ios/remote_fs_tests.py b/test/functional/ios/remote_fs_tests.py index 4c24b39f..89b42568 100644 --- a/test/functional/ios/remote_fs_tests.py +++ b/test/functional/ios/remote_fs_tests.py @@ -13,21 +13,15 @@ # limitations under the License. import os -import unittest from test.functional.ios.helper.test_helper import BaseTestCase -class RemoteFsTests(BaseTestCase): +class TestRemoteFs(BaseTestCase): - def test_push_file(self): + def test_push_file(self) -> None: file_name = 'test_image.jpg' source_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', file_name) destination_path = file_name self.driver.push_file(destination_path, source_path=source_path) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(RemoteFsTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/ios/safari_tests.py b/test/functional/ios/safari_tests.py index a0b5255f..23d907fc 100644 --- a/test/functional/ios/safari_tests.py +++ b/test/functional/ios/safari_tests.py @@ -12,15 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from appium import webdriver from .helper.desired_capabilities import get_desired_capabilities -class SafariTests(unittest.TestCase): - def setUp(self): +class TestSafari(object): + def setup_method(self) -> None: desired_caps = get_desired_capabilities() desired_caps.update({ 'browserName': 'safari', @@ -30,19 +28,14 @@ def setUp(self): self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - def tearDown(self): + def teardown_method(self) -> None: self.driver.quit() - def test_context(self): - self.assertEqual('NATIVE_APP', self.driver.contexts[0]) - self.assertTrue(self.driver.contexts[1].startswith('WEBVIEW_')) - self.assertTrue('WEBVIEW_' in self.driver.current_context) + def test_context(self) -> None: + assert 'NATIVE_APP' == self.driver.contexts[0] + assert self.driver.contexts[1].startswith('WEBVIEW_') + assert 'WEBVIEW_' in self.driver.current_context - def test_get(self): + def test_get(self) -> None: self.driver.get("http://google.com") - self.assertEqual('Google', self.driver.title) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(SafariTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert 'Google' == self.driver.title diff --git a/test/functional/ios/screen_record_tests.py b/test/functional/ios/screen_record_tests.py index 7ad79af6..b821f6d3 100644 --- a/test/functional/ios/screen_record_tests.py +++ b/test/functional/ios/screen_record_tests.py @@ -12,20 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from time import sleep from test.functional.ios.helper.test_helper import BaseTestCase -class ScreenRecordTests(BaseTestCase): - def test_screen_record(self): +class TestScreenRecord(BaseTestCase): + def test_screen_record(self) -> None: self.driver.start_recording_screen() sleep(10) result = self.driver.stop_recording_screen() - self.assertTrue(len(result) > 0) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(ScreenRecordTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert len(result) > 0 diff --git a/test/functional/ios/search_context/find_by_element_webelement_tests.py b/test/functional/ios/search_context/find_by_element_webelement_tests.py index 3da2fc42..96d8ed91 100644 --- a/test/functional/ios/search_context/find_by_element_webelement_tests.py +++ b/test/functional/ios/search_context/find_by_element_webelement_tests.py @@ -12,27 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from test.functional.ios.helper.test_helper import BaseTestCase -class FindByElementWebelementTests(BaseTestCase): +class TestFindByElementWebelement(BaseTestCase): - def test_find_element_by_path(self): + def test_find_element_by_path(self) -> None: el = self.driver.find_element_by_ios_predicate('wdName == "UICatalog"') - self.assertEqual('UICatalog', el.get_attribute('name')) + assert 'UICatalog' == el.get_attribute('name') c_el = el.find_elements_by_ios_predicate('label == "Action Sheets"') - self.assertEqual('Action Sheets', c_el[0].get_attribute('name')) + assert 'Action Sheets' == c_el[0].get_attribute('name') c_el = el.find_elements_by_ios_class_chain('**/XCUIElementTypeStaticText') - self.assertEqual('UICatalog', c_el[0].get_attribute('name')) + assert 'UICatalog' == c_el[0].get_attribute('name') c_el = el.find_elements_by_accessibility_id('UICatalog') - self.assertEqual('UICatalog', c_el[0].get_attribute('name')) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(FindByElementWebelementTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert 'UICatalog' == c_el[0].get_attribute('name') diff --git a/test/functional/ios/search_context/find_by_ios_class_chain_tests.py b/test/functional/ios/search_context/find_by_ios_class_chain_tests.py index 12537ad3..7ff0a24d 100644 --- a/test/functional/ios/search_context/find_by_ios_class_chain_tests.py +++ b/test/functional/ios/search_context/find_by_ios_class_chain_tests.py @@ -12,24 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from test.functional.ios.helper.test_helper import BaseTestCase -class FindByIOClassChainTests(BaseTestCase): - def test_find_element_by_path(self): +class TestFindByIOClassChain(BaseTestCase): + def test_find_element_by_path(self) -> None: els = self.driver.find_elements_by_ios_class_chain('XCUIElementTypeWindow/**/XCUIElementTypeStaticText') - self.assertEqual(35, len(els)) - self.assertEqual('UICatalog', els[0].get_attribute('name')) + assert 35 == len(els) + assert 'UICatalog' == els[0].get_attribute('name') - def test_find_multiple_elements_by_path(self): + def test_find_multiple_elements_by_path(self) -> None: el = self.driver.find_elements_by_ios_class_chain('XCUIElementTypeWindow/*/*/*') - self.assertEqual(2, len(el)) - self.assertEqual('UICatalog', el[0].get_attribute('name')) - self.assertEqual(None, el[1].get_attribute('name')) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(FindByIOClassChainTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert 2 == len(el) + assert 'UICatalog' == el[0].get_attribute('name') + assert el[1].get_attribute('name') is None diff --git a/test/functional/ios/search_context/find_by_ios_predicate_tests.py b/test/functional/ios/search_context/find_by_ios_predicate_tests.py index 6890b6b5..41908479 100644 --- a/test/functional/ios/search_context/find_by_ios_predicate_tests.py +++ b/test/functional/ios/search_context/find_by_ios_predicate_tests.py @@ -12,45 +12,38 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from test.functional.ios.helper.test_helper import BaseTestCase -class FindByIOSPredicateTests(BaseTestCase): - def test_find_element_by_name(self): +class TestFindByIOSPredicate(BaseTestCase): + def test_find_element_by_name(self) -> None: # Will throw exception if element is not found self.driver.find_element_by_ios_predicate('wdName == "Buttons"') - def test_find_multiple_element_by_type(self): + def test_find_multiple_element_by_type(self) -> None: e = self.driver.find_elements_by_ios_predicate('wdType == "XCUIElementTypeStaticText"') - self.assertNotEqual(len(e), 0) + assert len(e) != 0 - def test_find_element_by_label(self): + def test_find_element_by_label(self) -> None: # Will throw exception if element is not found self.driver.find_element_by_ios_predicate('label == "Buttons"') - def test_find_element_by_value(self): + def test_find_element_by_value(self) -> None: # Will throw exception if element is not found self.driver.find_element_by_ios_predicate('wdValue == "Buttons"') - def test_find_element_by_isvisible(self): + def test_find_element_by_isvisible(self) -> None: # Will throw exception if element is not found self.driver.find_element_by_ios_predicate('wdValue == "Buttons" AND visible == 1') # Should not find any elements e = self.driver.find_elements_by_ios_predicate('wdValue == "Buttons" AND visible == 0') - self.assertEqual(len(e), 0) + assert len(e) == 0 - def test_find_element_by_isenabled(self): + def test_find_element_by_isenabled(self) -> None: # Will throw exception if element is not found self.driver.find_element_by_ios_predicate('wdValue == "Buttons" AND enabled == 1') # Should not find any elements e = self.driver.find_elements_by_ios_predicate('wdValue == "Buttons" AND enabled == 0') - self.assertEqual(len(e), 0) - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(FindByIOSPredicateTests) - unittest.TextTestRunner(verbosity=2).run(suite) + assert len(e) == 0 diff --git a/test/functional/ios/webdriver_tests.py b/test/functional/ios/webdriver_tests.py index e3672161..5f3cecbf 100644 --- a/test/functional/ios/webdriver_tests.py +++ b/test/functional/ios/webdriver_tests.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest +from typing import TYPE_CHECKING +import pytest from selenium.webdriver.support.ui import WebDriverWait from appium import webdriver @@ -24,22 +25,24 @@ from ..test_helper import is_ci from .helper import desired_capabilities +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver -class WebDriverTests(BaseTestCase): - def test_all_sessions(self): - if is_ci(): - # TODO Due to not created 2nd session somehow - self.skipTest('Need to fix flaky test during running on CI.') +class TestWebDriver(BaseTestCase): + + # TODO Due to not created 2nd session somehow + @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI.') + def test_all_sessions(self) -> None: port = get_available_from_port_range(8200, 8300) desired_caps = desired_capabilities.get_desired_capabilities('UICatalog.app.zip') desired_caps['deviceName'] = 'iPhone Xs Max' desired_caps['wdaLocalPort'] = port - class session_counts_is_two(object): + class session_counts_is_two: TIMEOUT = 10 - def __call__(self, driver): + def __call__(self, driver: 'WebDriver') -> bool: return len(driver.all_sessions) == 2 driver2 = None @@ -47,26 +50,23 @@ def __call__(self, driver): driver2 = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) WebDriverWait( driver2, session_counts_is_two.TIMEOUT).until(session_counts_is_two()) - self.assertEqual(2, len(self.driver.all_sessions)) + assert len(self.driver.all_sessions) == 2 finally: if driver2 is not None: driver2.quit() - def test_app_management(self): + def test_app_management(self) -> None: # this only works in Xcode9+ if float(desired_capabilities.get_desired_capabilities( desired_capabilities.BUNDLE_ID)['platformVersion']) < 11: return - self.assertEqual(self.driver.query_app_state(desired_capabilities.BUNDLE_ID), - ApplicationState.RUNNING_IN_FOREGROUND) + assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND self.driver.background_app(-1) - self.assertTrue(self.driver.query_app_state(desired_capabilities.BUNDLE_ID) < - ApplicationState.RUNNING_IN_FOREGROUND) + assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) < ApplicationState.RUNNING_IN_FOREGROUND self.driver.activate_app(desired_capabilities.BUNDLE_ID) - self.assertEqual(self.driver.query_app_state(desired_capabilities.BUNDLE_ID), - ApplicationState.RUNNING_IN_FOREGROUND) + assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND - def test_clear(self): + def test_clear(self) -> None: self._move_to_textbox() el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] @@ -74,7 +74,7 @@ def test_clear(self): # Verify default text def_text = 'Placeholder text' text = el.get_attribute('value') - self.assertEqual(text, def_text) + assert text == def_text # Input some text, verify input_text = 'blah' @@ -85,30 +85,24 @@ def test_clear(self): # TODO Needs to get the element again to update value in the element. Remove below one line when it's fixed. el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] text = el.get_attribute('value') - self.assertEqual(text, input_text) + assert text == input_text # Clear text, verify el.clear() text = el.get_attribute('value') - self.assertEqual(text, def_text) + assert text == def_text - def test_press_button(self): + def test_press_button(self) -> None: self.driver.press_button("Home") if float(desired_capabilities.get_desired_capabilities( desired_capabilities.BUNDLE_ID)['platformVersion']) < 11: return - self.assertEqual(self.driver.query_app_state(desired_capabilities.BUNDLE_ID), - ApplicationState.RUNNING_IN_FOREGROUND) + assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND - def _move_to_textbox(self): + def _move_to_textbox(self) -> None: el1 = self.driver.find_element_by_accessibility_id('Sliders') el2 = self.driver.find_element_by_accessibility_id('Buttons') self.driver.scroll(el1, el2) # Click text fields self.driver.find_element_by_accessibility_id('Text Fields').click() - - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(WebDriverTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/test_helper.py b/test/functional/test_helper.py index a531d053..ae55b386 100644 --- a/test/functional/test_helper.py +++ b/test/functional/test_helper.py @@ -6,25 +6,31 @@ class NoAvailablePortError(Exception): pass -def get_available_from_port_range(from_port, to_port): +def get_available_from_port_range(from_port: int, to_port: int) -> int: """Returns available local port number. + + Args: + from_port (int): The start port to search + to_port (int): The end port to search + + Returns: + int: available local port number which are found first + """ - r = range(from_port, to_port) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - for port in r: + for port in range(from_port, to_port): try: if sock.connect_ex(('localhost', port)) != 0: return port finally: sock.close() - raise NoAvailablePortError('No available port between {} and {}'.format( - from_port, to_port)) + raise NoAvailablePortError(f'No available port between {from_port} and {to_port}') -def is_ci(): +def is_ci() -> bool: """Returns if current execution is running on CI Returns: diff --git a/test/unit/helper/test_helper.py b/test/unit/helper/test_helper.py index f7ff3ca0..b33d3a92 100644 --- a/test/unit/helper/test_helper.py +++ b/test/unit/helper/test_helper.py @@ -13,6 +13,7 @@ # limitations under the License. import json +from typing import TYPE_CHECKING, Any, Dict import httpretty @@ -21,17 +22,21 @@ # :return: A string of test URL SERVER_URL_BASE = 'http://localhost:4723/wd/hub' +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + from httpretty.core import HTTPrettyRequestEmpty -def appium_command(command): + +def appium_command(command: str) -> str: """Return a command of Appium Returns: str: A string of command URL """ - return '{}{}'.format(SERVER_URL_BASE, command) + return f'{SERVER_URL_BASE}{command}' -def android_w3c_driver(): +def android_w3c_driver() -> 'WebDriver': """Return a W3C driver which is generated by a mock response for Android Returns: @@ -86,7 +91,7 @@ def android_w3c_driver(): return driver -def ios_w3c_driver(): +def ios_w3c_driver() -> 'WebDriver': """Return a W3C driver which is generated by a mock response for iOS Returns: @@ -127,6 +132,6 @@ def ios_w3c_driver(): return driver -def get_httpretty_request_body(request): +def get_httpretty_request_body(request: 'HTTPrettyRequestEmpty') -> Dict[str, Any]: """Returns utf-8 decoded request body""" return json.loads(request.body.decode('utf-8')) diff --git a/test/unit/webdriver/device/clipboard_test.py b/test/unit/webdriver/device/clipboard_test.py index 22c80dbe..01176029 100644 --- a/test/unit/webdriver/device/clipboard_test.py +++ b/test/unit/webdriver/device/clipboard_test.py @@ -14,7 +14,6 @@ import httpretty -from appium.common.helper import appium_bytes from appium.webdriver.clipboard_content_type import ClipboardContentType from test.unit.helper.test_helper import ( android_w3c_driver, @@ -34,7 +33,7 @@ def test_set_clipboard_with_url(self): appium_command('/session/1234567890/appium/device/set_clipboard'), body='{"value": ""}' ) - driver.set_clipboard(appium_bytes(str('http://appium.io/'), 'UTF-8'), + driver.set_clipboard(bytes(str('http://appium.io/'), 'UTF-8'), ClipboardContentType.URL, 'label for android') d = get_httpretty_request_body(httpretty.last_request()) diff --git a/test/unit/webdriver/device/remote_fs_test.py b/test/unit/webdriver/device/remote_fs_test.py index 0855b936..dace0ffc 100644 --- a/test/unit/webdriver/device/remote_fs_test.py +++ b/test/unit/webdriver/device/remote_fs_test.py @@ -18,7 +18,6 @@ import pytest from selenium.common.exceptions import InvalidArgumentException -from appium.common.helper import appium_bytes from appium.webdriver.webdriver import WebDriver from test.unit.helper.test_helper import ( android_w3c_driver, @@ -37,7 +36,7 @@ def test_push_file(self): appium_command('/session/1234567890/appium/device/push_file'), ) dest_path = '/path/to/file.txt' - data = base64.b64encode(appium_bytes('HelloWorld', 'utf-8')).decode('utf-8') + data = base64.b64encode(bytes('HelloWorld', 'utf-8')).decode('utf-8') assert isinstance(driver.push_file(dest_path, data), WebDriver) @@ -80,7 +79,7 @@ def test_pull_file(self): ) dest_path = '/path/to/file.txt' - assert driver.pull_file(dest_path) == str(base64.b64encode(appium_bytes('HelloWorld', 'utf-8')).decode('utf-8')) + assert driver.pull_file(dest_path) == str(base64.b64encode(bytes('HelloWorld', 'utf-8')).decode('utf-8')) d = get_httpretty_request_body(httpretty.last_request()) assert d['path'] == dest_path diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index 40e458d5..87e45d18 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -275,7 +275,7 @@ def exceptionCallback(request, uri, headers): class SubWebDriver(WebDriver): def __init__(self, command_executor, desired_capabilities, direct_connection=False): - super(SubWebDriver, self).__init__( + super().__init__( command_executor=command_executor, desired_capabilities=desired_capabilities, direct_connection=direct_connection @@ -284,7 +284,7 @@ def __init__(self, command_executor, desired_capabilities, direct_connection=Fal class SubSubWebDriver(SubWebDriver): def __init__(self, command_executor, desired_capabilities, direct_connection=False): - super(SubSubWebDriver, self).__init__( + super().__init__( command_executor=command_executor, desired_capabilities=desired_capabilities, direct_connection=direct_connection diff --git a/tox.ini b/tox.ini index c059a95e..85ce3975 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,8 @@ [tox] skipsdist = True envlist = - py27, - py34, - py35, - py36, py37, -# py38 # TODO Remove comment-out when pylint and astroid upgraded to the latest and py2 support dropped + py38 [testenv] deps =