"""Base test case classes."""
import unittest
from functools import wraps
from selenium import webdriver
from webdriver_test_tools.config import WebDriverConfig
from webdriver_test_tools.common import utils
from webdriver_test_tools.webdriver.support import test
[docs]class WebDriverTestCase(unittest.TestCase):
"""Base class for web driver test cases.
This defines the common ``setUp()`` and ``tearDown()`` tasks as well as WebDriver-related
assertion methods and ``webdriver_test_tools`` framework-related decorator methods.
It does not initialize ``self.driver`` so will not work on its own. Tests should be
written with this as their parent class. Browser-specific implementations of test cases
will be generated when running tests.
**Instances of this class will have the following variables:**
:var WebDriverTestCase.driver: Selenium WebDriver object
:var WebDriverTestCase.WebDriverConfig: :class:`WebDriverConfig
<webdriver_test_tools.config.webdriver.WebDriverConfig>` class object
**Tests that implement this class override the following variables:**
:var WebDriverTestCase.SITE_URL: Go to this URL during :meth:`setUp()`. Tests that implement
WebDriverTestCase must set this accordingly.
:var WebDriverTestCase.SKIP_BROWSERS: (Optional) List of browser names to skip test
generation for. This can be useful if a test case class requires functionality
that is not implemented in a certain driver, or if its tests are meant for
specific browsers. Valid browser names are declared in the :class:`Browsers
<webdriver_test_tools.testcase.browsers.Browsers>` class.
:var WebDriverTestCase.SKIP_MOBILE: (Optional) By default, tests will be
generated for all enabled browsers, including mobile. If ``SKIP_MOBILE`` is set to
``True``, don't generate tests for mobile browsers. This can be helpful if the
layout changes between desktop and mobile viewports would alter the test
procedures.
:var WebDriverTestCase.DEFAULT_ASSERTION_TIMEOUT: (Optional) Default number
of seconds for :ref:`WebDriverTestCase assertion methods
<assertion-methods>` to wait for expected conditions to occur before
test fails. Defaults to the value of ``DEFAULT_ASSERTION_TIMEOUT`` set
in the test project's :class:`WebDriverConfig
<webdriver_test_tools.config.webdriver.WebDriverConfig>` class
**Browser-specific implementations of this class need to override the following:**
:function WebDriverTestCase.driver_init: Function that returns a Selenium WebDriver
object for the browser
:var WebDriverTestCase.DRIVER_NAME: Name of the browser. This is mostly used in the
docstrings of generated test classes to indicate what browser the tests are
being run in
:var WebDriverTestCase.SHORT_NAME: Short name for the driver used for command line
args, skipping, etc. Should be all lowercase with no spaces
:var WebDriverTestCase.CAPABILITIES: The ``DesiredCapabilities`` dictionary for the
browser. Used for initializing BrowserStack remote driver
**The following attributes are used for running tests on BrowserStack:**
:var WebDriverTestCase.ENABLE_BS: (Default = False) If set to True, :meth:`setUp()` will
initialize a Remote webdriver instead of a local one and run tests on
BrowserStack
:var WebDriverTestCase.COMMAND_EXECUTOR: Command executor URL. Test generator
needs to set this with the configured access key and username
**The following attributes are used for running tests in a headless browser:**
:var WebDriverTestCase.ENABLE_HEADLESS: (Default = False) If set to True, browser
implementations with headless browser support will configure their drivers to
run tests in a headless browser
"""
# Instance variables
driver = None
WebDriverConfig = WebDriverConfig
# Test case attributes
SITE_URL = None
SKIP_BROWSERS = []
SKIP_MOBILE = None
DEFAULT_ASSERTION_TIMEOUT = None
# Browser implementation attributes
DRIVER_NAME = None
SHORT_NAME = None
# BrowserStack attributes
ENABLE_BS = False
COMMAND_EXECUTOR = None
CAPABILITIES = None
# Headless browser attributes
ENABLE_HEADLESS = False
[docs] def bs_driver_init(self):
"""Initialize driver for BrowserStack
:return: ``webdriver.Remote`` object with the ``command_executor`` and
``desired_capabilities`` parameters set to ``self.COMMAND_EXECUTOR``
and ``self.CAPABILITIES`` respectively.
"""
self.CAPABILITIES['name'] = self._testMethodName
return webdriver.Remote(command_executor=self.COMMAND_EXECUTOR,
desired_capabilities=self.CAPABILITIES)
[docs] def driver_init(self):
"""Returns an initialized WebDriver object. Browser test case classes must
implement this.
"""
pass
[docs] def setUp(self):
"""Initialize driver and call ``self.driver.get(self.SITE_URL)``
If ``self.ENABLE_BS`` is ``False``, ``self.driver`` gets the returned
results of :meth:`self.driver_init() <WebDriverTestCase.driver_init>`.
If ``self.ENABLE_BS`` is ``True``, ``self.driver`` gets the returned
results of :meth:`self.bs_driver_init()
<WebDriverTestCase.bs_driver_init>`
Also checks if ``self.DEFAULT_ASSERTION_TIMEOUT`` is set and defaults
to ``self.WebDriverConfig.DEFAULT_ASSERTION_TIMEOUT`` if it's
unspecified
"""
self.driver = self.bs_driver_init() if self.ENABLE_BS else self.driver_init()
if not self.DEFAULT_ASSERTION_TIMEOUT or not isinstance(self.DEFAULT_ASSERTION_TIMEOUT, int):
self.DEFAULT_ASSERTION_TIMEOUT = self.WebDriverConfig.DEFAULT_ASSERTION_TIMEOUT
self.driver.get(self.SITE_URL)
[docs] def tearDown(self):
"""Calls ``self.driver.quit()``"""
self.driver.quit()
# Assertion methods
def _locator_string(self, locator):
"""Shorthand for formating locator tuple as a string for failure output
:param locator: WebDriver locator tuple in the format ``(By.<attr>, <locator string>)``
"""
return '("{0}", "{1}")'.format(*locator)
[docs] def assertExists(self, element_locator, msg=None, wait_timeout=None):
"""Fail if element doesn't exist
:param element_locator: webdriver locator tuple in the format
``(by.<attr>, <locator string>)``
:param msg: (Optional) if specified, used as the error message on
failure
:param wait_timeout: (Default = ``self.DEFAULT_ASSERTION_TIMEOUT``)
Number of seconds to wait for expected conditions to occur before
test fails
"""
wait_timeout = wait_timeout or self.DEFAULT_ASSERTION_TIMEOUT
if not test.existence_change_test(self.driver, element_locator, test_exists=True, wait_timeout=wait_timeout):
failure_message = 'No elements located using ' + self._locator_string(element_locator)
msg = self._formatMessage(msg, failure_message)
raise self.failureException(msg)
[docs] def assertNotExists(self, element_locator, msg=None, wait_timeout=None):
"""Fail if element exists
:param element_locator: webdriver locator tuple in the format
``(by.<attr>, <locator string>)``
:param msg: (Optional) if specified, used as the error message on
failure
:param wait_timeout: (Default = ``self.DEFAULT_ASSERTION_TIMEOUT``)
Number of seconds to wait for expected conditions to occur before
test fails
"""
wait_timeout = wait_timeout or self.DEFAULT_ASSERTION_TIMEOUT
if not test.existence_change_test(self.driver, element_locator, test_exists=False, wait_timeout=wait_timeout):
failure_message = 'Elements located using ' + self._locator_string(element_locator)
msg = self._formatMessage(msg, failure_message)
raise self.failureException(msg)
[docs] def assertInView(self, element_locator, msg=None, wait_timeout=None):
"""Fail if element isn't scrolled into view
:param element_locator: webdriver locator tuple in the format
``(by.<attr>, <locator string>)``
:param msg: (Optional) if specified, used as the error message on
failure
:param wait_timeout: (Default = ``self.DEFAULT_ASSERTION_TIMEOUT``)
Number of seconds to wait for expected conditions to occur before
test fails
"""
wait_timeout = wait_timeout or self.DEFAULT_ASSERTION_TIMEOUT
if not test.in_view_change_test(self.driver, element_locator, wait_timeout=wait_timeout):
failure_message = 'Element is not scrolled into view'
msg = self._formatMessage(msg, failure_message)
raise self.failureException(msg)
[docs] def assertNotInView(self, element_locator, msg=None, wait_timeout=None):
"""Fail if element is scrolled into view
:param element_locator: webdriver locator tuple in the format
``(by.<attr>, <locator string>)``
:param msg: (Optional) if specified, used as the error message on
failure
:param wait_timeout: (Default = ``self.DEFAULT_ASSERTION_TIMEOUT``)
Number of seconds to wait for expected conditions to occur before
test fails
"""
wait_timeout = wait_timeout or self.DEFAULT_ASSERTION_TIMEOUT
if test.in_view_change_test(self.driver, element_locator, wait_timeout=wait_timeout):
failure_message = 'Element is scrolled into view'
msg = self._formatMessage(msg, failure_message)
raise self.failureException(msg)
[docs] def assertVisible(self, element_locator, msg=None, wait_timeout=None):
"""Fail if element isn't visible
:param element_locator: webdriver locator tuple in the format
``(by.<attr>, <locator string>)``
:param msg: (Optional) if specified, used as the error message on
failure
:param wait_timeout: (Default = ``self.DEFAULT_ASSERTION_TIMEOUT``)
Number of seconds to wait for expected conditions to occur before
test fails
"""
wait_timeout = wait_timeout or self.DEFAULT_ASSERTION_TIMEOUT
if not test.visibility_change_test(self.driver, element_locator, wait_timeout=wait_timeout):
failure_message = 'Element is not visible'
msg = self._formatMessage(msg, failure_message)
raise self.failureException(msg)
[docs] def assertInvisible(self, element_locator, msg=None, wait_timeout=None):
"""Fail if element is visible
:param element_locator: webdriver locator tuple in the format
``(by.<attr>, <locator string>)``
:param msg: (Optional) if specified, used as the error message on
failure
:param wait_timeout: (Default = ``self.DEFAULT_ASSERTION_TIMEOUT``)
Number of seconds to wait for expected conditions to occur before
test fails
"""
wait_timeout = wait_timeout or self.DEFAULT_ASSERTION_TIMEOUT
if not test.visibility_change_test(self.driver, element_locator, test_visible=False, wait_timeout=wait_timeout):
failure_message = 'Element is visible'
msg = self._formatMessage(msg, failure_message)
raise self.failureException(msg)
[docs] def assertEnabled(self, element_locator, msg=None, wait_timeout=None):
"""Fail if element is disabled
:param element_locator: webdriver locator tuple in the format
``(by.<attr>, <locator string>)``
:param msg: (Optional) if specified, used as the error message on
failure
:param wait_timeout: (Default = ``self.DEFAULT_ASSERTION_TIMEOUT``)
Number of seconds to wait for expected conditions to occur before
test fails
"""
wait_timeout = wait_timeout or self.DEFAULT_ASSERTION_TIMEOUT
if not test.enabled_state_change_test(self.driver, element_locator, test_enabled=True, wait_timeout=wait_timeout):
failure_message = 'Element is disabled'
msg = self._formatMessage(msg, failure_message)
raise self.failureException(msg)
[docs] def assertDisabled(self, element_locator, msg=None, wait_timeout=None):
"""Fail if element is enabled
:param element_locator: webdriver locator tuple in the format
``(by.<attr>, <locator string>)``
:param msg: (Optional) if specified, used as the error message on
failure
:param wait_timeout: (Default = ``self.DEFAULT_ASSERTION_TIMEOUT``)
Number of seconds to wait for expected conditions to occur before
test fails
"""
wait_timeout = wait_timeout or self.DEFAULT_ASSERTION_TIMEOUT
if not test.enabled_state_change_test(self.driver, element_locator, test_enabled=False, wait_timeout=wait_timeout):
failure_message = 'Element is enabled'
msg = self._formatMessage(msg, failure_message)
raise self.failureException(msg)
[docs] def assertUrlChange(self, expected_url, msg=None, wait_timeout=None):
"""Fail if the URL doesn't match the expected URL.
Assertion uses webdriver_test_tools.test.url_change_test() using the
specified ``wait_timeout`` before determining that expected_url does
not match the current URL.
:param expected_url: The expected URL
:param msg: (Optional) if specified, used as the error message on
failure
:param wait_timeout: (Default = ``self.DEFAULT_ASSERTION_TIMEOUT``)
Number of seconds to wait for expected conditions to occur before
test fails
"""
wait_timeout = wait_timeout or self.DEFAULT_ASSERTION_TIMEOUT
if not test.url_change_test(self.driver, expected_url, wait_timeout=wait_timeout):
failure_message = 'Current URL = {}, expected URL = {}'.format(
self.driver.current_url, expected_url
)
msg = self._formatMessage(msg, failure_message)
raise self.failureException(msg)
[docs] def assertBaseUrlChange(self, expected_url, ignore_trailing_slash=True, msg=None, wait_timeout=None):
"""Fail if the URL (ignoring query strings) doesn't match the expected
URL.
Assertion uses webdriver_test_tools.test.url_change_test() using the
specified ``wait_timeout`` before determining that expected_url does
not match the current URL.
:param expected_url: The expected URL
:param ignore_trailing_slash: (Default = True) If True, ignore trailing
'/' in the expected url and current base URL when comparing
:param msg: (Optional) if specified, used as the error message on
failure
:param wait_timeout: (Default = ``self.DEFAULT_ASSERTION_TIMEOUT``)
Number of seconds to wait for expected conditions to occur before
test fails
"""
wait_timeout = wait_timeout or self.DEFAULT_ASSERTION_TIMEOUT
if not test.base_url_change_test(self.driver, expected_url,
ignore_trailing_slash=ignore_trailing_slash, wait_timeout=wait_timeout):
failure_message = 'Current base URL = {}, expected base URL = {}'.format(
utils.get_base_url(self.driver.current_url), expected_url
)
msg = self._formatMessage(msg, failure_message)
raise self.failureException(msg)
# Skipping Browsers
[docs] @staticmethod
def skipBrowsers(*browsers):
"""Conditionally skip a test method for certain browsers
Usage Example:
.. code:: python
@WebDriverTestCase.skipBrowsers(Browsers.SAFARI, Browsers.IE)
test_method(self):
...
"""
def decorator(test_method):
@wraps(test_method)
def wrapper(*args, **kwargs):
test_case_obj = args[0]
if test_case_obj.SHORT_NAME in browsers:
test_case_obj.skipTest('Skipping {}'.format(test_case_obj.DRIVER_NAME))
test_method(*args, **kwargs)
return wrapper
return decorator
[docs] @staticmethod
def skipMobile():
"""Conditionally skip a test method for mobile browsers
Usage Example:
.. code:: python
@WebDriverTestCase.skipMobile()
test_method(self):
...
"""
def decorator(test_method):
@wraps(test_method)
def wrapper(*args, **kwargs):
test_case_obj = args[0]
if issubclass(type(test_case_obj), WebDriverMobileTestCase):
test_case_obj.skipTest('Skipping for mobile')
test_method(*args, **kwargs)
return wrapper
return decorator
[docs] @staticmethod
def mobileOnly():
"""Conditionally skip a test method for non-mobile browsers
Usage Example:
.. code:: python
@WebDriverTestCase.mobileOnly()
test_method(self):
...
"""
def decorator(test_method):
@wraps(test_method)
def wrapper(*args, **kwargs):
test_case_obj = args[0]
if not issubclass(type(test_case_obj), WebDriverMobileTestCase):
test_case_obj.skipTest('Skipping for non-mobile')
test_method(*args, **kwargs)
return wrapper
return decorator
# Screenshots
[docs] def takeScreenshot(self, print_filename=False):
"""Save a screenshot using
:meth:`self.WebDriverConfig.new_screenshot_file
<webdriver_test_tools.config.webdriver.WebDriverConfig.new_screenshot_file>`
:param print_filename: (Default = False) If True, print the path to the
new file to standard out
:return: Path to the new screenshot file
"""
screenshot_file = self.WebDriverConfig.new_screenshot_file(self.SHORT_NAME, self._testMethodName)
self.driver.get_screenshot_as_file(screenshot_file)
if print_filename:
print('Screenshot taken: ' + screenshot_file)
return screenshot_file
[docs] @staticmethod
def screenshotOnFail():
"""Decorator for test methods that takes a screenshot if an assertion
fails. See :meth:`WebDriverConfig.new_screenshot_file
<webdriver_test_tools.config.webdriver.WebDriverConfig.new_screenshot_file>`
for details on filename and output directory
Usage Example:
.. code:: python
@WebDriverTestCase.screenshotOnFail()
test_method(self):
...
self.assertTrue(condition)
...
.. note::
Currently, this method does not take a screenshot for assertions
that fail within a subTest. Since subTests are designed to
continue test execution if an assertion fails, they don't raise
exceptions outside of their context.
"""
def decorator(test_method):
@wraps(test_method)
def wrapper(self, *args, **kwargs):
try:
test_method(self, *args, **kwargs)
except self.failureException as e:
screenshot_file = self.takeScreenshot()
# TODO: update error message to include screenshot path
raise
return wrapper
return decorator
# Misc Utility Methods
[docs] def is_mobile(self):
"""Check whether the test case is running in a mobile browser
:return: True if running in a mobile browser (or emulated one), False
if running in a desktop
"""
# WebDriverMobileTestCase overrides this method and returns True
return False
[docs]class WebDriverMobileTestCase(WebDriverTestCase):
"""Base class for mobile web driver test cases
If a test subclasses ``WebDriverMobileTestCase`` instead of
``WebDriverTestCase``, tests will only be generated for mobile browsers
"""
SKIP_MOBILE = False
[docs] def is_mobile(self):
return True