2016-02-07 10:24:36 +08:00
|
|
|
import sys
|
|
|
|
import unittest
|
2016-05-04 11:19:24 +08:00
|
|
|
from contextlib import contextmanager
|
2016-02-07 10:24:36 +08:00
|
|
|
|
|
|
|
from django.test import LiveServerTestCase, tag
|
2018-10-26 01:52:29 +08:00
|
|
|
from django.utils.decorators import classproperty
|
2016-02-07 10:24:36 +08:00
|
|
|
from django.utils.module_loading import import_string
|
|
|
|
from django.utils.text import capfirst
|
|
|
|
|
|
|
|
|
|
|
|
class SeleniumTestCaseBase(type(LiveServerTestCase)):
|
|
|
|
# List of browsers to dynamically create test classes for.
|
|
|
|
browsers = []
|
2018-10-26 01:52:29 +08:00
|
|
|
# A selenium hub URL to test against.
|
|
|
|
selenium_hub = None
|
|
|
|
# The external host Selenium Hub can reach.
|
|
|
|
external_host = None
|
2016-02-07 10:24:36 +08:00
|
|
|
# Sentinel value to differentiate browser-specific instances.
|
|
|
|
browser = None
|
2019-02-27 00:23:49 +08:00
|
|
|
# Run browsers in headless mode.
|
|
|
|
headless = False
|
2016-02-07 10:24:36 +08:00
|
|
|
|
|
|
|
def __new__(cls, name, bases, attrs):
|
|
|
|
"""
|
|
|
|
Dynamically create new classes and add them to the test module when
|
|
|
|
multiple browsers specs are provided (e.g. --selenium=firefox,chrome).
|
|
|
|
"""
|
2017-01-21 21:13:44 +08:00
|
|
|
test_class = super().__new__(cls, name, bases, attrs)
|
2016-02-07 10:24:36 +08:00
|
|
|
# If the test class is either browser-specific or a test base, return it.
|
|
|
|
if test_class.browser or not any(name.startswith('test') and callable(value) for name, value in attrs.items()):
|
|
|
|
return test_class
|
|
|
|
elif test_class.browsers:
|
|
|
|
# Reuse the created test class to make it browser-specific.
|
|
|
|
# We can't rename it to include the browser name or create a
|
|
|
|
# subclass like we do with the remaining browsers as it would
|
|
|
|
# either duplicate tests or prevent pickling of its instances.
|
|
|
|
first_browser = test_class.browsers[0]
|
|
|
|
test_class.browser = first_browser
|
2018-10-26 01:52:29 +08:00
|
|
|
# Listen on an external interface if using a selenium hub.
|
|
|
|
host = test_class.host if not test_class.selenium_hub else '0.0.0.0'
|
|
|
|
test_class.host = host
|
|
|
|
test_class.external_host = cls.external_host
|
2016-02-07 10:24:36 +08:00
|
|
|
# Create subclasses for each of the remaining browsers and expose
|
|
|
|
# them through the test's module namespace.
|
|
|
|
module = sys.modules[test_class.__module__]
|
|
|
|
for browser in test_class.browsers[1:]:
|
|
|
|
browser_test_class = cls.__new__(
|
|
|
|
cls,
|
2017-01-20 17:20:53 +08:00
|
|
|
"%s%s" % (capfirst(browser), name),
|
2016-02-07 10:24:36 +08:00
|
|
|
(test_class,),
|
2018-10-26 01:52:29 +08:00
|
|
|
{
|
|
|
|
'browser': browser,
|
|
|
|
'host': host,
|
|
|
|
'external_host': cls.external_host,
|
|
|
|
'__module__': test_class.__module__,
|
|
|
|
}
|
2016-02-07 10:24:36 +08:00
|
|
|
)
|
|
|
|
setattr(module, browser_test_class.__name__, browser_test_class)
|
|
|
|
return test_class
|
|
|
|
# If no browsers were specified, skip this class (it'll still be discovered).
|
|
|
|
return unittest.skip('No browsers specified.')(test_class)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def import_webdriver(cls, browser):
|
|
|
|
return import_string("selenium.webdriver.%s.webdriver.WebDriver" % browser)
|
|
|
|
|
2019-02-27 00:23:49 +08:00
|
|
|
@classmethod
|
|
|
|
def import_options(cls, browser):
|
|
|
|
return import_string('selenium.webdriver.%s.options.Options' % browser)
|
|
|
|
|
2018-10-26 01:52:29 +08:00
|
|
|
@classmethod
|
|
|
|
def get_capability(cls, browser):
|
2018-11-04 06:49:51 +08:00
|
|
|
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
2018-10-26 01:52:29 +08:00
|
|
|
return getattr(DesiredCapabilities, browser.upper())
|
|
|
|
|
2019-02-27 00:23:49 +08:00
|
|
|
def create_options(self):
|
|
|
|
options = self.import_options(self.browser)()
|
|
|
|
if self.headless:
|
|
|
|
try:
|
|
|
|
options.headless = True
|
|
|
|
except AttributeError:
|
|
|
|
pass # Only Chrome and Firefox support the headless mode.
|
|
|
|
return options
|
|
|
|
|
2016-02-07 10:24:36 +08:00
|
|
|
def create_webdriver(self):
|
2018-10-26 01:52:29 +08:00
|
|
|
if self.selenium_hub:
|
2018-11-04 06:49:51 +08:00
|
|
|
from selenium import webdriver
|
2018-10-26 01:52:29 +08:00
|
|
|
return webdriver.Remote(
|
|
|
|
command_executor=self.selenium_hub,
|
|
|
|
desired_capabilities=self.get_capability(self.browser),
|
|
|
|
)
|
2019-02-27 00:23:49 +08:00
|
|
|
return self.import_webdriver(self.browser)(options=self.create_options())
|
2016-02-07 10:24:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
@tag('selenium')
|
2017-01-07 19:11:46 +08:00
|
|
|
class SeleniumTestCase(LiveServerTestCase, metaclass=SeleniumTestCaseBase):
|
2016-05-04 11:19:24 +08:00
|
|
|
implicit_wait = 10
|
2018-10-26 01:52:29 +08:00
|
|
|
external_host = None
|
|
|
|
|
|
|
|
@classproperty
|
|
|
|
def live_server_url(cls):
|
|
|
|
return 'http://%s:%s' % (cls.external_host or cls.host, cls.server_thread.port)
|
|
|
|
|
|
|
|
@classproperty
|
|
|
|
def allowed_host(cls):
|
|
|
|
return cls.external_host or cls.host
|
2016-02-07 10:24:36 +08:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def setUpClass(cls):
|
|
|
|
cls.selenium = cls.create_webdriver()
|
2016-05-04 11:19:24 +08:00
|
|
|
cls.selenium.implicitly_wait(cls.implicit_wait)
|
2017-01-21 21:13:44 +08:00
|
|
|
super().setUpClass()
|
2016-02-07 10:24:36 +08:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _tearDownClassInternal(cls):
|
|
|
|
# quit() the WebDriver before attempting to terminate and join the
|
|
|
|
# single-threaded LiveServerThread to avoid a dead lock if the browser
|
|
|
|
# kept a connection alive.
|
|
|
|
if hasattr(cls, 'selenium'):
|
|
|
|
cls.selenium.quit()
|
2017-01-21 21:13:44 +08:00
|
|
|
super()._tearDownClassInternal()
|
2016-05-04 11:19:24 +08:00
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
def disable_implicit_wait(self):
|
2017-01-25 04:37:33 +08:00
|
|
|
"""Disable the default implicit wait."""
|
2016-05-04 11:19:24 +08:00
|
|
|
self.selenium.implicitly_wait(0)
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
self.selenium.implicitly_wait(self.implicit_wait)
|