Skip to content

Commit 493827a

Browse files
authored
Merge pull request #247 from seleniumbase/remote-desired-capabilities
Remote desired capabilities
2 parents 20ec154 + fa33c1f commit 493827a

File tree

11 files changed

+263
-23
lines changed

11 files changed

+263
-23
lines changed

examples/capabilities/ReadMe.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
### Using Desired Capabilities
2+
3+
You can specify browser desired capabilities for webdriver when running SeleniumBase tests on a remote SeleniumGrid server such as [BrowserStack](https://www.browserstack.com/automate/capabilities), [Sauce Labs](https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/), or [TestingBot](https://testingbot.com/support/other/test-options).
4+
5+
A sample run command may look like this: (When run from the ``SeleniumBase/examples/`` folder):
6+
7+
```bash
8+
pytest my_first_test.py --browser=remote --server=username:[email protected] --port=80 --cap_file=capabilities/sample_cap_file_BS.py
9+
```
10+
11+
A regex parser was built into SeleniumBase to capture all lines from the specified desired capabilities file in the following formats:
12+
``'KEY': 'VALUE'``
13+
``caps['KEY'] = "VALUE"``
14+
(Each pair must be on a separate line. You can interchange single and double quotes.)
15+
16+
You can also swap ``--browser=remote`` with an actual browser, eg ``--browser=chrome``, which will combine the default SeleniumBase desired capabilities with those that were specified in the capabilities file when using ``--cap_file=FILE.py``. (For example, you'll need default SeleniumBase desired capabilities when using a proxy server, which is not the same as the Selenium Grid server.)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Desired capabilities example file for BrowserStack
2+
# Generate from https://www.browserstack.com/automate/capabilities
3+
desired_cap = {
4+
'os': 'OS X',
5+
'os_version': 'Sierra',
6+
'browser': 'Chrome',
7+
'browser_version': '70.0',
8+
'browserstack.local': 'false',
9+
'browserstack.selenium_version': '3.14.0',
10+
'browserstack.chrome.driver': '2.43'
11+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Desired capabilities example file for Sauce Labs
2+
# Generate from https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/
3+
caps = {}
4+
caps['browserName'] = "chrome"
5+
caps['platform'] = "macOS 10.12"
6+
caps['version'] = "70.0"

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ ipdb
77
chardet
88
unittest2
99
selenium==3.141.0
10-
requests==2.20.0
10+
requests==2.20.1
1111
urllib3==1.24.1
1212
pytest>=4.0.0
1313
pytest-cov>=2.6.0

seleniumbase/core/browser_launcher.py

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from seleniumbase.config import proxy_list
1212
from seleniumbase.core import download_helper
1313
from seleniumbase.core import proxy_helper
14+
from seleniumbase.core import capabilities_parser
1415
from seleniumbase.fixtures import constants
1516
from seleniumbase.fixtures import page_utils
1617
from seleniumbase import drivers # webdriver storage folder for SeleniumBase
@@ -187,7 +188,8 @@ def validate_proxy_string(proxy_string):
187188

188189

189190
def get_driver(browser_name, headless=False, use_grid=False,
190-
servername='localhost', port=4444, proxy_string=None):
191+
servername='localhost', port=4444, proxy_string=None,
192+
cap_file=None):
191193
proxy_auth = False
192194
proxy_user = None
193195
proxy_pass = None
@@ -216,7 +218,7 @@ def get_driver(browser_name, headless=False, use_grid=False,
216218
if use_grid:
217219
return get_remote_driver(
218220
browser_name, headless, servername, port, proxy_string, proxy_auth,
219-
proxy_user, proxy_pass)
221+
proxy_user, proxy_pass, cap_file)
220222
else:
221223
return get_local_driver(
222224
browser_name, headless, proxy_string, proxy_auth,
@@ -225,20 +227,31 @@ def get_driver(browser_name, headless=False, use_grid=False,
225227

226228
def get_remote_driver(
227229
browser_name, headless, servername, port, proxy_string, proxy_auth,
228-
proxy_user, proxy_pass):
230+
proxy_user, proxy_pass, cap_file):
229231
downloads_path = download_helper.get_downloads_folder()
230232
download_helper.reset_downloads_folder()
231233
address = "http://%s:%s/wd/hub" % (servername, port)
234+
desired_caps = {}
235+
if cap_file:
236+
desired_caps = capabilities_parser.get_desired_capabilities(cap_file)
232237

233238
if browser_name == constants.Browser.GOOGLE_CHROME:
234239
chrome_options = _set_chrome_options(
235240
downloads_path, proxy_string, proxy_auth,
236241
proxy_user, proxy_pass)
237242
if headless:
238-
chrome_options.add_argument("--headless")
243+
if not proxy_auth:
244+
# Headless Chrome doesn't support extensions, which are
245+
# required when using a proxy server that has authentication.
246+
# Instead, base_case.py will use PyVirtualDisplay when not
247+
# using Chrome's built-in headless mode. See link for details:
248+
# https://bugs.chromium.org/p/chromium/issues/detail?id=706008
249+
chrome_options.add_argument("--headless")
239250
chrome_options.add_argument("--disable-gpu")
240251
chrome_options.add_argument("--no-sandbox")
241252
capabilities = chrome_options.to_capabilities()
253+
for key in desired_caps.keys():
254+
capabilities[key] = desired_caps[key]
242255
return webdriver.Remote(
243256
command_executor=address,
244257
desired_capabilities=capabilities)
@@ -251,6 +264,8 @@ def get_remote_driver(
251264
if headless:
252265
firefox_capabilities['moz:firefoxOptions'] = (
253266
{'args': ['-headless']})
267+
for key in desired_caps.keys():
268+
firefox_capabilities[key] = desired_caps[key]
254269
capabilities = firefox_capabilities
255270
address = "http://%s:%s/wd/hub" % (servername, port)
256271
return webdriver.Remote(
@@ -265,39 +280,76 @@ def get_remote_driver(
265280
if headless:
266281
firefox_capabilities['moz:firefoxOptions'] = (
267282
{'args': ['-headless']})
283+
for key in desired_caps.keys():
284+
firefox_capabilities[key] = desired_caps[key]
268285
capabilities = firefox_capabilities
269286
return webdriver.Remote(
270287
command_executor=address,
271288
desired_capabilities=capabilities,
272289
browser_profile=profile)
273290
elif browser_name == constants.Browser.INTERNET_EXPLORER:
291+
capabilities = webdriver.DesiredCapabilities.INTERNETEXPLORER
292+
for key in desired_caps.keys():
293+
capabilities[key] = desired_caps[key]
274294
return webdriver.Remote(
275295
command_executor=address,
276-
desired_capabilities=(
277-
webdriver.DesiredCapabilities.INTERNETEXPLORER))
296+
desired_capabilities=capabilities)
278297
elif browser_name == constants.Browser.EDGE:
298+
capabilities = webdriver.DesiredCapabilities.EDGE
299+
for key in desired_caps.keys():
300+
capabilities[key] = desired_caps[key]
279301
return webdriver.Remote(
280302
command_executor=address,
281-
desired_capabilities=(
282-
webdriver.DesiredCapabilities.EDGE))
303+
desired_capabilities=capabilities)
283304
elif browser_name == constants.Browser.SAFARI:
305+
capabilities = webdriver.DesiredCapabilities.SAFARI
306+
for key in desired_caps.keys():
307+
capabilities[key] = desired_caps[key]
284308
return webdriver.Remote(
285309
command_executor=address,
286-
desired_capabilities=(
287-
webdriver.DesiredCapabilities.SAFARI))
310+
desired_capabilities=capabilities)
288311
elif browser_name == constants.Browser.OPERA:
312+
capabilities = webdriver.DesiredCapabilities.OPERA
313+
for key in desired_caps.keys():
314+
capabilities[key] = desired_caps[key]
289315
return webdriver.Remote(
290316
command_executor=address,
291-
desired_capabilities=(
292-
webdriver.DesiredCapabilities.OPERA))
317+
desired_capabilities=capabilities)
293318
elif browser_name == constants.Browser.PHANTOM_JS:
319+
capabilities = webdriver.DesiredCapabilities.PHANTOMJS
320+
for key in desired_caps.keys():
321+
capabilities[key] = desired_caps[key]
294322
with warnings.catch_warnings():
295323
# Ignore "PhantomJS has been deprecated" UserWarning
296324
warnings.simplefilter("ignore", category=UserWarning)
297325
return webdriver.Remote(
298326
command_executor=address,
299-
desired_capabilities=(
300-
webdriver.DesiredCapabilities.PHANTOMJS))
327+
desired_capabilities=capabilities)
328+
elif browser_name == constants.Browser.ANDROID:
329+
capabilities = webdriver.DesiredCapabilities.ANDROID
330+
for key in desired_caps.keys():
331+
capabilities[key] = desired_caps[key]
332+
return webdriver.Remote(
333+
command_executor=address,
334+
desired_capabilities=capabilities)
335+
elif browser_name == constants.Browser.IPHONE:
336+
capabilities = webdriver.DesiredCapabilities.IPHONE
337+
for key in desired_caps.keys():
338+
capabilities[key] = desired_caps[key]
339+
return webdriver.Remote(
340+
command_executor=address,
341+
desired_capabilities=capabilities)
342+
elif browser_name == constants.Browser.IPAD:
343+
capabilities = webdriver.DesiredCapabilities.IPAD
344+
for key in desired_caps.keys():
345+
capabilities[key] = desired_caps[key]
346+
return webdriver.Remote(
347+
command_executor=address,
348+
desired_capabilities=capabilities)
349+
elif browser_name == constants.Browser.REMOTE:
350+
return webdriver.Remote(
351+
command_executor=address,
352+
desired_capabilities=desired_caps)
301353

302354

303355
def get_local_driver(
@@ -418,3 +470,6 @@ def get_local_driver(
418470
return webdriver.Chrome(executable_path=LOCAL_CHROMEDRIVER)
419471
else:
420472
return webdriver.Chrome()
473+
else:
474+
raise Exception(
475+
"%s is not a valid browser option for this system!" % browser_name)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import re
2+
3+
4+
def get_desired_capabilities(cap_file):
5+
if not cap_file.endswith('.py'):
6+
raise Exception("\n\n`%s` is not a Python file!\n\n" % cap_file)
7+
8+
f = open(cap_file, 'r')
9+
all_code = f.read()
10+
f.close()
11+
12+
desired_capabilities = {}
13+
num_capabilities = 0
14+
15+
code_lines = all_code.split('\n')
16+
for line in code_lines:
17+
if "desired_cap = {" in line:
18+
line = line.split("desired_cap = {")[1]
19+
20+
# 'KEY' : 'VALUE'
21+
data = re.match(r"^\s*'([\S\s]+)'\s*:\s*'([\S\s]+)'\s*[,}]?\s*$", line)
22+
if data:
23+
key = data.group(1)
24+
value = data.group(2)
25+
desired_capabilities[key] = value
26+
num_capabilities += 1
27+
continue
28+
29+
# "KEY" : "VALUE"
30+
data = re.match(r'^\s*"([\S\s]+)"\s*:\s*"([\S\s]+)"\s*[,}]?\s*$', line)
31+
if data:
32+
key = data.group(1)
33+
value = data.group(2)
34+
desired_capabilities[key] = value
35+
num_capabilities += 1
36+
continue
37+
38+
# 'KEY' : "VALUE"
39+
data = re.match(
40+
r'''^\s*'([\S\s]+)'\s*:\s*"([\S\s]+)"\s*[,}]?\s*$''', line)
41+
if data:
42+
key = data.group(1)
43+
value = data.group(2)
44+
desired_capabilities[key] = value
45+
num_capabilities += 1
46+
continue
47+
48+
# "KEY" : 'VALUE'
49+
data = re.match(
50+
r'''^\s*"([\S\s]+)"\s*:\s*'([\S\s]+)'\s*[,}]?\s*$''', line)
51+
if data:
52+
key = data.group(1)
53+
value = data.group(2)
54+
desired_capabilities[key] = value
55+
num_capabilities += 1
56+
continue
57+
58+
# caps['KEY'] = 'VALUE'
59+
data = re.match(r"^\s*caps\['([\S\s]+)'\]\s*=\s*'([\S\s]+)'\s*$", line)
60+
if data:
61+
key = data.group(1)
62+
value = data.group(2)
63+
desired_capabilities[key] = value
64+
num_capabilities += 1
65+
continue
66+
67+
# caps["KEY"] = "VALUE"
68+
data = re.match(r'^\s*caps\["([\S\s]+)"\]\s*=\s*"([\S\s]+)"\s*$', line)
69+
if data:
70+
key = data.group(1)
71+
value = data.group(2)
72+
desired_capabilities[key] = value
73+
num_capabilities += 1
74+
continue
75+
76+
# caps['KEY'] = "VALUE"
77+
data = re.match(
78+
r'''^\s*caps\['([\S\s]+)'\]\s*=\s*"([\S\s]+)"\s*$''', line)
79+
if data:
80+
key = data.group(1)
81+
value = data.group(2)
82+
desired_capabilities[key] = value
83+
num_capabilities += 1
84+
continue
85+
86+
# caps["KEY"] = 'VALUE'
87+
data = re.match(
88+
r'''^\s*caps\["([\S\s]+)"\]\s*=\s*'([\S\s]+)'\s*$''', line)
89+
if data:
90+
key = data.group(1)
91+
value = data.group(2)
92+
desired_capabilities[key] = value
93+
num_capabilities += 1
94+
continue
95+
96+
if num_capabilities == 0:
97+
raise Exception("Unable to parse desired capabilities file!")
98+
99+
return desired_capabilities

seleniumbase/fixtures/base_case.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,7 +2191,8 @@ def save_screenshot(self, name, folder=None):
21912191
return page_actions.save_screenshot(self.driver, name, folder)
21922192

21932193
def get_new_driver(self, browser=None, headless=None,
2194-
servername=None, port=None, proxy=None, switch_to=True):
2194+
servername=None, port=None, proxy=None, switch_to=True,
2195+
cap_file=None):
21952196
""" This method spins up an extra browser for tests that require
21962197
more than one. The first browser is already provided by tests
21972198
that import base_case.BaseCase from seleniumbase. If parameters
@@ -2204,6 +2205,27 @@ def get_new_driver(self, browser=None, headless=None,
22042205
proxy - if using a proxy server, specify the "host:port" combo here
22052206
switch_to - the option to switch to the new driver (default = True)
22062207
"""
2208+
if self.browser == "remote" and self.servername == "localhost":
2209+
raise Exception('Cannot use "remote" browser driver on localhost!'
2210+
' Did you mean to connect to a remote Grid server'
2211+
' such as BrowserStack or Sauce Labs? In that'
2212+
' case, you must specify the "server" and "port"'
2213+
' parameters on the command line! '
2214+
'Example: '
2215+
'--server=user:[email protected] --port=80')
2216+
browserstack_ref = (
2217+
'https://browserstack.com/automate/capabilities')
2218+
sauce_labs_ref = (
2219+
'https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/')
2220+
if self.browser == "remote" and not self.cap_file:
2221+
raise Exception('Need to specify a desired capabilities file when '
2222+
'using "--browser=remote". Add "--cap_file=FILE". '
2223+
'File should be in the Python format used by: '
2224+
'%s OR '
2225+
'%s '
2226+
'See SeleniumBase/examples/sample_cap_file_BS.py '
2227+
'and SeleniumBase/examples/sample_cap_file_SL.py'
2228+
% (browserstack_ref, sauce_labs_ref))
22072229
if browser is None:
22082230
browser = self.browser
22092231
browser_name = browser
@@ -2220,6 +2242,8 @@ def get_new_driver(self, browser=None, headless=None,
22202242
proxy_string = proxy
22212243
if proxy_string is None:
22222244
proxy_string = self.proxy_string
2245+
if cap_file is None:
2246+
cap_file = self.cap_file
22232247
valid_browsers = constants.ValidBrowsers.valid_browsers
22242248
if browser_name not in valid_browsers:
22252249
raise Exception("Browser: {%s} is not a valid browser option. "
@@ -2231,7 +2255,8 @@ def get_new_driver(self, browser=None, headless=None,
22312255
use_grid=use_grid,
22322256
servername=servername,
22332257
port=port,
2234-
proxy_string=proxy_string)
2258+
proxy_string=proxy_string,
2259+
cap_file=cap_file)
22352260
self._drivers_list.append(new_driver)
22362261
if switch_to:
22372262
self.driver = new_driver
@@ -2606,6 +2631,7 @@ def setUp(self):
26062631
self.servername = pytest.config.option.servername
26072632
self.port = pytest.config.option.port
26082633
self.proxy_string = pytest.config.option.proxy_string
2634+
self.cap_file = pytest.config.option.cap_file
26092635
self.database_env = pytest.config.option.database_env
26102636
self.log_path = pytest.config.option.log_path
26112637
self.browser = pytest.config.option.browser
@@ -2676,7 +2702,8 @@ def setUp(self):
26762702
servername=self.servername,
26772703
port=self.port,
26782704
proxy=self.proxy_string,
2679-
switch_to=True)
2705+
switch_to=True,
2706+
cap_file=self.cap_file)
26802707
self._default_driver = self.driver
26812708

26822709
def __insert_test_result(self, state, err):

0 commit comments

Comments
 (0)