import atexit
from contextlib import contextmanager
from selenium.common.exceptions import (
NoAlertPresentException,
NoSuchWindowException,
TimeoutException,
UnexpectedAlertPresentException,
WebDriverException)
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from time import sleep
import capybara
from capybara.driver.base import Base
from capybara.exceptions import ExpectationNotMet, ModalNotFound
from capybara.helpers import desc, Timer, toregex
from capybara.selenium.browser import get_browser
from capybara.selenium.node import Node
from capybara.utils import cached_property, isregex
[docs]class Driver(Base):
"""
A Capybara driver that uses Selenium WebDriver to drive a real browser.
Args:
app (object): The WSGI-compliant app to drive.
browser (str, optional): The name of the browser to use. Defaults to "firefox".
clear_local_storage (bool, optional): Whether to clear local storage on reset.
Defaults to False.
clear_session_storage (bool, optional): Whether to clear session storage on
reset. Defaults to False.
desired_capabilities (Dict[str, str | bool], optional): Desired
capabilities of the underlying browser. Defaults to a set of
reasonable defaults provided by Selenium.
options: Arbitrary keyword arguments for the underlying Selenium driver.
"""
def __init__(
self,
app,
browser="firefox",
clear_local_storage=False,
clear_session_storage=False,
desired_capabilities=None,
**options
):
self.app = app
self._browser_name = browser
self._clear_local_storage = clear_local_storage
self._clear_session_storage = clear_session_storage
self._desired_capabilities = desired_capabilities
self._options = options
self._frame_handles = []
@property
def needs_server(self):
return True
[docs] @cached_property
def browser(self):
capabilities = self._desired_capabilities
if self._firefox:
capabilities = (capabilities or DesiredCapabilities.FIREFOX).copy()
# Auto-accept unload alerts triggered by navigating away.
if capabilities.get("marionette"):
capabilities["unhandledPromptBehavior"] = "dismiss"
else:
capabilities["unexpectedAlertBehaviour"] = "ignore"
browser = get_browser(self._browser_name, capabilities=capabilities, **self._options)
atexit.register(browser.quit)
return browser
@property
def current_url(self):
return self.browser.current_url
@property
def title(self):
return self.browser.title
@property
def html(self):
return self.browser.page_source
@property
def text(self):
return self.browser.text
[docs] def switch_to_frame(self, frame):
if frame == "top":
self._frame_handles = []
self.browser.switch_to.default_content()
elif frame == "parent":
self._frame_handles.pop()
self.browser.switch_to.default_content()
for frame_handle in self._frame_handles:
self.browser.switch_to.frame(frame_handle)
else:
self._frame_handles.append(frame.native)
self.browser.switch_to.frame(frame.native)
@property
def current_window_handle(self):
return self.browser.current_window_handle
[docs] def window_size(self, handle):
with self._window(handle):
size = self.browser.get_window_size()
return [size["width"], size["height"]]
[docs] def resize_window_to(self, handle, width, height):
with self._window(handle):
try:
self.browser.set_window_size(width, height)
except WebDriverException as e:
if self._chrome and "failed to change window state" in str(e):
# Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
# and raises unnecessary error. Wait a bit and try again.
sleep(0.5)
self.browser.set_window_size(width, height)
else:
raise
[docs] def maximize_window(self, handle):
with self._window(handle):
self.browser.maximize_window()
[docs] def fullscreen_window(self, handle):
with self._window(handle):
self.browser.fullscreen_window()
[docs] def close_window(self, handle):
with self._window(handle):
self.browser.close()
@property
def window_handles(self):
return self.browser.window_handles
[docs] def open_new_window(self):
self.browser.execute_script("window.open();")
[docs] def switch_to_window(self, handle):
self.browser.switch_to.window(handle)
@property
def no_such_window_error(self):
return NoSuchWindowException
[docs] def visit(self, url):
self.browser.get(url)
[docs] def refresh(self):
try:
with self.accept_modal(None, wait=0.1):
self.browser.refresh()
except ModalNotFound:
pass
[docs] def go_back(self):
self.browser.back()
[docs] def go_forward(self):
self.browser.forward()
[docs] def execute_script(self, script, *args):
args = [arg.native if isinstance(arg, Node) else arg for arg in args]
return self.browser.execute_script(script, *args)
[docs] def evaluate_script(self, script, *args):
result = self.execute_script("return {0}".format(script.strip()), *args)
return self._wrap_element_script_result(result)
[docs] def evaluate_async_script(self, script, *args):
self.browser.set_script_timeout(capybara.default_max_wait_time)
args = [arg.native if isinstance(arg, Node) else arg for arg in args]
result = self.browser.execute_async_script(script, *args)
return self._wrap_element_script_result(result)
[docs] def save_screenshot(self, path, **kwargs):
self.browser.get_screenshot_as_file(path)
[docs] @contextmanager
def accept_modal(self, modal_type, text=None, response=None, wait=None):
yield
modal = self._find_modal(text=text, wait=wait)
if response:
modal.send_keys(response)
modal.accept()
[docs] @contextmanager
def dismiss_modal(self, modal_type, text=None, wait=None):
yield
modal = self._find_modal(text=text, wait=wait)
modal.dismiss()
[docs] def reset(self):
# Avoid starting the browser just to reset the session.
if "browser" in self.__dict__:
navigated = False
timer = Timer(10)
while True:
try:
# Only trigger a navigation if we haven't done it already,
# otherwise it can trigger an endless series of unload modals.
if not navigated:
self.browser.delete_all_cookies()
self._clear_storage()
self.browser.get("about:blank")
navigated = True
while True:
try:
next(self._find_xpath("/html/body/*"))
except StopIteration:
break
if timer.expired:
raise ExpectationNotMet("Timed out waiting for Selenium session reset")
sleep(0.05)
break
except UnexpectedAlertPresentException:
# This error is thrown if an unhandled alert is on the page.
try:
self.browser.switch_to.alert.accept()
# Allow time for the modal to be handled.
sleep(0.25)
except NoAlertPresentException:
# The alert is now gone.
if self.browser.current_url != "about:blank":
# If navigation has not occurred, Firefox may have dismissed the alert
# before we could accept it.
# Try to navigate again, anticipating the alert this time.
try:
self.browser.get("about:blank")
sleep(0.1) # Wait for the alert.
self.browser.switch_to.alert.accept()
except NoAlertPresentException:
# No alert appeared this time.
pass
# Try cleaning up the browser again.
continue
for handle in self.window_handles:
if handle != self.current_window_handle:
self.close_window(handle)
@property
def invalid_element_errors(self):
return (WebDriverException,)
@property
def _marionette(self):
return self._firefox and self.browser.w3c
@property
def _chrome(self):
return self._browser_name == "chrome"
@property
def _firefox(self):
return self._browser_name in ["ff", "firefox"]
def _clear_storage(self):
if "browser" in self.__dict__:
if self._clear_local_storage:
self.execute_script("window.localStorage.clear()")
if self._clear_session_storage:
self.execute_script("window.sessionStorage.clear()")
def _find_css(self, css):
return (Node(self, element) for element in self.browser.find_elements_by_css_selector(css))
def _find_xpath(self, xpath):
return (Node(self, element) for element in self.browser.find_elements_by_xpath(xpath))
def _find_modal(self, text=None, wait=None):
wait = wait or capybara.default_max_wait_time
try:
alert = WebDriverWait(self.browser, wait).until(EC.alert_is_present())
regexp = toregex(text)
if not regexp.search(alert.text):
qualifier = "matching" if isregex(text) else "with"
raise ModalNotFound("Unable to find modal dialog {0} {1}".format(qualifier, desc(text)))
return alert
except TimeoutException:
raise ModalNotFound("Unable to find modal dialog")
@contextmanager
def _window(self, handle):
original_handle = self.current_window_handle
if handle == original_handle:
yield
else:
self.switch_to_window(handle)
try:
yield
finally:
self.switch_to_window(original_handle)
def _wrap_element_script_result(self, arg):
if isinstance(arg, list):
return [self._wrap_element_script_result(e) for e in arg]
elif isinstance(arg, dict):
return {k: self._wrap_element_script_result(v) for k, v in iter(arg.items())}
elif isinstance(arg, WebElement):
return Node(self, arg)
else:
return arg