Skip to content

Commit 90ac16f

Browse files
author
Brandon Duffany
committed
Add remote webdriver support
This allows us to take advantage of SauceLabs as well as docker-selenium, among other things. Exciting stuff!
1 parent 3bfc115 commit 90ac16f

File tree

4 files changed

+126
-44
lines changed

4 files changed

+126
-44
lines changed

codebender_testing/config.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22

33
from selenium import webdriver
4-
4+
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
55

66
def _rel_path(*args):
77
"""Forms a path relative to this file's directory."""
@@ -55,18 +55,55 @@ def _get_firefox_profile():
5555
)
5656
return firefox_profile
5757

58-
# Webdrivers to be used for testing. Specifying additional webdrivers here
59-
# will cause every test to be re-run using that webdriver.
60-
# These webdrivers are specified as lambdas to allow for "lazy" evaluation.
61-
# The lambda invocation will return the actual webdriver and open up a
62-
# browser window, and we don't want a browser window to open whenever this
63-
# module is imported (hence the need for lazy evaluation).
64-
WEBDRIVERS = {
65-
"firefox": lambda: webdriver.Firefox(firefox_profile=_get_firefox_profile()),
66-
# "chrome": lambda: webdriver.Chrome()
67-
}
6858

69-
# Credentials to use when logging into the site via selenium
59+
def _get_chrome_profile():
60+
# TODO
61+
return None
62+
63+
64+
def get_browsers(config_path=None):
65+
"""Returns a list of capabilities. Each item in the list will cause
66+
the entire suite of tests to be re-run for a browser with those
67+
particular capabilities."""
68+
# TODO: read from browsers.yaml, and allow specifying custom config file.
69+
return [
70+
{
71+
"browserName": "firefox", # OK with other defaults for now.
72+
}
73+
]
74+
75+
76+
def create_webdriver(command_executor, desired_capabilities):
77+
"""Creates a new remote webdriver with the following properties:
78+
- The remote URL of the webdriver is defined by `command_executor`.
79+
- desired_capabilities is a dict with the same interpretation as
80+
it is used elsewhere in selenium. If no browserName key is present,
81+
we default to firefox.
82+
"""
83+
if 'browserName' not in desired_capabilities:
84+
desired_capabilities['browserName'] = 'firefox'
85+
browser_name = desired_capabilities['browserName']
86+
# Fill in defaults from DesiredCapabilities.{CHROME,FIREFOX} if they are
87+
# missing from the desired_capabilities dict above.
88+
_capabilities = desired_capabilities
89+
if browser_name == "chrome":
90+
desired_capabilities = DesiredCapabilities.CHROME.copy()
91+
desired_capabilities.update(_capabilities)
92+
browser_profile = _get_chrome_profile()
93+
elif browser_name == "firefox":
94+
desired_capabilities = DesiredCapabilities.FIREFOX.copy()
95+
desired_capabilities.update(_capabilities)
96+
browser_profile = _get_firefox_profile()
97+
else:
98+
raise ValueError("Invalid webdriver %s (only chrome and firefox are supported)" % browser_name)
99+
return webdriver.Remote(
100+
command_executor=command_executor,
101+
desired_capabilities=desired_capabilities,
102+
browser_profile=browser_profile,
103+
)
104+
105+
106+
# Credentials to use when logging into the bachelor site
70107
TEST_CREDENTIALS = {
71108
"username": "tester",
72109
"password": "testerPASS"
@@ -75,4 +112,4 @@ def _get_firefox_profile():
75112
TEST_PROJECT_NAME = "test_project"
76113

77114
# How long we wait until giving up on trying to locate an element
78-
ELEMENT_FIND_TIMEOUT = 5
115+
ELEMENT_FIND_TIMEOUT = 10

codebender_testing/utils.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from codebender_testing.config import ELEMENT_FIND_TIMEOUT
2121
from codebender_testing.config import TEST_CREDENTIALS
2222
from codebender_testing.config import TEST_PROJECT_NAME
23-
from codebender_testing.config import WEBDRIVERS
2423

2524

2625
# Time to wait until we give up on a DOM property becoming available.
@@ -92,16 +91,13 @@ class CodebenderSeleniumBot(object):
9291
# It is set via command line option in _testcase_attrs (below)
9392
site_url = None
9493

95-
def start(url=None, webdriver=None):
96-
"""Create the selenium webdriver, operating on `url`. We can't do this
97-
in an __init__ method, otherwise py.test complains about
98-
SeleniumTestCase having an init method.
99-
The webdriver that is created is specified as a key into the WEBDRIVERS
100-
dict (in codebender_testing.config)
94+
def init(self, url=None, webdriver=None):
95+
"""Create a bot with the given selenium webdriver, operating on `url`.
96+
We can't do this in an __init__ method, otherwise py.test complains,
97+
presumably because it does something special with __init__ for test
98+
cases.
10199
"""
102-
if webdriver is None:
103-
webdriver = WEBDRIVERS.keys()[0]
104-
self.driver = WEBDRIVERS[webdriver]()
100+
self.driver = webdriver
105101

106102
if url is None:
107103
url = BASE_URL
@@ -186,17 +182,22 @@ def upload_project(self, test_fname, project_name=None):
186182

187183
return last_project.text, last_project.get_attribute('href')
188184

189-
def login(self):
190-
"""Performs a login."""
185+
def login(self, credentials=None):
186+
"""Performs a login. Note that the current URL may change to an
187+
unspecified location when calling this function.
188+
`credentials` should be a dict with keys 'username' and 'password',
189+
mapped to the appropriate values."""
190+
if credentials is None:
191+
credentials = TEST_CREDENTIALS
191192
try:
192193
self.open()
193194
login_button = self.driver.find_element_by_id('login_btn')
194195
login_button.send_keys(Keys.ENTER)
195196
# Enter credentials and log in
196197
user_field = self.driver.find_element_by_id('username')
197-
user_field.send_keys(TEST_CREDENTIALS['username'])
198+
user_field.send_keys(credentials['username'])
198199
pass_field = self.driver.find_element_by_id('password')
199-
pass_field.send_keys(TEST_CREDENTIALS['password'])
200+
pass_field.send_keys(credentials['password'])
200201
do_login = self.driver.find_element_by_id('_submit')
201202
do_login.send_keys(Keys.ENTER)
202203
except NoSuchElementException:
@@ -346,11 +347,15 @@ def _testcase_attrs(cls, webdriver, testing_url, testing_full):
346347
cls.run_full_compile_tests = testing_full
347348

348349
@pytest.fixture(scope="class")
349-
def tester_login(self):
350-
self.login()
350+
def tester_login(self, testing_credentials):
351+
"""A fixture to perform a login with the credentials provided by the
352+
`testing_credentials` fixture.
353+
"""
354+
self.login(credentials=testing_credentials)
351355

352356
@pytest.fixture(scope="class")
353357
def tester_logout(self):
358+
"""A fixture to guarantee that we are logged out before running a test."""
354359
self.logout()
355360

356361

tests/conftest.py

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,91 @@
66
This is also where command-line arguments and pytest markers are defined.
77
"""
88

9+
import os
10+
import sys
11+
912
import pytest
1013

1114
from codebender_testing import config
1215

1316

14-
@pytest.fixture(scope="session", params=config.WEBDRIVERS.keys())
15-
def webdriver(request):
16-
"""Returns a webdriver that persists across the entire test session,
17-
and registers a finalizer to close the browser once the session is
18-
complete. The entire test session is repeated once per driver.
19-
"""
20-
driver = config.WEBDRIVERS[request.param]()
21-
request.addfinalizer(lambda: driver.quit())
22-
return driver
23-
2417
def pytest_addoption(parser):
25-
"""Adds command line options to py.test."""
18+
"""Adds command line options to the testing suite."""
19+
2620
parser.addoption("--url", action="store", default=config.BASE_URL,
2721
help="URL to use for testing, e.g. http://localhost, http://codebender.cc")
22+
2823
parser.addoption("--full", action="store_true", default=False,
29-
help="Whether to run the complete set of compile tests.")
24+
help="Run the complete set of compile tests (a minimal set of tests is run by default).")
3025

3126
parser.addoption("--source", action="store", default=config.SOURCE_BACHELOR,
3227
help="Indicate the source used to generate the repo. "
3328
"By default, we assume `bachelor`.")
3429

30+
31+
@pytest.fixture(scope="session", params=config.get_browsers())
32+
def webdriver(request):
33+
"""Returns a webdriver that persists across the entire test session,
34+
and registers a finalizer to close the browser once the session is
35+
complete. The entire test session is repeated once per driver.
36+
"""
37+
38+
# TODO: maybe have a different way of specifying this (?)
39+
command_executor = os.environ['CODEBENDER_SELENIUM_HUB_URL']
40+
desired_capabilities = request.param
41+
42+
driver = config.create_webdriver(command_executor, desired_capabilities)
43+
44+
# TODO: update sauce status via SauceClient, but only if the command_executor
45+
# is a sauce URL.
46+
def finalizer():
47+
# print("Link to your job: https://saucelabs.com/jobs/%s" % driver.session_id)
48+
try:
49+
pass
50+
# TODO:
51+
# if sys.exc_info() == (None, None, None):
52+
# sauce.jobs.update_job(driver.session_id, passed=True)
53+
# else:
54+
# sauce.jobs.update_job(driver.session_id, passed=False)
55+
finally:
56+
driver.quit()
57+
58+
request.addfinalizer(finalizer)
59+
return driver
60+
61+
3562
@pytest.fixture(scope="class")
3663
def testing_url(request):
3764
"""A fixture to get the --url parameter."""
3865
return request.config.getoption("--url")
3966

67+
4068
@pytest.fixture(scope="class")
4169
def source(request):
4270
"""A fixture to specify the source repository from which the site was
4371
derived (e.g. bachelor or codebender_cc)
4472
"""
4573
return request.config.getoption("--source")
4674

75+
76+
@pytest.fixture(scope="class")
77+
def testing_credentials(request):
78+
"""A fixture to get testing credentials specified via the environment
79+
variables CODEBENDER_TEST_USER and CODEBENDER_TEST_PASS. Defaults to the
80+
credentials specified in config.TEST_CREDENTIALS.
81+
"""
82+
return {
83+
'username': os.environ.get('CODEBENDER_TEST_USER', config.TEST_CREDENTIALS['username']),
84+
'password': os.environ.get('CODEBENDER_TEST_PASS', config.TEST_CREDENTIALS['password']),
85+
}
86+
87+
4788
@pytest.fixture(scope="class")
4889
def testing_full(request):
4990
"""A fixture to get the --full parameter."""
5091
return request.config.getoption("--full")
5192

93+
5294
@pytest.fixture(autouse=True)
5395
def requires_source(request, source):
5496
"""Skips tests that require a certain source version (e.g. bachelor or
@@ -67,6 +109,7 @@ def test_some_feature():
67109
if required_source != source:
68110
pytest.skip('skipped test that requires --source=' + source)
69111

112+
70113
@pytest.fixture(autouse=True)
71114
def requires_url(request, testing_url):
72115
"""Skips tests that require a certain site URL in order to run properly.

tests/sketch/test_sketch.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import time
22

3-
from selenium.common.exceptions import StaleElementReferenceException
43
from selenium.webdriver.common.by import By
5-
from selenium.webdriver.common.keys import Keys
64
from selenium.webdriver.support import expected_conditions
75
from selenium.webdriver.support.select import Select
86
from selenium.webdriver.support.ui import WebDriverWait
@@ -81,7 +79,6 @@ def test_serial_monitor_disables_fields(self):
8179
ports_field = self.get_element(By.ID, 'ports_placeholder')
8280
assert ports_field.get_attribute('disabled') == 'true'
8381

84-
8582
def test_clone_project(self):
8683
"""Tests that clicking the 'Clone Project' link brings us to a new
8784
sketch with the title 'test_project clone'."""

0 commit comments

Comments
 (0)