Source code for capybara.session

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)