Skip to content

Commit

Permalink
[py] allow browser options to be passed to Remote WebDriver (#4876)
Browse files Browse the repository at this point in the history
This also deprecates the previous browser WebDriver keywords for options in favor of a standardized 'options'
I.E. 'firefox_options' becomes 'options'
  • Loading branch information
lmtierney authored Nov 8, 2017
1 parent e8fdbae commit fa164f6
Show file tree
Hide file tree
Showing 12 changed files with 92 additions and 47 deletions.
3 changes: 2 additions & 1 deletion py/selenium/webdriver/chrome/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@


class Options(object):
KEY = "goog:chromeOptions"

def __init__(self):
self._binary_location = ''
Expand Down Expand Up @@ -166,6 +167,6 @@ def to_capabilities(self):
if self.debugger_address:
chrome_options["debuggerAddress"] = self.debugger_address

chrome["goog:chromeOptions"] = chrome_options
chrome[self.KEY] = chrome_options

return chrome
18 changes: 12 additions & 6 deletions py/selenium/webdriver/chrome/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import warnings

from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
from .remote_connection import ChromeRemoteConnection
Expand All @@ -30,8 +31,9 @@ class WebDriver(RemoteWebDriver):
"""

def __init__(self, executable_path="chromedriver", port=0,
chrome_options=None, service_args=None,
desired_capabilities=None, service_log_path=None):
options=None, service_args=None,
desired_capabilities=None, service_log_path=None,
chrome_options=None):
"""
Creates a new instance of the chrome driver.
Expand All @@ -42,17 +44,21 @@ def __init__(self, executable_path="chromedriver", port=0,
- port - port you would like the service to run, if left as 0, a free port will be found.
- desired_capabilities: Dictionary object with non-browser specific
capabilities only, such as "proxy" or "loggingPref".
- chrome_options: this takes an instance of ChromeOptions
- options: this takes an instance of ChromeOptions
"""
if chrome_options is None:
if chrome_options:
warnings.warn('use options instead of chrome_options', DeprecationWarning)
options = chrome_options

if options is None:
# desired_capabilities stays as passed in
if desired_capabilities is None:
desired_capabilities = self.create_options().to_capabilities()
else:
if desired_capabilities is None:
desired_capabilities = chrome_options.to_capabilities()
desired_capabilities = options.to_capabilities()
else:
desired_capabilities.update(chrome_options.to_capabilities())
desired_capabilities.update(options.to_capabilities())

self.service = Service(
executable_path,
Expand Down
46 changes: 25 additions & 21 deletions py/selenium/webdriver/firefox/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import warnings

try:
import http.client as http_client
Expand Down Expand Up @@ -54,8 +55,8 @@ class WebDriver(RemoteWebDriver):

def __init__(self, firefox_profile=None, firefox_binary=None,
timeout=30, capabilities=None, proxy=None,
executable_path="geckodriver", firefox_options=None,
log_path="geckodriver.log"):
executable_path="geckodriver", options=None,
log_path="geckodriver.log", firefox_options=None):
"""Starts a new local session of Firefox.
Based on the combination and specificity of the various keyword
Expand All @@ -68,20 +69,20 @@ def __init__(self, firefox_profile=None, firefox_binary=None,
dictionary that is passed on to the remote end.
As some of the options, such as `firefox_profile` and
`firefox_options.profile` are mutually exclusive, precedence is
`options.profile` are mutually exclusive, precedence is
given from how specific the setting is. `capabilities` is the
least specific keyword argument, followed by `firefox_options`,
least specific keyword argument, followed by `options`,
followed by `firefox_binary` and `firefox_profile`.
In practice this means that if `firefox_profile` and
`firefox_options.profile` are both set, the selected profile
`options.profile` are both set, the selected profile
instance will always come from the most specific variable.
In this case that would be `firefox_profile`. This will result in
`firefox_options.profile` to be ignored because it is considered
`options.profile` to be ignored because it is considered
a less specific setting than the top-level `firefox_profile`
keyword argument. Similarily, if you had specified a
`capabilities["firefoxOptions"]["profile"]` Base64 string,
this would rank below `firefox_options.profile`.
`capabilities["moz:firefoxOptions"]["profile"]` Base64 string,
this would rank below `options.profile`.
:param firefox_profile: Instance of ``FirefoxProfile`` object
or a string. If undefined, a fresh profile will be created
Expand All @@ -97,43 +98,46 @@ def __init__(self, firefox_profile=None, firefox_binary=None,
:param executable_path: Full path to override which geckodriver
binary to use for Firefox 47.0.1 and greater, which
defaults to picking up the binary from the system path.
:param firefox_options: Instance of ``options.Options``.
:param options: Instance of ``options.Options``.
:param log_path: Where to log information from the driver.
"""
if firefox_options:
warnings.warn('use options instead of firefox_options', DeprecationWarning)
options = firefox_options
self.binary = None
self.profile = None
self.service = None

if capabilities is None:
capabilities = DesiredCapabilities.FIREFOX.copy()
if firefox_options is None:
firefox_options = Options()
if options is None:
options = Options()

capabilities = dict(capabilities)

if capabilities.get("binary"):
self.binary = capabilities["binary"]

# firefox_options overrides capabilities
if firefox_options is not None:
if firefox_options.binary is not None:
self.binary = firefox_options.binary
if firefox_options.profile is not None:
self.profile = firefox_options.profile
# options overrides capabilities
if options is not None:
if options.binary is not None:
self.binary = options.binary
if options.profile is not None:
self.profile = options.profile

# firefox_binary and firefox_profile
# override firefox_options
# override options
if firefox_binary is not None:
if isinstance(firefox_binary, basestring):
firefox_binary = FirefoxBinary(firefox_binary)
self.binary = firefox_binary
firefox_options.binary = firefox_binary
options.binary = firefox_binary
if firefox_profile is not None:
if isinstance(firefox_profile, basestring):
firefox_profile = FirefoxProfile(firefox_profile)
self.profile = firefox_profile
firefox_options.profile = firefox_profile
options.profile = firefox_profile

# W3C remote
# TODO(ato): Perform conformance negotiation
Expand All @@ -143,7 +147,7 @@ def __init__(self, firefox_profile=None, firefox_binary=None,
self.service = Service(executable_path, log_path=log_path)
self.service.start()

capabilities.update(firefox_options.to_capabilities())
capabilities.update(options.to_capabilities())

executor = FirefoxRemoteConnection(
remote_server_addr=self.service.service_url)
Expand Down
15 changes: 10 additions & 5 deletions py/selenium/webdriver/ie/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import warnings

from selenium.webdriver.common import utils
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
Expand All @@ -33,7 +34,8 @@ class WebDriver(RemoteWebDriver):

def __init__(self, executable_path='IEDriverServer.exe', capabilities=None,
port=DEFAULT_PORT, timeout=DEFAULT_TIMEOUT, host=DEFAULT_HOST,
log_level=DEFAULT_LOG_LEVEL, log_file=DEFAULT_LOG_FILE, ie_options=None):
log_level=DEFAULT_LOG_LEVEL, log_file=DEFAULT_LOG_FILE, options=None,
ie_options=None):
"""
Creates a new instance of the chrome driver.
Expand All @@ -45,24 +47,27 @@ def __init__(self, executable_path='IEDriverServer.exe', capabilities=None,
- port - port you would like the service to run, if left as 0, a free port will be found.
- log_level - log level you would like the service to run.
- log_file - log file you would like the service to log to.
- ie_options: IE Options instance, providing additional IE options
- options: IE Options instance, providing additional IE options
"""
if ie_options:
warnings.warn('use options instead of ie_options', DeprecationWarning)
options = ie_options
self.port = port
if self.port == 0:
self.port = utils.free_port()
self.host = host
self.log_level = log_level
self.log_file = log_file

if ie_options is None:
if options is None:
# desired_capabilities stays as passed in
if capabilities is None:
capabilities = self.create_options().to_capabilities()
else:
if capabilities is None:
capabilities = ie_options.to_capabilities()
capabilities = options.to_capabilities()
else:
capabilities.update(ie_options.to_capabilities())
capabilities.update(options.to_capabilities())

self.iedriver = Service(
executable_path,
Expand Down
5 changes: 3 additions & 2 deletions py/selenium/webdriver/opera/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


class Options(ChromeOptions):
KEY = "operaOptions"

def __init__(self):
ChromeOptions.__init__(self)
Expand Down Expand Up @@ -86,8 +87,8 @@ def to_capabilities(self):
"""
capabilities = ChromeOptions.to_capabilities(self)
capabilities.update(DesiredCapabilities.OPERA)
opera_options = capabilities["operaOptions"] = \
capabilities.pop("chromeOptions")
opera_options = capabilities[self.KEY] = \
capabilities.pop(ChromeOptions.KEY)

if self.android_package_name:
opera_options["androidPackage"] = self.android_package_name
Expand Down
17 changes: 11 additions & 6 deletions py/selenium/webdriver/opera/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import warnings

from selenium.webdriver.chrome.webdriver import WebDriver as ChromiumDriver
from .options import Options
Expand All @@ -24,8 +25,9 @@ class OperaDriver(ChromiumDriver):
to drive the Opera browser based on Chromium."""

def __init__(self, executable_path=None, port=0,
opera_options=None, service_args=None,
desired_capabilities=None, service_log_path=None):
options=None, service_args=None,
desired_capabilities=None, service_log_path=None,
opera_options=None):
"""
Creates a new instance of the operadriver.
Expand All @@ -38,15 +40,18 @@ def __init__(self, executable_path=None, port=0,
a free port will be found.
- desired_capabilities: Dictionary object with non-browser specific
capabilities only, such as "proxy" or "loggingPref".
- chrome_options: this takes an instance of ChromeOptions
- options: this takes an instance of ChromeOptions
"""
if opera_options:
warnings.warn('use options instead of opera_options', DeprecationWarning)
options = opera_options

executable_path = (executable_path if executable_path is not None
else "operadriver")
ChromiumDriver.__init__(self,
executable_path=executable_path,
port=port,
chrome_options=opera_options,
options=options,
service_args=service_args,
desired_capabilities=desired_capabilities,
service_log_path=service_log_path)
Expand All @@ -65,9 +70,9 @@ def __init__(self,
port=0,
service_log_path=None,
service_args=None,
opera_options=None):
options=None):
OperaDriver.__init__(self, executable_path=executable_path,
port=port, opera_options=opera_options,
port=port, options=options,
service_args=service_args,
desired_capabilities=desired_capabilities,
service_log_path=service_log_path)
5 changes: 4 additions & 1 deletion py/selenium/webdriver/remote/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class WebDriver(object):

def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
desired_capabilities=None, browser_profile=None, proxy=None,
keep_alive=False, file_detector=None):
keep_alive=False, file_detector=None, options=None):
"""
Create a new driver that will issue commands using the wire protocol.
Expand All @@ -128,6 +128,7 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
HTTP keep-alive. Defaults to False.
- file_detector - Pass custom file detector object during instantiation. If None,
then default LocalFileDetector() will be used.
- options - instance of a driver options.Options class
"""
if desired_capabilities is None:
raise WebDriverException("Desired Capabilities can't be None")
Expand All @@ -137,6 +138,8 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
warnings.warn("Please use FirefoxOptions to set proxy",
DeprecationWarning)
proxy.add_to_capabilities(desired_capabilities)
if options is not None:
desired_capabilities.update(options.to_capabilities())
self.command_executor = command_executor
if type(self.command_executor) is bytes or isinstance(self.command_executor, str):
self.command_executor = RemoteConnection(command_executor, keep_alive=keep_alive)
Expand Down
2 changes: 1 addition & 1 deletion py/test/selenium/webdriver/ie/ie_launcher_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def test_launch_ie_with_options(pages):
opts = Options()
expected = "clicks.html"
opts.initial_browser_url = pages.url(expected)
driver = Ie(ie_options=opts)
driver = Ie(options=opts)
actual = driver.current_url
driver.quit()
assert expected in actual
2 changes: 1 addition & 1 deletion py/test/selenium/webdriver/marionette/mn_options_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

@pytest.fixture
def driver_kwargs(driver_kwargs):
driver_kwargs['firefox_options'] = Options()
driver_kwargs['options'] = Options()
return driver_kwargs


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def driver_kwargs(request, driver_kwargs):
options = Options()
options.set_preference('browser.startup.homepage_override.mstone', '')
options.set_preference('startup.homepage_welcome_url', 'about:')
driver_kwargs['firefox_options'] = options
driver_kwargs['options'] = options
return driver_kwargs


Expand Down
4 changes: 2 additions & 2 deletions py/test/selenium/webdriver/marionette/mn_profile_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
from selenium.webdriver.firefox.options import Options


@pytest.fixture(params=['capabilities', 'firefox_profile', 'firefox_options'])
@pytest.fixture(params=['capabilities', 'firefox_profile', 'options'])
def driver_kwargs(request, driver_kwargs, profile):
if request.param == 'capabilities':
options = {'profile': profile}
driver_kwargs[request.param].setdefault('moz:firefoxOptions', options)
elif request.param == 'firefox_profile':
driver_kwargs[request.param] = profile
elif request.param == 'firefox_options':
elif request.param == 'options':
options = Options()
options.profile = profile
driver_kwargs[request.param] = options
Expand Down
20 changes: 20 additions & 0 deletions py/test/unit/selenium/webdriver/remote/test_new_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@


from copy import deepcopy
from importlib import import_module

import pytest

from selenium.webdriver import DesiredCapabilities
from selenium.webdriver.remote.command import Command
from selenium.webdriver.remote.webdriver import WebDriver

Expand All @@ -40,3 +44,19 @@ def test_converts_proxy_type_value_to_lowercase_for_w3c(mocker):
expected_params = {'capabilities': {'firstMatch': [{}], 'alwaysMatch': w3c_caps},
'desiredCapabilities': oss_caps}
mock.assert_called_with(Command.NEW_SESSION, expected_params)


@pytest.mark.parametrize('browser_name', ['firefox', 'chrome', 'ie', 'opera'])
def test_accepts_firefox_options_to_remote_driver(mocker, browser_name):
options = import_module('selenium.webdriver.{}.options'.format(browser_name))
caps_name = browser_name.upper() if browser_name != 'ie' else 'INTERNETEXPLORER'
mock = mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.start_session')

opts = options.Options()
opts.add_argument('foo')
expected_caps = getattr(DesiredCapabilities, caps_name)
caps = expected_caps.copy()
expected_caps.update(opts.to_capabilities())

WebDriver(desired_capabilities=caps, options=opts)
mock.assert_called_with(expected_caps, None)

0 comments on commit fa164f6

Please sign in to comment.