diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index af1dac1e..7bcfe3d3 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -29,6 +29,7 @@ jobs: XCODE_VERSION: 15.3 IOS_VERSION: 17.4 IPHONE_MODEL: iPhone 15 Plus + GLOBAL_DEFAULT_TIMEOUT: 600 steps: - uses: actions/checkout@v3 diff --git a/Pipfile b/Pipfile index 9bc2d4b3..a573df28 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,5 @@ tox = "~=4.23" types-python-dateutil = "~=2.9" [packages] -selenium = "==4.25" +selenium = "==4.26.1" typing-extensions = "~=4.12.2" diff --git a/README.md b/README.md index 4aa10444..24ad09d5 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz). |Appium Python Client| Selenium binding| Python version | |----|----|----| +|`4.3.0`+ |`4.26.0`+ | 3.8+ | |`3.0.0` - `4.2.1` |`4.12.0` - `4.25.0` | 3.8+ | |`2.10.0` - `2.11.1` |`4.1.0` - `4.11.2` | 3.7+ | |`2.2.0` - `2.9.0` |`4.1.0` - `4.9.0` | 3.7+ | @@ -311,6 +312,21 @@ options.set_capability('browser_name', 'safari') driver = webdriver.Remote('http://127.0.0.1:4723', options=options, strict_ssl=False) ``` +Since Appium Python client v4.3.0, we recommend using `selenium.webdriver.remote.client_config.ClientConfig` +instead of giving `strict_ssl` as an argument of `webdriver.Remote` below to configure the validation. + +```python +from appium import webdriver + +from selenium.webdriver.remote.client_config import ClientConfig + +client_config = ClientConfig( + remote_server_addr='http://127.0.0.1:4723', + ignore_certificates=True +) +driver = webdriver.Remote(client_config.remote_server_addr, options=options, client_config=client_config) +``` + ## Set custom `AppiumConnection` The first argument of `webdriver.Remote` can set an arbitrary command executor for you. @@ -364,6 +380,18 @@ driver = webdriver.Remote(custom_executor, options=options) ``` +The `AppiumConnection` can set `selenium.webdriver.remote.client_config.ClientConfig` as well. + +## Relaxing HTTP request read timeout + +Appium Python Client has `120` seconds read timeout on each HTTP request since the version v4.3.0 because of +the corresponding selenium binding version. +You have two methods to extend the read timeout. + +1. Set `GLOBAL_DEFAULT_TIMEOUT` environment variable +2. Configure timeout via `selenium.webdriver.remote.client_config.ClientConfig` + - `timeout` argument, or + - `init_args_for_pool_manager` argument for `urllib3.PoolManager` ## Documentation diff --git a/appium/webdriver/appium_connection.py b/appium/webdriver/appium_connection.py index e0527483..388f860d 100644 --- a/appium/webdriver/appium_connection.py +++ b/appium/webdriver/appium_connection.py @@ -13,9 +13,8 @@ # limitations under the License. import uuid -from typing import TYPE_CHECKING, Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict -import urllib3 from selenium.webdriver.remote.remote_connection import RemoteConnection from appium.common.helper import library_version @@ -26,55 +25,40 @@ PREFIX_HEADER = 'appium/' +HEADER_IDEMOTENCY_KEY = 'X-Idempotency-Key' -class AppiumConnection(RemoteConnection): - _proxy_url: Optional[str] - - def __init__( - self, - remote_server_addr: str, - keep_alive: bool = False, - ignore_proxy: Optional[bool] = False, - init_args_for_pool_manager: Union[Dict[str, Any], None] = None, - ): - # Need to call before super().__init__ in order to pass arguments for the pool manager in the super. - self._init_args_for_pool_manager = init_args_for_pool_manager or {} - - super().__init__(remote_server_addr, keep_alive=keep_alive, ignore_proxy=ignore_proxy) - - def _get_connection_manager(self) -> Union[urllib3.PoolManager, urllib3.ProxyManager]: - # https://github.com/SeleniumHQ/selenium/blob/0e0194b0e52a34e7df4b841f1ed74506beea5c3e/py/selenium/webdriver/remote/remote_connection.py#L134 - pool_manager_init_args = {'timeout': self.get_timeout()} - - if self._ca_certs: - pool_manager_init_args['cert_reqs'] = 'CERT_REQUIRED' - pool_manager_init_args['ca_certs'] = self._ca_certs - else: - # This line is necessary to disable certificate verification - pool_manager_init_args['cert_reqs'] = 'CERT_NONE' - pool_manager_init_args.update(self._init_args_for_pool_manager) +def _get_new_headers(key: str, headers: Dict[str, str]) -> Dict[str, str]: + """Return a new dictionary of heafers without the given key. + The key match is case-insensitive.""" + lower_key = key.lower() + return {k: v for k, v in headers.items() if k.lower() != lower_key} + - if self._proxy_url: - if self._proxy_url.lower().startswith('sock'): - from urllib3.contrib.socks import SOCKSProxyManager +class AppiumConnection(RemoteConnection): + """ + A subclass of selenium.webdriver.remote.remote_connection.Remoteconnection. - return SOCKSProxyManager(self._proxy_url, **pool_manager_init_args) - if self._identify_http_proxy_auth(): - self._proxy_url, self._basic_proxy_auth = self._separate_http_proxy_auth() - pool_manager_init_args['proxy_headers'] = urllib3.make_headers(proxy_basic_auth=self._basic_proxy_auth) - return urllib3.ProxyManager(self._proxy_url, **pool_manager_init_args) + The changes are: + - The default user agent + - Adds 'X-Idempotency-Key' header in a new session request to avoid proceeding + the same request multiple times in the Appium server side. + - https://github.com/appium/appium-base-driver/pull/400 + """ - return urllib3.PoolManager(**pool_manager_init_args) + user_agent = f'{PREFIX_HEADER}{library_version()} ({RemoteConnection.user_agent})' + extra_headers = {} @classmethod 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) - # e.g. appium/0.49 (selenium/3.141.0 (python linux)) - headers['User-Agent'] = f'{PREFIX_HEADER}{library_version()} ({headers["User-Agent"]})' + """Override get_remote_connection_headers in RemoteConnection to control the extra headers. + This method will be used in sending a request method in this class. + """ + if parsed_url.path.endswith('/session'): # https://github.com/appium/appium-base-driver/pull/400 - headers['X-Idempotency-Key'] = str(uuid.uuid4()) + cls.extra_headers[HEADER_IDEMOTENCY_KEY] = str(uuid.uuid4()) + else: + cls.extra_headers = _get_new_headers(HEADER_IDEMOTENCY_KEY, cls.extra_headers) - return headers + return {**super().get_remote_connection_headers(parsed_url, keep_alive=keep_alive), **cls.extra_headers} diff --git a/appium/webdriver/extensions/android/activities.py b/appium/webdriver/extensions/android/activities.py index 2e9ed68d..807b6a1f 100644 --- a/appium/webdriver/extensions/android/activities.py +++ b/appium/webdriver/extensions/android/activities.py @@ -56,9 +56,8 @@ def wait_activity(self, activity: str, timeout: int, interval: int = 1) -> bool: return False def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_CURRENT_ACTIVITY] = ( + self.command_executor.add_command( + Command.GET_CURRENT_ACTIVITY, 'GET', '/session/$sessionId/appium/device/current_activity', ) diff --git a/appium/webdriver/extensions/android/common.py b/appium/webdriver/extensions/android/common.py index b130bef1..fe75260b 100644 --- a/appium/webdriver/extensions/android/common.py +++ b/appium/webdriver/extensions/android/common.py @@ -47,13 +47,13 @@ def current_package(self) -> str: return self.mark_extension_absence(ext_name).execute(Command.GET_CURRENT_PACKAGE)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_CURRENT_PACKAGE] = ( + self.command_executor.add_command( + Command.GET_CURRENT_PACKAGE, 'GET', '/session/$sessionId/appium/device/current_package', ) - commands[Command.OPEN_NOTIFICATIONS] = ( + self.command_executor.add_command( + Command.OPEN_NOTIFICATIONS, 'POST', '/session/$sessionId/appium/device/open_notifications', ) diff --git a/appium/webdriver/extensions/android/display.py b/appium/webdriver/extensions/android/display.py index e04bd1be..28abdcbb 100644 --- a/appium/webdriver/extensions/android/display.py +++ b/appium/webdriver/extensions/android/display.py @@ -41,9 +41,8 @@ def get_display_density(self) -> int: return self.mark_extension_absence(ext_name).execute(Command.GET_DISPLAY_DENSITY)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_DISPLAY_DENSITY] = ( + self.command_executor.add_command( + 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 d41bc872..ed43d3c6 100644 --- a/appium/webdriver/extensions/android/gsm.py +++ b/appium/webdriver/extensions/android/gsm.py @@ -142,11 +142,6 @@ def set_gsm_voice(self, state: str) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.MAKE_GSM_CALL] = ('POST', '/session/$sessionId/appium/device/gsm_call') - commands[Command.SET_GSM_SIGNAL] = ( - 'POST', - '/session/$sessionId/appium/device/gsm_signal', - ) - commands[Command.SET_GSM_VOICE] = ('POST', '/session/$sessionId/appium/device/gsm_voice') + self.command_executor.add_command(Command.MAKE_GSM_CALL, 'POST', '/session/$sessionId/appium/device/gsm_call') + self.command_executor.add_command(Command.SET_GSM_SIGNAL, 'POST', '/session/$sessionId/appium/device/gsm_signal') + self.command_executor.add_command(Command.SET_GSM_VOICE, 'POST', '/session/$sessionId/appium/device/gsm_voice') diff --git a/appium/webdriver/extensions/android/network.py b/appium/webdriver/extensions/android/network.py index a60a98ac..6054e29d 100644 --- a/appium/webdriver/extensions/android/network.py +++ b/appium/webdriver/extensions/android/network.py @@ -157,18 +157,19 @@ def set_network_speed(self, speed_type: str) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.TOGGLE_WIFI] = ('POST', '/session/$sessionId/appium/device/toggle_wifi') - commands[Command.GET_NETWORK_CONNECTION] = ( + self.command_executor.add_command(Command.TOGGLE_WIFI, 'POST', '/session/$sessionId/appium/device/toggle_wifi') + self.command_executor.add_command( + Command.GET_NETWORK_CONNECTION, 'GET', '/session/$sessionId/network_connection', ) - commands[Command.SET_NETWORK_CONNECTION] = ( + self.command_executor.add_command( + Command.SET_NETWORK_CONNECTION, 'POST', '/session/$sessionId/network_connection', ) - commands[Command.SET_NETWORK_SPEED] = ( + self.command_executor.add_command( + Command.SET_NETWORK_SPEED, 'POST', '/session/$sessionId/appium/device/network_speed', ) diff --git a/appium/webdriver/extensions/android/performance.py b/appium/webdriver/extensions/android/performance.py index 0611c751..f5781cf3 100644 --- a/appium/webdriver/extensions/android/performance.py +++ b/appium/webdriver/extensions/android/performance.py @@ -73,13 +73,13 @@ def get_performance_data_types(self) -> List[str]: return self.mark_extension_absence(ext_name).execute(Command.GET_PERFORMANCE_DATA_TYPES)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_PERFORMANCE_DATA] = ( + self.command_executor.add_command( + Command.GET_PERFORMANCE_DATA, 'POST', '/session/$sessionId/appium/getPerformanceData', ) - commands[Command.GET_PERFORMANCE_DATA_TYPES] = ( + self.command_executor.add_command( + Command.GET_PERFORMANCE_DATA_TYPES, 'POST', '/session/$sessionId/appium/performanceData/types', ) diff --git a/appium/webdriver/extensions/android/power.py b/appium/webdriver/extensions/android/power.py index c881e2c5..537ab680 100644 --- a/appium/webdriver/extensions/android/power.py +++ b/appium/webdriver/extensions/android/power.py @@ -72,10 +72,9 @@ def set_power_ac(self, ac_state: str) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.SET_POWER_CAPACITY] = ( + self.command_executor.add_command( + Command.SET_POWER_CAPACITY, 'POST', '/session/$sessionId/appium/device/power_capacity', ) - commands[Command.SET_POWER_AC] = ('POST', '/session/$sessionId/appium/device/power_ac') + self.command_executor.add_command(Command.SET_POWER_AC, 'POST', '/session/$sessionId/appium/device/power_ac') diff --git a/appium/webdriver/extensions/android/sms.py b/appium/webdriver/extensions/android/sms.py index 753217f0..f5769c56 100644 --- a/appium/webdriver/extensions/android/sms.py +++ b/appium/webdriver/extensions/android/sms.py @@ -47,6 +47,4 @@ def send_sms(self, phone_number: str, message: str) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.SEND_SMS] = ('POST', '/session/$sessionId/appium/device/send_sms') + self.command_executor.add_command(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 d2fed74f..a02c21f8 100644 --- a/appium/webdriver/extensions/android/system_bars.py +++ b/appium/webdriver/extensions/android/system_bars.py @@ -51,9 +51,8 @@ def get_system_bars(self) -> Dict[str, Dict[str, Union[int, bool]]]: return self.mark_extension_absence(ext_name).execute(Command.GET_SYSTEM_BARS)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_SYSTEM_BARS] = ( + self.command_executor.add_command( + 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 fb718b72..b259422e 100644 --- a/appium/webdriver/extensions/applications.py +++ b/appium/webdriver/extensions/applications.py @@ -248,25 +248,27 @@ def app_strings(self, language: Union[str, None] = None, string_file: Union[str, return self.mark_extension_absence(ext_name).execute(Command.GET_APP_STRINGS, data)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.BACKGROUND] = ('POST', '/session/$sessionId/appium/app/background') - commands[Command.IS_APP_INSTALLED] = ( + self.command_executor.add_command(Command.BACKGROUND, 'POST', '/session/$sessionId/appium/app/background') + self.command_executor.add_command( + Command.IS_APP_INSTALLED, 'POST', '/session/$sessionId/appium/device/app_installed', ) - commands[Command.INSTALL_APP] = ('POST', '/session/$sessionId/appium/device/install_app') - commands[Command.REMOVE_APP] = ('POST', '/session/$sessionId/appium/device/remove_app') - commands[Command.TERMINATE_APP] = ( + self.command_executor.add_command(Command.INSTALL_APP, 'POST', '/session/$sessionId/appium/device/install_app') + self.command_executor.add_command(Command.REMOVE_APP, 'POST', '/session/$sessionId/appium/device/remove_app') + self.command_executor.add_command( + Command.TERMINATE_APP, 'POST', '/session/$sessionId/appium/device/terminate_app', ) - commands[Command.ACTIVATE_APP] = ( + self.command_executor.add_command( + Command.ACTIVATE_APP, 'POST', '/session/$sessionId/appium/device/activate_app', ) - commands[Command.QUERY_APP_STATE] = ( + self.command_executor.add_command( + Command.QUERY_APP_STATE, 'POST', '/session/$sessionId/appium/device/app_state', ) - commands[Command.GET_APP_STRINGS] = ('POST', '/session/$sessionId/appium/app/strings') + self.command_executor.add_command(Command.GET_APP_STRINGS, 'POST', '/session/$sessionId/appium/app/strings') diff --git a/appium/webdriver/extensions/clipboard.py b/appium/webdriver/extensions/clipboard.py index 2483de3e..f5354f2e 100644 --- a/appium/webdriver/extensions/clipboard.py +++ b/appium/webdriver/extensions/clipboard.py @@ -95,13 +95,13 @@ def get_clipboard_text(self) -> str: return self.get_clipboard(ClipboardContentType.PLAINTEXT).decode('UTF-8') def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.SET_CLIPBOARD] = ( + self.command_executor.add_command( + Command.SET_CLIPBOARD, 'POST', '/session/$sessionId/appium/device/set_clipboard', ) - commands[Command.GET_CLIPBOARD] = ( + self.command_executor.add_command( + Command.GET_CLIPBOARD, 'POST', '/session/$sessionId/appium/device/get_clipboard', ) diff --git a/appium/webdriver/extensions/context.py b/appium/webdriver/extensions/context.py index 356604fe..628432e1 100644 --- a/appium/webdriver/extensions/context.py +++ b/appium/webdriver/extensions/context.py @@ -58,8 +58,6 @@ def context(self) -> str: return self.current_context def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.CONTEXTS] = ('GET', '/session/$sessionId/contexts') - commands[Command.GET_CURRENT_CONTEXT] = ('GET', '/session/$sessionId/context') - commands[Command.SWITCH_TO_CONTEXT] = ('POST', '/session/$sessionId/context') + self.command_executor.add_command(Command.CONTEXTS, 'GET', '/session/$sessionId/contexts') + self.command_executor.add_command(Command.GET_CURRENT_CONTEXT, 'GET', '/session/$sessionId/context') + self.command_executor.add_command(Command.SWITCH_TO_CONTEXT, 'POST', '/session/$sessionId/context') diff --git a/appium/webdriver/extensions/device_time.py b/appium/webdriver/extensions/device_time.py index 62de92b4..22ac3c25 100644 --- a/appium/webdriver/extensions/device_time.py +++ b/appium/webdriver/extensions/device_time.py @@ -63,13 +63,13 @@ def get_device_time(self, format: Optional[str] = None) -> str: return self.mark_extension_absence(ext_name).execute(Command.GET_DEVICE_TIME_POST, {'format': format})['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_DEVICE_TIME_GET] = ( + self.command_executor.add_command( + Command.GET_DEVICE_TIME_GET, 'GET', '/session/$sessionId/appium/device/system_time', ) - commands[Command.GET_DEVICE_TIME_POST] = ( + self.command_executor.add_command( + Command.GET_DEVICE_TIME_POST, 'POST', '/session/$sessionId/appium/device/system_time', ) diff --git a/appium/webdriver/extensions/execute_driver.py b/appium/webdriver/extensions/execute_driver.py index fcfff282..c1625522 100644 --- a/appium/webdriver/extensions/execute_driver.py +++ b/appium/webdriver/extensions/execute_driver.py @@ -57,6 +57,4 @@ def __init__(self, res: Dict): return Result(response) def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.EXECUTE_DRIVER] = ('POST', '/session/$sessionId/appium/execute_driver') + self.command_executor.add_command(Command.EXECUTE_DRIVER, 'POST', '/session/$sessionId/appium/execute_driver') diff --git a/appium/webdriver/extensions/hw_actions.py b/appium/webdriver/extensions/hw_actions.py index ca9fc4de..b6bbb468 100644 --- a/appium/webdriver/extensions/hw_actions.py +++ b/appium/webdriver/extensions/hw_actions.py @@ -132,18 +132,18 @@ def finger_print(self, finger_id: int) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.LOCK] = ('POST', '/session/$sessionId/appium/device/lock') - commands[Command.UNLOCK] = ('POST', '/session/$sessionId/appium/device/unlock') - commands[Command.IS_LOCKED] = ('POST', '/session/$sessionId/appium/device/is_locked') - commands[Command.SHAKE] = ('POST', '/session/$sessionId/appium/device/shake') - commands[Command.TOUCH_ID] = ('POST', '/session/$sessionId/appium/simulator/touch_id') - commands[Command.TOGGLE_TOUCH_ID_ENROLLMENT] = ( + self.command_executor.add_command(Command.LOCK, 'POST', '/session/$sessionId/appium/device/lock') + self.command_executor.add_command(Command.UNLOCK, 'POST', '/session/$sessionId/appium/device/unlock') + self.command_executor.add_command(Command.IS_LOCKED, 'POST', '/session/$sessionId/appium/device/is_locked') + self.command_executor.add_command(Command.SHAKE, 'POST', '/session/$sessionId/appium/device/shake') + self.command_executor.add_command(Command.TOUCH_ID, 'POST', '/session/$sessionId/appium/simulator/touch_id') + self.command_executor.add_command( + Command.TOGGLE_TOUCH_ID_ENROLLMENT, 'POST', '/session/$sessionId/appium/simulator/toggle_touch_id_enrollment', ) - commands[Command.FINGER_PRINT] = ( + self.command_executor.add_command( + Command.FINGER_PRINT, 'POST', '/session/$sessionId/appium/device/finger_print', ) diff --git a/appium/webdriver/extensions/images_comparison.py b/appium/webdriver/extensions/images_comparison.py index 6913c3fd..6f730815 100644 --- a/appium/webdriver/extensions/images_comparison.py +++ b/appium/webdriver/extensions/images_comparison.py @@ -129,6 +129,4 @@ def get_images_similarity(self, base64_image1: bytes, base64_image2: bytes, **op return self.execute(Command.COMPARE_IMAGES, options)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.COMPARE_IMAGES] = ('POST', '/session/$sessionId/appium/compare_images') + self.command_executor.add_command(Command.COMPARE_IMAGES, 'POST', '/session/$sessionId/appium/compare_images') diff --git a/appium/webdriver/extensions/keyboard.py b/appium/webdriver/extensions/keyboard.py index fe833ef7..4640b116 100644 --- a/appium/webdriver/extensions/keyboard.py +++ b/appium/webdriver/extensions/keyboard.py @@ -145,22 +145,24 @@ def long_press_keycode(self, keycode: int, metastate: Optional[int] = None, flag return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.HIDE_KEYBOARD] = ( + self.command_executor.add_command( + Command.HIDE_KEYBOARD, 'POST', '/session/$sessionId/appium/device/hide_keyboard', ) - commands[Command.IS_KEYBOARD_SHOWN] = ( + self.command_executor.add_command( + Command.IS_KEYBOARD_SHOWN, 'GET', '/session/$sessionId/appium/device/is_keyboard_shown', ) - commands[Command.KEY_EVENT] = ('POST', '/session/$sessionId/appium/device/keyevent') - commands[Command.PRESS_KEYCODE] = ( + self.command_executor.add_command(Command.KEY_EVENT, 'POST', '/session/$sessionId/appium/device/keyevent') + self.command_executor.add_command( + Command.PRESS_KEYCODE, 'POST', '/session/$sessionId/appium/device/press_keycode', ) - commands[Command.LONG_PRESS_KEYCODE] = ( + self.command_executor.add_command( + Command.LONG_PRESS_KEYCODE, 'POST', '/session/$sessionId/appium/device/long_press_keycode', ) diff --git a/appium/webdriver/extensions/location.py b/appium/webdriver/extensions/location.py index 93a75ca7..3141050a 100644 --- a/appium/webdriver/extensions/location.py +++ b/appium/webdriver/extensions/location.py @@ -89,11 +89,10 @@ def location(self) -> Dict[str, float]: def _add_commands(self) -> None: """Add location endpoints. They are not int w3c spec.""" - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.TOGGLE_LOCATION_SERVICES] = ( + self.command_executor.add_command( + Command.TOGGLE_LOCATION_SERVICES, 'POST', '/session/$sessionId/appium/device/toggle_location_services', ) - commands[Command.GET_LOCATION] = ('GET', '/session/$sessionId/location') - commands[Command.SET_LOCATION] = ('POST', '/session/$sessionId/location') + self.command_executor.add_command(Command.GET_LOCATION, 'GET', '/session/$sessionId/location') + self.command_executor.add_command(Command.SET_LOCATION, 'POST', '/session/$sessionId/location') diff --git a/appium/webdriver/extensions/log_event.py b/appium/webdriver/extensions/log_event.py index 8bf6932f..4ca53b3f 100644 --- a/appium/webdriver/extensions/log_event.py +++ b/appium/webdriver/extensions/log_event.py @@ -64,7 +64,5 @@ def log_event(self, vendor: str, event: str) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_EVENTS] = ('POST', '/session/$sessionId/appium/events') - commands[Command.LOG_EVENT] = ('POST', '/session/$sessionId/appium/log_event') + self.command_executor.add_command(Command.GET_EVENTS, 'POST', '/session/$sessionId/appium/events') + self.command_executor.add_command(Command.LOG_EVENT, 'POST', '/session/$sessionId/appium/log_event') diff --git a/appium/webdriver/extensions/remote_fs.py b/appium/webdriver/extensions/remote_fs.py index e7a7fa1e..5ccebd37 100644 --- a/appium/webdriver/extensions/remote_fs.py +++ b/appium/webdriver/extensions/remote_fs.py @@ -105,8 +105,6 @@ def push_file(self, destination_path: str, base64data: Optional[str] = None, sou return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.PULL_FILE] = ('POST', '/session/$sessionId/appium/device/pull_file') - commands[Command.PULL_FOLDER] = ('POST', '/session/$sessionId/appium/device/pull_folder') - commands[Command.PUSH_FILE] = ('POST', '/session/$sessionId/appium/device/push_file') + self.command_executor.add_command(Command.PULL_FILE, 'POST', '/session/$sessionId/appium/device/pull_file') + self.command_executor.add_command(Command.PULL_FOLDER, 'POST', '/session/$sessionId/appium/device/pull_folder') + self.command_executor.add_command(Command.PUSH_FILE, 'POST', '/session/$sessionId/appium/device/push_file') diff --git a/appium/webdriver/extensions/screen_record.py b/appium/webdriver/extensions/screen_record.py index 53c97811..11e26c41 100644 --- a/appium/webdriver/extensions/screen_record.py +++ b/appium/webdriver/extensions/screen_record.py @@ -195,13 +195,13 @@ def stop_recording_screen(self, **options: Any) -> bytes: return self.execute(Command.STOP_RECORDING_SCREEN, {'options': options})['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember - commands = self.command_executor._commands - commands[Command.START_RECORDING_SCREEN] = ( + self.command_executor.add_command( + Command.START_RECORDING_SCREEN, 'POST', '/session/$sessionId/appium/start_recording_screen', ) - commands[Command.STOP_RECORDING_SCREEN] = ( + self.command_executor.add_command( + Command.STOP_RECORDING_SCREEN, 'POST', '/session/$sessionId/appium/stop_recording_screen', ) diff --git a/appium/webdriver/extensions/session.py b/appium/webdriver/extensions/session.py index 8960066d..e0bcd8b6 100644 --- a/appium/webdriver/extensions/session.py +++ b/appium/webdriver/extensions/session.py @@ -38,6 +38,4 @@ def events(self) -> Dict: return {} def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_SESSION] = ('GET', '/session/$sessionId') + self.command_executor.add_command(Command.GET_SESSION, 'GET', '/session/$sessionId') diff --git a/appium/webdriver/extensions/settings.py b/appium/webdriver/extensions/settings.py index 1a5fb9ee..d9a55111 100644 --- a/appium/webdriver/extensions/settings.py +++ b/appium/webdriver/extensions/settings.py @@ -45,7 +45,5 @@ def update_settings(self, settings: Dict[str, Any]) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_SETTINGS] = ('GET', '/session/$sessionId/appium/settings') - commands[Command.UPDATE_SETTINGS] = ('POST', '/session/$sessionId/appium/settings') + self.command_executor.add_command(Command.GET_SETTINGS, 'GET', '/session/$sessionId/appium/settings') + self.command_executor.add_command(Command.UPDATE_SETTINGS, 'POST', '/session/$sessionId/appium/settings') diff --git a/appium/webdriver/locator_converter.py b/appium/webdriver/locator_converter.py new file mode 100644 index 00000000..4c6416c6 --- /dev/null +++ b/appium/webdriver/locator_converter.py @@ -0,0 +1,29 @@ +#!/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. + +from typing import Tuple + +from selenium.webdriver.remote.locator_converter import LocatorConverter + + +class AppiumLocatorConverter(LocatorConverter): + """A custom locator converter in Appium. + + Appium supports locators which are not defined in W3C WebDriver, + so Appium Python client wants to keep the given locators + to the Appium server as-is. + """ + + def convert(self, by: str, value: str) -> Tuple[str, str]: + return (by, value) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index d292c0ac..200d5f91 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -22,6 +22,7 @@ WebDriverException, ) from selenium.webdriver.common.by import By +from selenium.webdriver.remote.client_config import ClientConfig from selenium.webdriver.remote.command import Command as RemoteCommand from selenium.webdriver.remote.remote_connection import RemoteConnection from typing_extensions import Self @@ -57,6 +58,7 @@ from .extensions.screen_record import ScreenRecord from .extensions.session import Session from .extensions.settings import Settings +from .locator_converter import AppiumLocatorConverter from .mobilecommand import MobileCommand as Command from .switch_to import MobileSwitchTo from .webelement import WebElement as MobileWebElement @@ -200,7 +202,7 @@ class WebDriver( Sms, SystemBars, ): - def __init__( + def __init__( # noqa: PLR0913 self, command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4444/wd/hub', keep_alive: bool = True, @@ -208,24 +210,26 @@ def __init__( extensions: Optional[List['WebDriver']] = None, strict_ssl: bool = True, options: Union[AppiumOptions, List[AppiumOptions], None] = None, + client_config: Optional[ClientConfig] = None, ): - if strict_ssl is False: - # noinspection PyPackageRequirements - import urllib3 - - # noinspection PyPackageRequirements - import urllib3.exceptions - - # noinspection PyUnresolvedReferences - AppiumConnection.set_certificate_bundle_path(None) - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - if isinstance(command_executor, str): - command_executor = AppiumConnection(command_executor, keep_alive=keep_alive) + client_config = client_config or ClientConfig( + remote_server_addr=command_executor, keep_alive=keep_alive, ignore_certificates=not strict_ssl + ) + client_config.remote_server_addr = command_executor + command_executor = AppiumConnection(remote_server_addr=command_executor, client_config=client_config) + elif isinstance(command_executor, AppiumConnection) and strict_ssl is False: + logger.warning( + "Please set 'ignore_certificates' in the given 'appium.webdriver.appium_connection.AppiumConnection' or " + "'selenium.webdriver.remote.client_config.ClientConfig' instead. Ignoring." + ) super().__init__( command_executor=command_executor, options=options, + locator_converter=AppiumLocatorConverter(), + web_element_cls=MobileWebElement, + client_config=client_config, ) if hasattr(self, 'command_executor'): @@ -257,8 +261,7 @@ def __init__( # add a new method named 'instance.method_name()' and call it setattr(WebDriver, method_name, getattr(instance, method_name)) method, url_cmd = instance.add_command() - # noinspection PyProtectedMember - self.command_executor._commands[method_name] = (method.upper(), url_cmd) # type: ignore + self.command_executor.add_command(method_name, method.upper(), url_cmd) def delete_extensions(self) -> None: """Delete extensions added in the class with 'setattr'""" @@ -346,72 +349,6 @@ def get_status(self) -> Dict: """ return self.execute(Command.GET_STATUS)['value'] - def find_element(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = None) -> MobileWebElement: - """ - Find an element given a AppiumBy strategy and locator - - Args: - by: The strategy - value: The locator - - Usage: - driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='accessibility_id') - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - - """ - # We prefer to patch locators in the client code - # Checking current context every time a locator is accessed could significantly slow down tests - # Check https://github.com/appium/python-client/pull/724 before submitting any issue - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - return self.execute(RemoteCommand.FIND_ELEMENT, {'using': by, 'value': value})['value'] - - def find_elements(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = None) -> Union[List[MobileWebElement], List]: - """ - Find elements given a AppiumBy strategy and locator - - Args: - by: The strategy - value: The locator - - Usage: - driver.find_elements(by=AppiumBy.ACCESSIBILITY_ID, value='accessibility_id') - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - """ - # We prefer to patch locators in the client code - # Checking current context every time a locator is accessed could significantly slow down tests - # Check https://github.com/appium/python-client/pull/724 before submitting any issue - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - # Return empty list if driver returns null - # See https://github.com/SeleniumHQ/selenium/issues/4555 - - return self.execute(RemoteCommand.FIND_ELEMENTS, {'using': by, 'value': value})['value'] or [] - def create_web_element(self, element_id: Union[int, str]) -> MobileWebElement: """Creates a web element with the specified element_id. @@ -503,31 +440,29 @@ def _add_commands(self) -> None: if get_atter: get_atter(self) - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - - commands[Command.GET_STATUS] = ('GET', '/status') + self.command_executor.add_command(Command.GET_STATUS, 'GET', '/status') # FIXME: remove after a while as MJSONWP - commands[Command.TOUCH_ACTION] = ('POST', '/session/$sessionId/touch/perform') - commands[Command.MULTI_ACTION] = ('POST', '/session/$sessionId/touch/multi/perform') + self.command_executor.add_command(Command.TOUCH_ACTION, 'POST', '/session/$sessionId/touch/perform') + self.command_executor.add_command(Command.MULTI_ACTION, 'POST', '/session/$sessionId/touch/multi/perform') # TODO Move commands for element to webelement - commands[Command.CLEAR] = ('POST', '/session/$sessionId/element/$id/clear') - commands[Command.LOCATION_IN_VIEW] = ( + self.command_executor.add_command(Command.CLEAR, 'POST', '/session/$sessionId/element/$id/clear') + self.command_executor.add_command( + Command.LOCATION_IN_VIEW, 'GET', '/session/$sessionId/element/$id/location_in_view', ) # MJSONWP for Selenium v4 - commands[Command.IS_ELEMENT_DISPLAYED] = ('GET', '/session/$sessionId/element/$id/displayed') - commands[Command.GET_CAPABILITIES] = ('GET', '/session/$sessionId') + self.command_executor.add_command(Command.IS_ELEMENT_DISPLAYED, 'GET', '/session/$sessionId/element/$id/displayed') + self.command_executor.add_command(Command.GET_CAPABILITIES, 'GET', '/session/$sessionId') - commands[Command.GET_SCREEN_ORIENTATION] = ('GET', '/session/$sessionId/orientation') - commands[Command.SET_SCREEN_ORIENTATION] = ('POST', '/session/$sessionId/orientation') + self.command_executor.add_command(Command.GET_SCREEN_ORIENTATION, 'GET', '/session/$sessionId/orientation') + self.command_executor.add_command(Command.SET_SCREEN_ORIENTATION, 'POST', '/session/$sessionId/orientation') # override for Appium 1.x # Appium 2.0 and Appium 1.22 work with `/se/log` and `/se/log/types` # FIXME: remove after a while - commands[Command.GET_LOG] = ('POST', '/session/$sessionId/log') - commands[Command.GET_AVAILABLE_LOG_TYPES] = ('GET', '/session/$sessionId/log/types') + self.command_executor.add_command(Command.GET_LOG, 'POST', '/session/$sessionId/log') + self.command_executor.add_command(Command.GET_AVAILABLE_LOG_TYPES, 'GET', '/session/$sessionId/log/types') diff --git a/appium/webdriver/webelement.py b/appium/webdriver/webelement.py index 4fb98e5d..14a9c9db 100644 --- a/appium/webdriver/webelement.py +++ b/appium/webdriver/webelement.py @@ -12,15 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Callable, Dict, List, Optional, Union +from typing import Callable, Dict, Optional, Union from selenium.webdriver.common.utils import keys_to_typing from selenium.webdriver.remote.command import Command as RemoteCommand from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement from typing_extensions import Self -from appium.webdriver.common.appiumby import AppiumBy - from .mobilecommand import MobileCommand as Command @@ -80,70 +78,6 @@ def is_displayed(self) -> bool: """ return self._execute(Command.IS_ELEMENT_DISPLAYED)['value'] - def find_element(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = None) -> 'WebElement': - """Find an element given a AppiumBy strategy and locator - - Override for Appium - - Prefer the find_element_by_* methods when possible. - - Args: - by: The strategy - value: The locator - - Usage: - element = element.find_element(AppiumBy.ID, 'foo') - - Returns: - `appium.webdriver.webelement.WebElement` - """ - # We prefer to patch locators in the client code - # Checking current context every time a locator is accessed could significantly slow down tests - # Check https://github.com/appium/python-client/pull/724 before submitting any issue - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - return self._execute(RemoteCommand.FIND_CHILD_ELEMENT, {'using': by, 'value': value})['value'] - - def find_elements(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = None) -> List['WebElement']: - """Find elements given a AppiumBy strategy and locator - - Args: - by: The strategy - value: The locator - - Usage: - element = element.find_elements(AppiumBy.CLASS_NAME, 'foo') - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement` - """ - # We prefer to patch locators in the client code - # Checking current context every time a locator is accessed could significantly slow down tests - # Check https://github.com/appium/python-client/pull/724 before submitting any issue - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - return self._execute(RemoteCommand.FIND_CHILD_ELEMENTS, {'using': by, 'value': value})['value'] - def clear(self) -> Self: """Clears text. diff --git a/setup.py b/setup.py index 254d4e02..35ce5b9f 100644 --- a/setup.py +++ b/setup.py @@ -49,5 +49,5 @@ 'Topic :: Software Development :: Quality Assurance', 'Topic :: Software Development :: Testing', ], - install_requires=['selenium ~= 4.12, < 4.26'], + install_requires=['selenium ~= 4.26, < 5.0'], ) diff --git a/test/unit/webdriver/appium_connection_test.py b/test/unit/webdriver/appium_connection_test.py new file mode 100644 index 00000000..c59303e7 --- /dev/null +++ b/test/unit/webdriver/appium_connection_test.py @@ -0,0 +1,39 @@ +import unittest +from urllib import parse + +from appium.webdriver import appium_connection + + +class AppiumConnectionTest(unittest.TestCase): + def test_get_remote_connection_headers(self): + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http://127.0.0.1:4723/session') + ) + self.assertIsNotNone(headers.get('X-Idempotency-Key')) + + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http://127.0.0.1:4723/session/session_id') + ) + self.assertIsNone(headers.get('X-Idempotency-Key')) + + appium_connection.AppiumConnection.extra_headers = {'custom': 'header'} + + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http://127.0.0.1:4723/session') + ) + self.assertIsNotNone(headers.get('X-Idempotency-Key')) + self.assertEqual(headers.get('custom'), 'header') + + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http://127.0.0.1:4723/session/session_id') + ) + self.assertIsNone(headers.get('X-Idempotency-Key')) + self.assertEqual(headers.get('custom'), 'header') + + def test_remove_headers_case_insensitive(self): + for h in ['X-Idempotency-Key', 'X-idempotency-Key', 'x-idempotency-key']: + appium_connection.AppiumConnection.extra_headers = {h: 'value'} + appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http://127.0.0.1:4723/session/session_id') + ) + self.assertEqual(appium_connection.AppiumConnection.extra_headers, {}) diff --git a/test/unit/webdriver/search_context/android_test.py b/test/unit/webdriver/search_context/android_test.py index 8076ad7f..879d78f5 100644 --- a/test/unit/webdriver/search_context/android_test.py +++ b/test/unit/webdriver/search_context/android_test.py @@ -22,6 +22,62 @@ class TestWebDriverAndroidSearchContext(object): + @httpretty.activate + def test_find_element_by_id(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element( + by=AppiumBy.ID, + value='id data', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'id data' + assert isinstance(el, MobileWebElement) + + @httpretty.activate + def test_find_elements_by_id(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}', + ) + els = driver.find_elements( + by=AppiumBy.ID, + value='id data', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'id data' + assert isinstance(els[0], MobileWebElement) + + @httpretty.activate + def test_find_child_element_by_id(self): + driver = android_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "child-element-id"}}', + ) + el = element.find_element( + by=AppiumBy.ID, + value='id data', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'id data' + assert isinstance(el, MobileWebElement) + @httpretty.activate def test_find_element_by_android_data_matcher(self): driver = android_w3c_driver() diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index f9a07a04..47a4353e 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -18,7 +18,6 @@ import urllib3 from mock import patch -from appium import version as appium_version from appium import webdriver from appium.options.android import UiAutomator2Options from appium.webdriver.appium_connection import AppiumConnection @@ -54,7 +53,8 @@ def test_create_session(self): request = httpretty.HTTPretty.latest_requests[0] assert request.headers['content-type'] == 'application/json;charset=UTF-8' - assert f'appium/{appium_version.version} (selenium' in request.headers['user-agent'] + assert request.headers['user-agent'].startswith('appium/') + assert '(selenium/' in request.headers['user-agent'] request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) assert request_json.get('capabilities') is not None @@ -130,7 +130,7 @@ def test_create_session_register_uridirect(self): direct_connection=True, ) - assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._url + assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._client_config.remote_server_addr assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts assert isinstance(driver.command_executor, AppiumConnection) @@ -170,7 +170,7 @@ def test_create_session_register_uridirect_no_direct_connect_path(self): direct_connection=True, ) - assert SERVER_URL_BASE == driver.command_executor._url + assert SERVER_URL_BASE == driver.command_executor._client_config.remote_server_addr assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts assert isinstance(driver.command_executor, AppiumConnection) @@ -303,7 +303,8 @@ class CustomAppiumConnection(AppiumConnection): request = httpretty.HTTPretty.latest_requests[0] assert request.headers['content-type'] == 'application/json;charset=UTF-8' - assert f'appium/{appium_version.version} (selenium' in request.headers['user-agent'] + assert request.headers['user-agent'].startswith('appium/') + assert '(selenium/' in request.headers['user-agent'] request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) assert request_json.get('capabilities') is not None @@ -347,7 +348,8 @@ class CustomAppiumConnection(AppiumConnection): request = httpretty.HTTPretty.latest_requests[0] assert request.headers['content-type'] == 'application/json;charset=UTF-8' - assert f'appium/{appium_version.version} (selenium' in request.headers['user-agent'] + assert request.headers['user-agent'].startswith('appium/') + assert '(selenium/' in request.headers['user-agent'] request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) assert request_json.get('capabilities') is not None