from collections import Hashable
from contextlib import contextmanager
from datetime import datetime
from functools import wraps
import os
import random
import capybara
from capybara.compat import ParseResult, urlparse
from capybara.driver.node import Node
from capybara.exceptions import ScopeError, WindowError
from capybara.node.base import Base
from capybara.node.document import Document
from capybara.node.element import Element
from capybara.selector import selectors
from capybara.server import Server
from capybara.session_matchers import SessionMatchersMixin
from capybara.utils import cached_property, encode_string
from capybara.window import Window
_DOCUMENT_METHODS = ["assert_no_title", "assert_title", "has_no_title", "has_title"]
_DOCUMENT_PROPERTIES = ["title"]
_NODE_METHODS = [
"assert_all_of_selectors", "assert_no_selector", "assert_none_of_selectors", "assert_no_text",
"assert_selector", "assert_style", "assert_text", "attach_file", "check", "choose",
"click_button", "click_link", "click_link_or_button", "click_on", "fill_in", "find", "find_all",
"find_button", "find_by_id", "find_field", "find_first", "find_link", "has_all_of_selectors",
"has_button", "has_checked_field", "has_content", "has_css", "has_field", "has_link",
"has_no_button", "has_no_checked_field", "has_no_css", "has_no_field", "has_no_link",
"has_no_select", "has_no_selector", "has_no_table", "has_no_text", "has_no_unchecked_field",
"has_no_xpath", "has_none_of_selectors", "has_select", "has_selector", "has_table", "has_text",
"has_unchecked_field", "has_xpath", "select", "uncheck", "unselect"]
_NODE_PROPERTIES = ["text"]
_SESSION_METHODS = [
"accept_alert", "accept_confirm", "accept_prompt", "assert_current_path",
"assert_no_current_path", "dismiss_confirm", "dismiss_prompt", "evaluate_async_script",
"evaluate_script", "execute_script", "fieldset", "frame", "go_back", "go_forward",
"has_current_path", "has_no_current_path", "open_new_window", "refresh", "reset", "save_page",
"save_screenshot", "scope", "switch_to_frame", "switch_to_window", "table", "visit", "window",
"window_opened_by"]
_SESSION_PROPERTIES = ["current_host", "current_path", "current_url", "current_window", "windows"]
DSL_METHODS = _DOCUMENT_METHODS + _NODE_METHODS + _SESSION_METHODS
[docs]class Session(SessionMatchersMixin, object):
"""
The Session class represents a single user's interaction with the system. The Session can use
any of the underlying drivers. A session can be initialized manually like this::
session = Session("selenium", MyWSGIApp)
The application given as the second argument is optional. When running Capybara against an
external page, you might want to leave it out::
session = Session("selenium")
session.visit("http://www.google.com")
Session provides a number of methods and properties for controlling the navigation of the page,
such as :meth:`visit`, :attr:`current_path`, and so on. It also delegates a number of methods to
a :class:`Document`, representing the current HTML document. This allows interaction::
session.fill_in("q", value="Capybara")
session.click_button("Search")
assert session.has_text("Capybara")
When using :mod:`capybara.dsl`, the Session is initialized automatically for you.
Args:
mode (str): The name of the driver to use.
app (object): The WSGI-compliant app with which to interact.
"""
def __init__(self, mode, app):
self.mode = mode
self.app = app
self.server = Server(app).boot() if app and self.driver.needs_server else None
self.synchronized = False
self._scopes = [None]
[docs] @cached_property
def driver(self):
""" driver.Base: The driver for the current session. """
return capybara.drivers[self.mode](self.app)
[docs] @cached_property
def document(self):
""" Document: The document for the current page. """
return Document(self, self.driver)
@property
def current_scope(self):
""" node.Base: The current node relative to which all interaction will be scoped. """
scope = self._scopes[-1]
if scope in [None, "frame"]:
scope = self.document
return scope
@property
def html(self):
""" str: A snapshot of the DOM of the current document, as it looks right now. """
return self.driver.html
body = html
""" Alias for :attr:`html`. """
source = html
""" Alias for :attr:`html`. """
@property
def current_path(self):
""" str: Path of the current page, without any domain information. """
if not self.current_url:
return
path = urlparse(self.current_url).path
return path if path else None
@property
def current_host(self):
""" str: Host of the current page. """
if not self.current_url:
return
result = urlparse(self.current_url)
scheme, netloc = result.scheme, result.netloc
host = netloc.split(":")[0] if netloc else None
return "{0}://{1}".format(scheme, host) if host else None
@property
def current_url(self):
""" str: Fully qualified URL of the current page. """
return self.driver.current_url
[docs] def visit(self, visit_uri):
"""
Navigate to the given URL. The URL can either be a relative URL or an absolute URL. The
behavior of either depends on the driver. ::
session.visit("/foo")
session.visit("http://google.com")
For drivers which can run against an external application, such as the Selenium driver,
giving an absolute URL will navigate to that page. This allows testing applications running
on remote servers. For these drivers, setting :data:`capybara.app_host` will make the
remote server the default. For example::
capybara.app_host = "http://google.com"
session.visit("/") # visits the Google homepage
Args:
visit_uri (str): The URL to navigate to.
"""
self.raise_server_error()
visit_uri = urlparse(visit_uri)
if capybara.app_host:
uri_base = urlparse(capybara.app_host)
elif self.server:
uri_base = urlparse("http://{}:{}".format(self.server.host, self.server.port))
else:
uri_base = None
visit_uri = ParseResult(
scheme=visit_uri.scheme or (uri_base.scheme if uri_base else ""),
netloc=visit_uri.netloc or (uri_base.netloc if uri_base else ""),
path=visit_uri.path,
params=visit_uri.params,
query=visit_uri.query,
fragment=visit_uri.fragment)
self.driver.visit(visit_uri.geturl())
[docs] def refresh(self):
""" Refresh the page. """
self.raise_server_error()
self.driver.refresh()
[docs] def go_back(self):
""" Move back a single entry in the browser's history. """
self.driver.go_back()
[docs] def go_forward(self):
""" Move forward a single entry in the browser's history. """
self.driver.go_forward()
[docs] @contextmanager
def scope(self, *args, **kwargs):
"""
Executes the wrapped code within the context of a node. ``scope`` takes the same options
as :meth:`find`. For the duration of the context, any command to Capybara will be handled
as though it were scoped to the given element. ::
with scope("xpath", "//div[@id='delivery-address']"):
fill_in("Street", value="12 Main Street")
Just as with :meth:`find`, if multiple elements match the selector given to ``scope``, an
error will be raised, and just as with :meth:`find`, this behavior can be controlled
through the ``match`` and ``exact`` options.
It is possible to omit the first argument, in that case, the selector is assumed to be of
the type set in :data:`capybara.default_selector`. ::
with scope("div#delivery-address"):
fill_in("Street", value="12 Main Street")
Note that a lot of uses of ``scope`` can be replaced more succinctly with chaining::
find("div#delivery-address").fill_in("Street", value="12 Main Street")
Args:
*args: Variable length argument list for the call to :meth:`find`.
**kwargs: Arbitrary keywords arguments for the call to :meth:`find`.
"""
new_scope = args[0] if isinstance(args[0], Base) else self.find(*args, **kwargs)
self._scopes.append(new_scope)
try:
yield
finally:
self._scopes.pop()
[docs] @contextmanager
def fieldset(self, locator):
"""
Execute the wrapped code within a specific fieldset given the id or legend of that
fieldset.
Args:
locator (str): The id or legend of the fieldset.
"""
with self.scope("fieldset", locator):
yield
[docs] @contextmanager
def table(self, locator):
"""
Execute the wrapped code within a specific table given the id or caption of that table.
Args:
locator (str): The id or caption of the table.
"""
with self.scope("table", locator):
yield
[docs] @contextmanager
def frame(self, locator=None, *args, **kwargs):
"""
Execute the wrapped code within the given iframe using the given frame or frame name/id.
May not be supported by all drivers.
Args:
locator (str | Element, optional): The name/id of the frame or the frame's element.
Defaults to the only frame in the document.
"""
self.switch_to_frame(self._find_frame(locator, *args, **kwargs))
try:
yield
finally:
self.switch_to_frame("parent")
def _find_frame(self, locator=None, *args, **kwargs):
if isinstance(locator, Element):
return locator
if isinstance(locator, Hashable) and locator in selectors:
selector = locator
return self.find(selector, *args, **kwargs)
return self.find("frame", locator, **kwargs)
@property
def current_window(self):
""" Window: The current window. """
return Window(self, self.driver.current_window_handle)
@property
def windows(self):
"""
Get all opened windows. The order of the windows in the returned list is not defined. The
driver may sort windows by their creation time but it's not required.
Returns:
List[Window]: A list of all windows.
"""
return [Window(self, window_handle) for window_handle in self.driver.window_handles]
[docs] def open_new_window(self):
"""
Open new window. The current window doesn't change as a result of this call. It should be
switched explicitly.
Returns:
Window: The window that has been opened.
"""
return self.window_opened_by(lambda: self.driver.open_new_window())
[docs] def switch_to_frame(self, frame):
"""
Switch to the given frame.
If you use this method you are responsible for making sure you switch back to the parent
frame when done in the frame changed to. :meth:`frame` is preferred over this method and
should be used when possible. May not be supported by all drivers.
Args:
frame (Element | str): The iframe/frame element to switch to.
"""
if isinstance(frame, Element):
self.driver.switch_to_frame(frame)
self._scopes.append("frame")
elif frame == "parent":
if self._scopes[-1] != "frame":
raise ScopeError("`switch_to_frame(\"parent\")` cannot be called "
"from inside a descendant frame's `scope` context.")
self._scopes.pop()
self.driver.switch_to_frame("parent")
elif frame == "top":
if "frame" in self._scopes:
idx = self._scopes.index("frame")
if any([scope not in ["frame", None] for scope in self._scopes[idx:]]):
raise ScopeError("`switch_to_frame(\"top\")` cannot be called "
"from inside a descendant frame's `scope` context.")
self._scopes = self._scopes[:idx]
self.driver.switch_to_frame("top")
else:
raise ValueError(
"You must provide a frame element, \"parent\", or \"top\" "
"when calling switch_to_frame")
[docs] def switch_to_window(self, window, wait=None):
"""
If ``window`` is a lambda, it switches to the first window for which ``window`` returns a
value other than False or None. If a window that matches can't be found, the window will be
switched back and :exc:`WindowError` will be raised.
Args:
window (Window | lambda): The window that should be switched to, or a filtering lambda.
wait (int | float, optional): The number of seconds to wait to find the window.
Returns:
Window: The new current window.
Raises:
ScopeError: If this method is invoked inside :meth:`scope, :meth:`frame`, or
:meth:`window`.
WindowError: If no window matches the given lambda.
"""
if len(self._scopes) > 1:
raise ScopeError(
"`switch_to_window` is not supposed to be invoked from "
"within `scope`s, `frame`s, or other `window`s.")
if isinstance(window, Window):
self.driver.switch_to_window(window.handle)
return window
else:
@self.document.synchronize(errors=(WindowError,), wait=wait)
def switch_and_get_matching_window():
original_window_handle = self.driver.current_window_handle
try:
for handle in self.driver.window_handles:
self.driver.switch_to_window(handle)
result = window()
if result:
return Window(self, handle)
except Exception:
self.driver.switch_to_window(original_window_handle)
raise
self.driver.switch_to_window(original_window_handle)
raise WindowError("Could not find a window matching lambda")
return switch_and_get_matching_window()
[docs] @contextmanager
def window(self, window):
"""
This method does the following:
1. Switches to the given window (it can be located by window instance/lambda/string).
2. Executes the given block (within window located at previous step).
3. Switches back (this step will be invoked even if exception happens at second step).
Args:
window (Window | lambda): The desired :class:`Window`, or a lambda that will be run in
the context of each open window and returns ``True`` for the desired window.
"""
original = self.current_window
if window != original:
self.switch_to_window(window)
self._scopes.append(None)
try:
yield
finally:
self._scopes.pop()
if original != window:
self.switch_to_window(original)
[docs] def window_opened_by(self, trigger_func, wait=None):
"""
Get the window that has been opened by the passed lambda. It will wait for it to be opened
(in the same way as other Capybara methods wait). It's better to use this method than
``windows[-1]`` `as order of windows isn't defined in some drivers`__.
__ https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html#h_note_10
Args:
trigger_func (func): The function that should trigger the opening of a new window.
wait (int | float, optional): Maximum wait time. Defaults to
:data:`capybara.default_max_wait_time`.
Returns:
Window: The window that has been opened within the lambda.
Raises:
WindowError: If lambda passed to window hasn't opened window or opened more than one
window.
"""
old_handles = set(self.driver.window_handles)
trigger_func()
@self.document.synchronize(wait=wait, errors=(WindowError,))
def get_new_window():
opened_handles = set(self.driver.window_handles) - old_handles
if len(opened_handles) != 1:
raise WindowError("lambda passed to `window_opened_by` "
"opened {0} windows instead of 1".format(len(opened_handles)))
return Window(self, list(opened_handles)[0])
return get_new_window()
[docs] def execute_script(self, script, *args):
"""
Execute the given script, not returning a result. This is useful for scripts that return
complex objects, such as jQuery statements. ``execute_script`` should be used over
:meth:`evaluate_script` whenever possible.
Args:
script (str): A string of JavaScript to execute.
*args: Variable length argument list to pass to the executed JavaScript string.
"""
args = [arg.base if isinstance(arg, Base) else arg for arg in args]
self.driver.execute_script(script, *args)
[docs] def evaluate_script(self, script, *args):
"""
Evaluate the given JavaScript and return the result. Be careful when using this with
scripts that return complex objects, such as jQuery statements. :meth:`execute_script`
might be a better alternative.
Args:
script (str): A string of JavaScript to evaluate.
*args: Variable length argument list to pass to the executed JavaScript string.
Returns:
object: The result of the evaluated JavaScript (may be driver specific).
"""
args = [arg.base if isinstance(arg, Base) else arg for arg in args]
result = self.driver.evaluate_script(script, *args)
return self._wrap_element_script_result(result)
[docs] def evaluate_async_script(self, script, *args):
"""
Evaluate the given JavaScript and obtain the result from a callback function which will be
passed as the last argument to the script.
Args:
script (str): A string of JavaScript to evaluate.
*args: Variable length argument list to pass to the executed JavaScript string.
Returns:
object: The result of the evaluated JavaScript (may be driver specific).
"""
args = [arg.base if isinstance(arg, Base) else arg for arg in args]
result = self.driver.evaluate_async_script(script, *args)
return self._wrap_element_script_result(result)
[docs] @contextmanager
def accept_alert(self, text=None, wait=None):
"""
Execute the wrapped code, accepting an alert.
Args:
text (str | RegexObject, optional): Text to match against the text in the modal.
wait (int | float, optional): Maximum time to wait for the modal to appear after
executing the wrapped code.
Raises:
ModalNotFound: If a modal dialog hasn't been found.
"""
wait = wait or capybara.default_max_wait_time
with self.driver.accept_modal("alert", text=text, wait=wait):
yield
[docs] @contextmanager
def accept_confirm(self, text=None, wait=None):
"""
Execute the wrapped code, accepting a confirm.
Args:
text (str | RegexObject, optional): Text to match against the text in the modal.
wait (int | float, optional): Maximum time to wait for the modal to appear after
executing the wrapped code.
Raises:
ModalNotFound: If a modal dialog hasn't been found.
"""
with self.driver.accept_modal("confirm", text=text, wait=wait):
yield
[docs] @contextmanager
def dismiss_confirm(self, text=None, wait=None):
"""
Execute the wrapped code, dismissing a confirm.
Args:
text (str | RegexObject, optional): Text to match against the text in the modal.
wait (int | float, optional): Maximum time to wait for the modal to appear after
executing the wrapped code.
Raises:
ModalNotFound: If a modal dialog hasn't been found.
"""
with self.driver.dismiss_modal("confirm", text=text, wait=wait):
yield
[docs] @contextmanager
def accept_prompt(self, text=None, response=None, wait=None):
"""
Execute the wrapped code, accepting a prompt, optionally responding to the prompt.
Args:
text (str | RegexObject, optional): Text to match against the text in the modal.
response (str, optional): Response to provide to the prompt.
wait (int | float, optional): Maximum time to wait for the modal to appear after
executing the wrapped code.
Raises:
ModalNotFound: If a modal dialog hasn't been found.
"""
with self.driver.accept_modal("prompt", text=text, response=response, wait=wait):
yield
[docs] @contextmanager
def dismiss_prompt(self, text=None, wait=None):
"""
Execute the wrapped code, dismissing a prompt.
Args:
text (str | RegexObject, optional): Text to match against the text in the modal.
wait (int | float, optional): Maximum time to wait for the modal to appear after
executing the wrapped code.
Raises:
ModalNotFound: If a modal dialog hasn't been found.
"""
with self.driver.dismiss_modal("prompt", text=text, wait=wait):
yield
[docs] def save_page(self, path=None):
"""
Save a snapshot of the page.
If invoked without arguments, it will save a file to :data:`capybara.save_path` and the
file will be given a randomly generated filename. If invoked with a relative path, the path
will be relative to :data:`capybara.save_path`.
Args:
path (str, optional): The path to where it should be saved.
Returns:
str: The path to which the file was saved.
"""
path = _prepare_path(path, "html")
with open(path, "wb") as f:
f.write(encode_string(self.body))
return path
[docs] def save_screenshot(self, path=None, **kwargs):
"""
Save a screenshot of the page.
If invoked without arguments, it will save a file to :data:`capybara.save_path` and the
file will be given a randomly generated filename. If invoked with a relative path, the path
will be relative to :data:`capybara.save_path`.
Args:
path (str, optional): The path to where it should be saved.
**kwargs: Arbitrary keywords arguments for the driver.
Returns:
str: The path to which the file was saved.
"""
path = _prepare_path(path, "png")
self.driver.save_screenshot(path, **kwargs)
return path
[docs] def reset(self):
"""
Reset the session (i.e., remove cookies and navigate to a blank page).
This method does not:
* accept modal dialogs if they are present (the Selenium driver does, but others may not),
* clear the browser cache/HTML 5 local storage/IndexedDB/Web SQL database/etc., or
* modify the state of the driver/underlying browser in any other way
as doing so would result in performance downsides and it's not needed to do everything
from the list above for most apps.
If you want to do anything from the list above on a general basis you can write a test
teardown method.
"""
self.driver.reset()
if self.server:
self.server.wait_for_pending_requests()
self.raise_server_error()
[docs] def raise_server_error(self):
""" Raise errors encountered by the server. """
if self.server and self.server.error:
try:
if capybara.raise_server_errors:
raise self.server.error
finally:
self.server.reset_error()
cleanup = reset
""" Alias for :meth:`reset`. """
reset_session = reset
""" Alias for :meth:`reset`. """
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, Node):
return Element(self, arg, None, None)
else:
return arg
def _define_document_method(method_name):
@wraps(getattr(Document, method_name))
def func(self, *args, **kwargs):
return getattr(self.document, method_name)(*args, **kwargs)
setattr(Session, method_name, func)
def _define_document_property(property_name):
def fget(self):
return getattr(self.document, property_name)
fdoc = getattr(Document, property_name).__doc__
setattr(Session, property_name, property(fget, None, None, fdoc))
def _define_node_method(method_name):
@wraps(getattr(Base, method_name))
def func(self, *args, **kwargs):
return getattr(self.current_scope, method_name)(*args, **kwargs)
setattr(Session, method_name, func)
def _define_node_property(property_name):
def fget(self):
return getattr(self.current_scope, property_name)
fdoc = getattr(Base, property_name).__doc__
setattr(Session, property_name, property(fget, None, None, fdoc))
for method_name in _DOCUMENT_METHODS:
_define_document_method(method_name)
for property_name in _DOCUMENT_PROPERTIES:
_define_document_property(property_name)
for method_name in _NODE_METHODS:
_define_node_method(method_name)
for property_name in _NODE_PROPERTIES:
_define_node_property(property_name)
def _prepare_path(path, extension):
save_path = capybara.save_path or os.getcwd()
path = os.path.normpath(os.path.join(save_path, path or _default_fn(extension)))
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
return path
def _default_fn(extension):
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
return "capybara-{timestamp}{random}.{extension}".format(
timestamp=timestamp,
random=random.randint(0, 10**10),
extension=extension)