diff --git a/README.md b/README.md index 543da044..1f49579c 100644 --- a/README.md +++ b/README.md @@ -154,11 +154,35 @@ desired_caps['app'] = PATH('../../apps/UICatalog.app.zip') self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) ``` - ## Changed or added functionality The methods that do change are... +### Direct Connect URLs + +If your Selenium/Appium server decorates the new session capabilities response with the following keys: + +- `directConnectProtocol` +- `directConnectHost` +- `directConnectPort` +- `directConnectPath` + +Then python client will switch its endpoint to the one specified by the values of those keys. + +```python +import unittest +from appium import webdriver + +desired_caps = {} +desired_caps['platformName'] = 'iOS' +desired_caps['platformVersion'] = '11.4' +desired_caps['automationName'] = 'xcuitest' +desired_caps['deviceName'] = 'iPhone Simulator' +desired_caps['app'] = PATH('../../apps/UICatalog.app.zip') + +self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, direct_connection=True) +``` + ### Switching between 'Native' and 'Webview' diff --git a/appium/common/logger.py b/appium/common/logger.py new file mode 100644 index 00000000..372d7fa8 --- /dev/null +++ b/appium/common/logger.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import sys + + +def setup_logger(level=logging.NOTSET): + logger.propagate = False + logger.setLevel(level) + handler = logging.StreamHandler(stream=sys.stderr) + logger.addHandler(handler) + + +# global logger +logger = logging.getLogger(__name__) +setup_logger() diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index a2d085c6..87f439e2 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -20,6 +20,8 @@ from selenium.common.exceptions import InvalidArgumentException from selenium.webdriver.common.by import By from selenium.webdriver.remote.command import Command as RemoteCommand +from selenium.webdriver.remote.remote_connection import RemoteConnection + from appium.webdriver.common.mobileby import MobileBy from .appium_connection import AppiumConnection @@ -43,6 +45,7 @@ from .switch_to import MobileSwitchTo from .webelement import WebElement as MobileWebElement +from appium.common.logger import logger # From remote/webdriver.py _W3C_CAPABILITY_NAMES = frozenset([ @@ -117,7 +120,7 @@ class WebDriver( ): def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', - desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=False): + desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=False, direct_connection=False): super(WebDriver, self).__init__( AppiumConnection(command_executor, keep_alive=keep_alive), @@ -126,12 +129,15 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', proxy ) - if self.command_executor is not None: + if hasattr(self, 'command_executor'): self._addCommands() self.error_handler = MobileErrorHandler() self._switch_to = MobileSwitchTo(self) + if direct_connection: + self._update_command_executor(keep_alive=keep_alive) + # add new method to the `find_by_*` pantheon By.IOS_UIAUTOMATION = MobileBy.IOS_UIAUTOMATION By.IOS_PREDICATE = MobileBy.IOS_PREDICATE @@ -142,6 +148,36 @@ 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): + """Update command executor following directConnect feature""" + direct_protocol = 'directConnectProtocol' + direct_host = 'directConnectHost' + direct_port = 'directConnectPort' + direct_path = 'directConnectPath' + + if (not {direct_protocol, direct_host, direct_port, direct_path}.issubset(set(self.capabilities))): + message = 'Direct connect capabilities from server were:\n' + for key in [direct_protocol, direct_host, direct_port, direct_path]: + message += '{}: \'{}\'\n'.format(key, self.capabilities.get(key, '')) + logger.warning(message) + return + + protocol = self.capabilities[direct_protocol] + 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 + ) + + 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): """ Override for Appium diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index 52cb0fe9..ccf55349 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -176,3 +176,78 @@ def test_find_elements_by_android_data_matcher_no_value(self): assert d['using'] == '-android datamatcher' assert d['value'] == '{}' assert len(els) == 0 + + @httpretty.activate + def test_create_session_register_uridirect(self): + httpretty.register_uri( + httpretty.POST, + 'http://localhost:4723/wd/hub/session', + body=json.dumps({'value': { + 'sessionId': 'session-id', + 'capabilities': { + 'deviceName': 'Android Emulator', + 'directConnectProtocol': 'http', + 'directConnectHost': 'localhost2', + 'directConnectPort': 4800, + 'directConnectPath': '/special/path/wd/hub', + } + }}) + ) + + httpretty.register_uri( + httpretty.GET, + 'http://localhost2:4800/special/path/wd/hub/session/session-id/contexts', + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}) + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2' + } + driver = webdriver.Remote( + 'http://localhost:4723/wd/hub', + desired_caps, + direct_connection=True + ) + + assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._url + assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts + + @httpretty.activate + def test_create_session_register_uridirect_no_direct_connect_path(self): + httpretty.register_uri( + httpretty.POST, + 'http://localhost:4723/wd/hub/session', + body=json.dumps({'value': { + 'sessionId': 'session-id', + 'capabilities': { + 'deviceName': 'Android Emulator', + 'directConnectProtocol': 'http', + 'directConnectHost': 'localhost2', + 'directConnectPort': 4800 + } + }}) + ) + + httpretty.register_uri( + httpretty.GET, + 'http://localhost:4723/wd/hub/session/session-id/contexts', + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}) + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2' + } + driver = webdriver.Remote( + 'http://localhost:4723/wd/hub', + desired_caps, + direct_connection=True + ) + + assert 'http://localhost:4723/wd/hub' == driver.command_executor._url + assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts