|
@@ -9,6 +9,105 @@ import urllib
|
|
|
from datetime import datetime
|
|
|
from django.conf import settings
|
|
|
from selenium import webdriver
|
|
|
+from selenium.webdriver.support.ui import WebDriverWait
|
|
|
+from selenium.webdriver.support import expected_conditions
|
|
|
+from urlparse import urlparse
|
|
|
+
|
|
|
+
|
|
|
+class Browser(object):
|
|
|
+ def __init__(self, driver, live_server, percy):
|
|
|
+ self.driver = driver
|
|
|
+ self.live_server_url = live_server.url
|
|
|
+ self.percy = percy
|
|
|
+ self.domain = urlparse(self.live_server_url).hostname
|
|
|
+ self._has_initialized_cookie_store = False
|
|
|
+
|
|
|
+ def __getattr__(self, attr):
|
|
|
+ return getattr(self.driver, attr)
|
|
|
+
|
|
|
+ def route(self, path, *args, **kwargs):
|
|
|
+ """
|
|
|
+ Return the absolute URI for a given route in Sentry.
|
|
|
+ """
|
|
|
+ return '{}/{}'.format(self.live_server_url, path.strip('/').format(
|
|
|
+ *args, **kwargs
|
|
|
+ ))
|
|
|
+
|
|
|
+ def get(self, path, *args, **kwargs):
|
|
|
+ self.driver.get(self.route(path), *args, **kwargs)
|
|
|
+ return self
|
|
|
+
|
|
|
+ def post(self, path, *args, **kwargs):
|
|
|
+ self.driver.post(self.route(path), *args, **kwargs)
|
|
|
+ return self
|
|
|
+
|
|
|
+ def put(self, path, *args, **kwargs):
|
|
|
+ self.driver.put(self.route(path), *args, **kwargs)
|
|
|
+ return self
|
|
|
+
|
|
|
+ def delete(self, path, *args, **kwargs):
|
|
|
+ self.driver.delete(self.route(path), *args, **kwargs)
|
|
|
+ return self
|
|
|
+
|
|
|
+ def wait_until(self, selector, timeout=3):
|
|
|
+ """
|
|
|
+ Waits until ``selector`` is found in the browser, or until ``timeout``
|
|
|
+ is hit, whichever happens first.
|
|
|
+ """
|
|
|
+ from selenium.webdriver.common.by import By
|
|
|
+
|
|
|
+ WebDriverWait(self.driver, timeout).until(
|
|
|
+ expected_conditions.presence_of_element_located(
|
|
|
+ (By.CSS_SELECTOR, selector)
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ return self
|
|
|
+
|
|
|
+ def wait_until_not(self, selector, timeout=3):
|
|
|
+ """
|
|
|
+ Waits until ``selector`` is NOT found in the browser, or until
|
|
|
+ ``timeout`` is hit, whichever happens first.
|
|
|
+ """
|
|
|
+ from selenium.webdriver.common.by import By
|
|
|
+
|
|
|
+ WebDriverWait(self.driver, timeout).until_not(
|
|
|
+ expected_conditions.presence_of_element_located(
|
|
|
+ (By.CSS_SELECTOR, selector)
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ return self
|
|
|
+
|
|
|
+ def snapshot(self, name):
|
|
|
+ """
|
|
|
+ Capture a screenshot of the current state of the page. Screenshots
|
|
|
+ are captured both locally (in ``cls.screenshots_path``) as well as
|
|
|
+ with Percy (when enabled).
|
|
|
+ """
|
|
|
+ # TODO(dcramer): ideally this would take the executing test package
|
|
|
+ # into account for duplicate names
|
|
|
+ self.percy.snapshot(name=name)
|
|
|
+ return self
|
|
|
+
|
|
|
+ def save_cookie(self, name, value, path='/',
|
|
|
+ expires='Tue, 20 Jun 2025 19:07:44 GMT'):
|
|
|
+ # XXX(dcramer): "hit a url before trying to set cookies"
|
|
|
+ if not self._has_initialized_cookie_store:
|
|
|
+ self.get('/')
|
|
|
+ self._has_initialized_cookie_store = True
|
|
|
+
|
|
|
+ # XXX(dcramer): PhantomJS does not let us add cookies with the native
|
|
|
+ # selenium API because....
|
|
|
+ # http://stackoverflow.com/questions/37103621/adding-cookies-working-with-firefox-webdriver-but-not-in-phantomjs
|
|
|
+ # TODO(dcramer): this should be escaped, but idgaf
|
|
|
+ self.driver.execute_script("document.cookie = '{name}={value}; path={path}; domain={domain}; expires={expires}';\n".format(
|
|
|
+ name=name,
|
|
|
+ value=value,
|
|
|
+ expires=expires,
|
|
|
+ path=path,
|
|
|
+ domain=self.domain,
|
|
|
+ ))
|
|
|
|
|
|
|
|
|
def pytest_configure(config):
|
|
@@ -91,6 +190,8 @@ def pytest_configure(config):
|
|
|
settings.CELERY_ALWAYS_EAGER = False
|
|
|
settings.CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
|
|
|
|
|
|
+ settings.DEBUG_VIEWS = True
|
|
|
+
|
|
|
settings.DISABLE_RAVEN = True
|
|
|
|
|
|
settings.CACHES = {
|
|
@@ -160,7 +261,7 @@ def pytest_runtest_teardown(item):
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
|
-def browser(request, live_server):
|
|
|
+def driver(request, live_server):
|
|
|
# Initialize Selenium.
|
|
|
# NOTE: this relies on the phantomjs binary packaged from npm to be in the right
|
|
|
# location in node_modules.
|
|
@@ -170,33 +271,34 @@ def browser(request, live_server):
|
|
|
'bin',
|
|
|
'phantomjs',
|
|
|
)
|
|
|
- browser = webdriver.PhantomJS(executable_path=phantomjs_path)
|
|
|
+ driver = webdriver.PhantomJS(executable_path=phantomjs_path)
|
|
|
|
|
|
def fin():
|
|
|
# Teardown Selenium.
|
|
|
try:
|
|
|
- browser.close()
|
|
|
+ driver.close()
|
|
|
except Exception:
|
|
|
pass
|
|
|
# TODO: remove this when fixed in: https://github.com/seleniumhq/selenium/issues/767
|
|
|
- browser.service.process.send_signal(signal.SIGTERM)
|
|
|
- browser.quit()
|
|
|
+ driver.service.process.send_signal(signal.SIGTERM)
|
|
|
+ driver.quit()
|
|
|
|
|
|
+ request.session._driver = driver
|
|
|
request.addfinalizer(fin)
|
|
|
- return browser
|
|
|
+ return driver
|
|
|
|
|
|
|
|
|
# TODO(dcramer): ideally we could bundle up more of the browser logic here
|
|
|
# rather than splitting it between the fixtures and AcceptanceTestCase
|
|
|
@pytest.fixture(scope='session')
|
|
|
-def percy(request, browser):
|
|
|
+def percy(request, driver):
|
|
|
import percy
|
|
|
|
|
|
# Initialize Percy.
|
|
|
loader = percy.ResourceLoader(
|
|
|
root_dir=settings.STATIC_ROOT,
|
|
|
base_url=urllib.quote(settings.STATIC_URL),
|
|
|
- webdriver=browser,
|
|
|
+ webdriver=driver,
|
|
|
)
|
|
|
percy_config = percy.Config(default_widths=settings.PERCY_DEFAULT_TESTING_WIDTHS)
|
|
|
percy = percy.Runner(loader=loader, config=percy_config)
|
|
@@ -213,20 +315,8 @@ def live_server_class(request, live_server):
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
|
-def screenshots_path_class(request, browser):
|
|
|
- date = datetime.utcnow()
|
|
|
- # AcceptanceTestCase.snapshot saves local screenshots here
|
|
|
- path = os.path.normpath(os.path.join(
|
|
|
- os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, 'tmp', 'selenium-screenshots', date.strftime('%s'))
|
|
|
- )
|
|
|
- print('Screenshots will be stored in {}'.format(path))
|
|
|
- os.makedirs(path)
|
|
|
- request.cls.screenshots_path = path
|
|
|
-
|
|
|
-
|
|
|
-@pytest.fixture(scope='class')
|
|
|
-def browser_class(request, browser):
|
|
|
- request.cls.browser = browser
|
|
|
+def browser_class(request, driver, live_server, percy):
|
|
|
+ request.cls.browser = Browser(driver, live_server, percy)
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
@@ -235,11 +325,92 @@ def percy_class(request, percy):
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
|
-def reset_browser_session(request):
|
|
|
- if not hasattr(request, 'browser'):
|
|
|
+def setup_selenium_session(request):
|
|
|
+ driver = getattr(request.session, '_driver', None)
|
|
|
+ if not driver:
|
|
|
return
|
|
|
+ driver.delete_all_cookies()
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.tryfirst
|
|
|
+def pytest_runtest_makereport(item, call, __multicall__):
|
|
|
+ report = __multicall__.execute()
|
|
|
+ summary = []
|
|
|
+ extra = getattr(report, 'extra', [])
|
|
|
+ driver = getattr(item.session, '_driver', None)
|
|
|
+ if driver is not None:
|
|
|
+ _gather_url(item, report, driver, summary, extra)
|
|
|
+ _gather_screenshot(item, report, driver, summary, extra)
|
|
|
+ _gather_html(item, report, driver, summary, extra)
|
|
|
+ _gather_logs(item, report, driver, summary, extra)
|
|
|
+ if summary:
|
|
|
+ report.sections.append(('selenium', '\n'.join(summary)))
|
|
|
+ report.extra = extra
|
|
|
+ return report
|
|
|
+
|
|
|
+
|
|
|
+def _gather_url(item, report, driver, summary, extra):
|
|
|
+ try:
|
|
|
+ url = driver.current_url
|
|
|
+ except Exception as e:
|
|
|
+ summary.append('WARNING: Failed to gather URL: {0}'.format(e))
|
|
|
+ return
|
|
|
+ pytest_html = item.config.pluginmanager.getplugin('html')
|
|
|
+ if pytest_html is not None:
|
|
|
+ # add url to the html report
|
|
|
+ extra.append(pytest_html.extras.url(url))
|
|
|
+ summary.append('URL: {0}'.format(url))
|
|
|
+
|
|
|
+
|
|
|
+def _gather_screenshot(item, report, driver, summary, extra):
|
|
|
+ try:
|
|
|
+ screenshot = driver.get_screenshot_as_base64()
|
|
|
+ except Exception as e:
|
|
|
+ summary.append('WARNING: Failed to gather screenshot: {0}'.format(e))
|
|
|
+ return
|
|
|
+ pytest_html = item.config.pluginmanager.getplugin('html')
|
|
|
+ if pytest_html is not None:
|
|
|
+ # add screenshot to the html report
|
|
|
+ extra.append(pytest_html.extras.image(screenshot, 'Screenshot'))
|
|
|
|
|
|
- browser = request.browser
|
|
|
|
|
|
- browser.delete_all_cookies()
|
|
|
- browser.get('about:blank')
|
|
|
+def _gather_html(item, report, driver, summary, extra):
|
|
|
+ try:
|
|
|
+ html = driver.page_source.encode('utf-8')
|
|
|
+ except Exception as e:
|
|
|
+ summary.append('WARNING: Failed to gather HTML: {0}'.format(e))
|
|
|
+ return
|
|
|
+ pytest_html = item.config.pluginmanager.getplugin('html')
|
|
|
+ if pytest_html is not None:
|
|
|
+ # add page source to the html report
|
|
|
+ extra.append(pytest_html.extras.text(html, 'HTML'))
|
|
|
+
|
|
|
+
|
|
|
+def _gather_logs(item, report, driver, summary, extra):
|
|
|
+ try:
|
|
|
+ types = driver.log_types
|
|
|
+ except Exception as e:
|
|
|
+ # note that some drivers may not implement log types
|
|
|
+ summary.append('WARNING: Failed to gather log types: {0}'.format(e))
|
|
|
+ return
|
|
|
+ for name in types:
|
|
|
+ try:
|
|
|
+ log = driver.get_log(name)
|
|
|
+ except Exception as e:
|
|
|
+ summary.append('WARNING: Failed to gather {0} log: {1}'.format(
|
|
|
+ name, e))
|
|
|
+ return
|
|
|
+ pytest_html = item.config.pluginmanager.getplugin('html')
|
|
|
+ if pytest_html is not None:
|
|
|
+ extra.append(pytest_html.extras.text(
|
|
|
+ format_log(log), '%s Log' % name.title()))
|
|
|
+
|
|
|
+
|
|
|
+def format_log(log):
|
|
|
+ timestamp_format = '%Y-%m-%d %H:%M:%S.%f'
|
|
|
+ entries = [u'{0} {1[level]} - {1[message]}'.format(
|
|
|
+ datetime.utcfromtimestamp(entry['timestamp'] / 1000.0).strftime(
|
|
|
+ timestamp_format), entry).rstrip() for entry in log]
|
|
|
+ log = '\n'.join(entries)
|
|
|
+ log = log.encode('utf-8')
|
|
|
+ return log
|