diff --git a/appium/options/__init__.py b/appium/options/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/appium/options/android/__init__.py b/appium/options/android/__init__.py new file mode 100644 index 000000000..f03a4e7a0 --- /dev/null +++ b/appium/options/android/__init__.py @@ -0,0 +1 @@ +from .uiautomator2.base import UiAutomator2Options diff --git a/appium/options/android/uiautomator2/__init__.py b/appium/options/android/uiautomator2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/appium/options/android/uiautomator2/base.py b/appium/options/android/uiautomator2/base.py new file mode 100644 index 000000000..d067d78f1 --- /dev/null +++ b/appium/options/android/uiautomator2/base.py @@ -0,0 +1,31 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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 Dict + +from appium.options.common.base import AppiumOptions + + +class UiAutomator2Options( + AppiumOptions, +): + @property + def default_capabilities(self) -> Dict: + return { + AppiumOptions.AUTOMATION_NAME: 'UIAutomator2', + AppiumOptions.PLATFORM_NAME: 'Android', + } diff --git a/appium/options/common/__init__.py b/appium/options/common/__init__.py new file mode 100644 index 000000000..a7a37ea6f --- /dev/null +++ b/appium/options/common/__init__.py @@ -0,0 +1 @@ +from .base import AppiumOptions diff --git a/appium/options/common/automation_name_option.py b/appium/options/common/automation_name_option.py new file mode 100644 index 000000000..b52250608 --- /dev/null +++ b/appium/options/common/automation_name_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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 Optional + +from .supports_capabilities import SupportsCapabilities + + +class AutomationNameOption(SupportsCapabilities): + AUTOMATION_NAME = 'automationName' + + @property + def automation_name(self) -> Optional[str]: + """ + :Returns: String representing the name of the automation engine + """ + return self.get_capability(self.AUTOMATION_NAME) + + @automation_name.setter + def automation_name(self, value: str) -> None: + """ + Set the automation driver to use. + + :Args: + - value: One of supported automation names + + """ + self.set_capability(self.AUTOMATION_NAME, value) diff --git a/appium/options/common/base.py b/appium/options/common/base.py new file mode 100644 index 000000000..f7245d257 --- /dev/null +++ b/appium/options/common/base.py @@ -0,0 +1,110 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import copy +from typing import Any, Dict, TypeVar + +from selenium.webdriver.common.options import BaseOptions + +from .automation_name_option import AutomationNameOption + +APPIUM_PREFIX = 'appium:' +T = TypeVar('T', bound='AppiumOptions') + + +class AppiumOptions( + BaseOptions, + AutomationNameOption, +): + _caps: Dict + PLATFORM_NAME = 'platformName' + W3C_CAPABILITY_NAMES = frozenset( + [ + 'acceptInsecureCerts', + 'browserName', + 'browserVersion', + PLATFORM_NAME, + 'pageLoadStrategy', + 'proxy', + 'setWindowRect', + 'timeouts', + 'unhandledPromptBehavior', + ] + ) + _OSS_W3C_CONVERSION = { + 'acceptSslCerts': 'acceptInsecureCerts', + 'version': 'browserVersion', + 'platform': PLATFORM_NAME, + } + + # noinspection PyMissingConstructor + def __init__(self) -> None: + self._caps = self.default_capabilities + # FIXME: https://github.com/SeleniumHQ/selenium/issues/10755 + self._ignore_local_proxy = False + + def set_capability(self, name: str, value: Any) -> T: + w3c_name = name if name in self.W3C_CAPABILITY_NAMES or ':' in name else f'{APPIUM_PREFIX}{name}' + if value is None: + if w3c_name in self._caps: + del self._caps[w3c_name] + else: + self._caps[w3c_name] = value + return self + + def get_capability(self, name: str) -> Any: + """Fetches capability value or None if the capability is not set""" + return self._caps[name] if name in self._caps else self._caps.get(f'{APPIUM_PREFIX}{name}') + + def load_capabilities(self, caps: Dict[str, Any]) -> T: + """Sets multiple capabilities""" + for name, value in caps.items(): + self.set_capability(name, value) + return self + + @staticmethod + def as_w3c(capabilities: Dict) -> Dict: + """ + Formats given capabilities to a valid W3C session request object + + :param capabilities: Capabilities mapping + :return: W3C session request object + """ + + def process_key(k: str) -> str: + key = AppiumOptions._OSS_W3C_CONVERSION.get(k, k) + if key in AppiumOptions.W3C_CAPABILITY_NAMES: + return key + return key if ':' in key else f'{APPIUM_PREFIX}{key}' + + processed_caps = {process_key(k): v for k, v in copy.deepcopy(capabilities).items()} + return {'capabilities': {'firstMatch': [{}], 'alwaysMatch': processed_caps}} + + def to_w3c(self) -> Dict: + """ + Formats the instance to a valid W3C session request object + + :return: W3C session request object + """ + return self.as_w3c(self.to_capabilities()) + + def to_capabilities(self) -> Dict: + return copy.copy(self._caps) + + @property + def default_capabilities(self) -> Dict: + return {} diff --git a/appium/options/common/supports_capabilities.py b/appium/options/common/supports_capabilities.py new file mode 100644 index 000000000..b56f153ca --- /dev/null +++ b/appium/options/common/supports_capabilities.py @@ -0,0 +1,30 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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 Any, TypeVar + +from appium.protocols.protocol import Protocol + +T = TypeVar('T') + + +class SupportsCapabilities(Protocol): + def set_capability(self: T, name: str, value: Any) -> T: + ... + + def get_capability(self: T, name: str) -> Any: + ... diff --git a/appium/options/ios/__init__.py b/appium/options/ios/__init__.py new file mode 100644 index 000000000..cc2b162d6 --- /dev/null +++ b/appium/options/ios/__init__.py @@ -0,0 +1 @@ +from .xcuitest.base import XCUITestOptions diff --git a/appium/options/ios/xcuitest/__init__.py b/appium/options/ios/xcuitest/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/appium/options/ios/xcuitest/base.py b/appium/options/ios/xcuitest/base.py new file mode 100644 index 000000000..9718dfec3 --- /dev/null +++ b/appium/options/ios/xcuitest/base.py @@ -0,0 +1,31 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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 Dict + +from appium.options.common.base import AppiumOptions + + +class XCUITestOptions( + AppiumOptions, +): + @property + def default_capabilities(self) -> Dict: + return { + AppiumOptions.AUTOMATION_NAME: 'XCUITest', + AppiumOptions.PLATFORM_NAME: 'iOS', + } diff --git a/appium/options/mac/__init__.py b/appium/options/mac/__init__.py new file mode 100644 index 000000000..41a091df5 --- /dev/null +++ b/appium/options/mac/__init__.py @@ -0,0 +1 @@ +from .mac2.base import Mac2Options diff --git a/appium/options/mac/mac2/__init__.py b/appium/options/mac/mac2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/appium/options/mac/mac2/base.py b/appium/options/mac/mac2/base.py new file mode 100644 index 000000000..7c1ebc700 --- /dev/null +++ b/appium/options/mac/mac2/base.py @@ -0,0 +1,31 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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 Dict + +from appium.options.common.base import AppiumOptions + + +class Mac2Options( + AppiumOptions, +): + @property + def default_capabilities(self) -> Dict: + return { + AppiumOptions.AUTOMATION_NAME: 'Mac2', + AppiumOptions.PLATFORM_NAME: 'Mac', + } diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 7694365e6..16257df4b 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -14,7 +14,6 @@ # pylint: disable=too-many-lines,too-many-public-methods,too-many-statements,no-self-use -import copy from typing import Any, Callable, Dict, List, Optional, Tuple, Union from selenium import webdriver @@ -24,6 +23,7 @@ from selenium.webdriver.remote.remote_connection import RemoteConnection from appium.common.logger import logger +from appium.options.common.base import AppiumOptions from appium.webdriver.common.appiumby import AppiumBy from .appium_connection import AppiumConnection @@ -59,56 +59,6 @@ from .switch_to import MobileSwitchTo from .webelement import WebElement as MobileWebElement -# From remote/webdriver.py -_W3C_CAPABILITY_NAMES = frozenset( - [ - 'acceptInsecureCerts', - 'browserName', - 'browserVersion', - 'platformName', - 'pageLoadStrategy', - 'proxy', - 'setWindowRect', - 'timeouts', - 'unhandledPromptBehavior', - ] -) - -# From remote/webdriver.py -_OSS_W3C_CONVERSION = {'acceptSslCerts': 'acceptInsecureCerts', 'version': 'browserVersion', 'platform': 'platformName'} - -_EXTENSION_CAPABILITY = ':' - -# override -# Add appium prefix for the MJSONWP capabilities - - -def _make_w3c_caps(caps: Dict) -> Dict[str, Union[Dict[str, Any], List[Dict[str, Any]]]]: - appium_prefix = 'appium:' - - caps = copy.deepcopy(caps) - profile = caps.get('firefox_profile') - always_match = {} - if caps.get('proxy') and caps['proxy'].get('proxyType'): - caps['proxy']['proxyType'] = caps['proxy']['proxyType'].lower() - for k, v in caps.items(): - if v and k in _OSS_W3C_CONVERSION: - always_match[_OSS_W3C_CONVERSION[k]] = v.lower() if k == 'platform' else v - if k in _W3C_CAPABILITY_NAMES or _EXTENSION_CAPABILITY in k: - always_match[k] = v - else: - if not k.startswith(appium_prefix): - always_match[appium_prefix + k] = v - if profile: - moz_opts = always_match.get('moz:firefoxOptions', {}) - # If it's already present, assume the caller did that intentionally. - if 'profile' not in moz_opts: - # Don't mutate the original capabilities. - new_opts = copy.deepcopy(moz_opts) - new_opts['profile'] = profile - always_match['moz:firefoxOptions'] = new_opts - return {'alwaysMatch': always_match, 'firstMatch': [{}]} - class ExtensionBase: """ @@ -260,6 +210,7 @@ def __init__( direct_connection: bool = True, extensions: Optional[List['WebDriver']] = None, strict_ssl: bool = True, + options: Union[AppiumOptions, List[AppiumOptions]] = None, ): if strict_ssl is False: @@ -267,12 +218,20 @@ def __init__( # noinspection PyPackageRequirements import urllib3 + # pylint: disable=E1101 + # noinspection PyPackageRequirements + import urllib3.exceptions + # noinspection PyUnresolvedReferences AppiumConnection.set_certificate_bundle_path(None) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) super().__init__( - AppiumConnection(command_executor, keep_alive=keep_alive), desired_capabilities, browser_profile, proxy + command_executor=AppiumConnection(command_executor, keep_alive=keep_alive), + desired_capabilities=desired_capabilities, + browser_profile=browser_profile, + proxy=proxy, + options=options, ) if hasattr(self, 'command_executor'): @@ -343,45 +302,25 @@ def _update_command_executor(self, keep_alive: bool) -> None: # https://github.com/SeleniumHQ/selenium/blob/06fdf2966df6bca47c0ae45e8201cd30db9b9a49/py/selenium/webdriver/remote/webdriver.py#L277 # noinspection PyAttributeOutsideInit - def start_session(self, capabilities: Dict, browser_profile: Optional[str] = None) -> None: + def start_session(self, capabilities: Union[Dict, AppiumOptions], browser_profile: Optional[str] = None) -> None: """Creates a new session with the desired capabilities. Override for Appium Args: - capabilities: Capabilities which have following keys like 'automation_name', 'platform_name', - 'platform_version', 'app'. - Read https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md for more details. + capabilities: Read https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md + for more details. browser_profile: Browser profile """ - if not isinstance(capabilities, dict): - raise InvalidArgumentException('Capabilities must be a dictionary') - if browser_profile: - if 'moz:firefoxOptions' in capabilities: - # encoded is defined in selenium's original codes - capabilities['moz:firefoxOptions']['profile'] = browser_profile.encoded # type: ignore - else: - # encoded is defined in selenium's original codes - capabilities.update({'firefox_profile': browser_profile.encoded}) # type: ignore - - parameters = self._merge_capabilities(capabilities) - - response = self.execute(RemoteCommand.NEW_SESSION, parameters) + if not isinstance(capabilities, (dict, AppiumOptions)): + raise InvalidArgumentException('Capabilities must be a dictionary or AppiumOptions instance') + + w3c_caps = AppiumOptions.as_w3c(capabilities) if isinstance(capabilities, dict) else capabilities.to_w3c() + response = self.execute(RemoteCommand.NEW_SESSION, w3c_caps) if 'sessionId' not in response: response = response['value'] self.session_id = response['sessionId'] - self.caps = response.get('value') - - # if capabilities is none we are probably speaking to - # a W3C endpoint - if self.caps is None: - self.caps = response.get('capabilities') - - # noinspection PyMethodMayBeStatic - def _merge_capabilities(self, capabilities: Dict) -> Dict[str, Any]: - """Manage capabilities whether W3C format or MJSONWP format""" - w3c_caps = _make_w3c_caps(capabilities) - return {'capabilities': w3c_caps, 'desiredCapabilities': capabilities} + self.caps = response.get('value') or response.get('capabilities') def find_element(self, by: str = AppiumBy.ID, value: Union[str, Dict] = None) -> MobileWebElement: """ @@ -552,7 +491,7 @@ def _add_commands(self) -> None: '/session/$sessionId/element/$id/location_in_view', ) - ## MJSONWP for Selenium v4 + # MJSONWP for Selenium v4 commands[Command.IS_ELEMENT_DISPLAYED] = ('GET', '/session/$sessionId/element/$id/displayed') commands[Command.GET_CAPABILITIES] = ('GET', '/session/$sessionId') diff --git a/test/functional/android/helper/test_helper.py b/test/functional/android/helper/test_helper.py index 1f726f55e..1e94d34db 100644 --- a/test/functional/android/helper/test_helper.py +++ b/test/functional/android/helper/test_helper.py @@ -15,17 +15,13 @@ import base64 import os -from typing import TYPE_CHECKING from appium import webdriver +from appium.options.android import UiAutomator2Options from test.functional.test_helper import is_ci from . import desired_capabilities -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - from appium.webdriver.webelement import WebElement - # the emulator is sometimes slow and needs time to think SLEEPY_TIME = 10 @@ -36,11 +32,16 @@ class BaseTestCase: 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) + self.driver = webdriver.Remote( + 'http://localhost:4723/wd/hub', options=UiAutomator2Options().load_capabilities(desired_caps) + ) if is_ci(): self.driver.start_recording_screen() def teardown_method(self, method) -> None: # type: ignore + if not hasattr(self, 'driver'): + return + if is_ci(): payload = self.driver.stop_recording_screen() video_path = os.path.join(os.getcwd(), method.__name__ + '.mp4') diff --git a/test/functional/ios/helper/test_helper.py b/test/functional/ios/helper/test_helper.py index c9a4da868..3491aa25f 100644 --- a/test/functional/ios/helper/test_helper.py +++ b/test/functional/ios/helper/test_helper.py @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. - import base64 import os from appium import webdriver +from appium.options.ios import XCUITestOptions from test.functional.test_helper import is_ci from . import desired_capabilities @@ -25,11 +25,16 @@ class BaseTestCase(object): 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) + self.driver = webdriver.Remote( + 'http://localhost:4723/wd/hub', options=XCUITestOptions().load_capabilities(desired_caps) + ) if is_ci(): self.driver.start_recording_screen() def teardown_method(self, method) -> None: # type: ignore + if not hasattr(self, 'driver'): + return + if is_ci(): payload = self.driver.stop_recording_screen() video_path = os.path.join(os.getcwd(), method.__name__ + '.mp4') diff --git a/test/functional/mac/helper/test_helper.py b/test/functional/mac/helper/test_helper.py index f52bc9aa9..5232d0641 100644 --- a/test/functional/mac/helper/test_helper.py +++ b/test/functional/mac/helper/test_helper.py @@ -14,13 +14,19 @@ from appium import webdriver +from appium.options.mac import Mac2Options from .desired_capabilities import get_desired_capabilities class BaseTestCase(object): def setup_method(self) -> None: - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', get_desired_capabilities()) + self.driver = webdriver.Remote( + 'http://localhost:4723/wd/hub', options=Mac2Options().load_capabilities(get_desired_capabilities()) + ) def teardown_method(self, method) -> None: # type: ignore + if not hasattr(self, 'driver'): + return + self.driver.quit() diff --git a/test/unit/helper/test_helper.py b/test/unit/helper/test_helper.py index 9766e1b76..b0a818e9b 100644 --- a/test/unit/helper/test_helper.py +++ b/test/unit/helper/test_helper.py @@ -18,6 +18,8 @@ import httpretty from appium import webdriver +from appium.options.android import UiAutomator2Options +from appium.options.ios import XCUITestOptions # :return: A string of test URL SERVER_URL_BASE = 'http://localhost:4723/wd/hub' @@ -81,7 +83,7 @@ def android_w3c_driver() -> 'WebDriver': 'automationName': 'UIAutomator2', } - driver = webdriver.Remote(SERVER_URL_BASE, desired_caps) + driver = webdriver.Remote(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps)) return driver @@ -115,7 +117,7 @@ def ios_w3c_driver() -> 'WebDriver': 'automationName': 'XCUITest', } - driver = webdriver.Remote(SERVER_URL_BASE, desired_caps) + driver = webdriver.Remote(SERVER_URL_BASE, options=XCUITestOptions().load_capabilities(desired_caps)) return driver @@ -149,7 +151,9 @@ def ios_w3c_driver_with_extensions(extensions) -> 'WebDriver': 'automationName': 'XCUITest', } - driver = webdriver.Remote(SERVER_URL_BASE, desired_caps, extensions=extensions) + driver = webdriver.Remote( + SERVER_URL_BASE, options=XCUITestOptions().load_capabilities(desired_caps), extensions=extensions + ) return driver diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index 6565277aa..00be6cbdf 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -15,11 +15,11 @@ import json import httpretty -import pytest from mock import patch from appium import version as appium_version from appium import webdriver +from appium.options.android import UiAutomator2Options from appium.webdriver.webdriver import ExtensionBase, WebDriver from test.unit.helper.test_helper import ( android_w3c_driver, @@ -57,7 +57,13 @@ def test_create_session(self): request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) assert request_json.get('capabilities') is not None - assert request_json.get('desiredCapabilities') is not None + assert request_json['capabilities']['alwaysMatch'] == { + 'platformName': 'Android', + 'appium:deviceName': 'Android Emulator', + 'appium:app': 'path/to/app', + 'appium:automationName': 'UIAutomator2', + } + assert request_json.get('desiredCapabilities') is None assert driver.session_id == 'session-id' @@ -75,13 +81,10 @@ def test_create_session_change_session_id(self): body='{ "value": "title on another session id"}', ) - desired_caps = { - 'platformName': 'Android', - 'deviceName': 'Android Emulator', - 'app': 'path/to/app', - 'automationName': 'UIAutomator2', - } - driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) + options = ( + UiAutomator2Options().set_capability('deviceName', 'Android Emulator').set_capability('app', 'path/to/app') + ) + driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options) # current session assert driver.session_id == 'session-id' @@ -124,7 +127,11 @@ def test_create_session_register_uridirect(self): 'app': 'path/to/app', 'automationName': 'UIAutomator2', } - driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, direct_connection=True) + driver = webdriver.Remote( + 'http://localhost:4723/wd/hub', + options=UiAutomator2Options().load_capabilities(desired_caps), + direct_connection=True, + ) assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._url assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts @@ -161,7 +168,11 @@ def test_create_session_register_uridirect_no_direct_connect_path(self): 'app': 'path/to/app', 'automationName': 'UIAutomator2', } - driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, direct_connection=True) + driver = webdriver.Remote( + 'http://localhost:4723/wd/hub', + options=UiAutomator2Options().load_capabilities(desired_caps), + direct_connection=True, + ) assert 'http://localhost:4723/wd/hub' == driver.command_executor._url assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts @@ -231,7 +242,7 @@ def test_command(self): return self.execute()['value'] def add_command(self): - return ('get', 'session/$sessionId/path/to/custom/url') + return 'get', 'session/$sessionId/path/to/custom/url' driver = ios_w3c_driver_with_extensions([CustomURLCommand]) httpretty.register_uri( @@ -254,7 +265,7 @@ def test_command(self, argument): return self.execute(argument)['value'] def add_command(self): - return ('post', 'session/$sessionId/path/to/custom/url') + return 'post', 'session/$sessionId/path/to/custom/url' driver = ios_w3c_driver_with_extensions([CustomURLCommand]) httpretty.register_uri( @@ -280,7 +291,7 @@ def test_command(self, element_id): return self.execute({'id': element_id})['value'] def add_command(self): - return ('GET', 'session/$sessionId/path/to/custom/$id/url') + return 'GET', 'session/$sessionId/path/to/custom/$id/url' driver = ios_w3c_driver_with_extensions([CustomURLCommand]) httpretty.register_uri( @@ -294,20 +305,22 @@ def add_command(self): class SubWebDriver(WebDriver): - def __init__(self, command_executor, desired_capabilities, direct_connection=False): + def __init__(self, command_executor, desired_capabilities=None, direct_connection=False, options=None): super().__init__( command_executor=command_executor, desired_capabilities=desired_capabilities, direct_connection=direct_connection, + options=options, ) class SubSubWebDriver(SubWebDriver): - def __init__(self, command_executor, desired_capabilities, direct_connection=False): + def __init__(self, command_executor, desired_capabilities=None, direct_connection=False, options=None): super().__init__( command_executor=command_executor, desired_capabilities=desired_capabilities, direct_connection=direct_connection, + options=options, ) @@ -350,7 +363,9 @@ def android_w3c_driver(self, driver_class): 'automationName': 'UIAutomator2', } - driver = driver_class('http://localhost:4723/wd/hub', desired_caps) + driver = driver_class( + 'http://localhost:4723/wd/hub', options=UiAutomator2Options().load_capabilities(desired_caps) + ) return driver @httpretty.activate