1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
# frozen_string_literal: true
module Capybara
module Node
##
#
# A {Capybara::Node::Base} represents either an element on a page through the subclass
# {Capybara::Node::Element} or a document through {Capybara::Node::Document}.
#
# Both types of Node share the same methods, used for interacting with the
# elements on the page. These methods are divided into three categories,
# finders, actions and matchers. These are found in the modules
# {Capybara::Node::Finders}, {Capybara::Node::Actions} and {Capybara::Node::Matchers}
# respectively.
#
# A {Capybara::Session} exposes all methods from {Capybara::Node::Document} directly:
#
# session = Capybara::Session.new(:rack_test, my_app)
# session.visit('/')
# session.fill_in('Foo', with: 'Bar') # from Capybara::Node::Actions
# bar = session.find('#bar') # from Capybara::Node::Finders
# bar.select('Baz', from: 'Quox') # from Capybara::Node::Actions
# session.has_css?('#foobar') # from Capybara::Node::Matchers
#
class Base
attr_reader :session, :base, :query_scope
include Capybara::Node::Finders
include Capybara::Node::Actions
include Capybara::Node::Matchers
def initialize(session, base)
@session = session
@base = base
end
# overridden in subclasses, e.g. Capybara::Node::Element
def reload
self
end
##
#
# This method is Capybara's primary defence against asynchronicity
# problems. It works by attempting to run a given block of code until it
# succeeds. The exact behaviour of this method depends on a number of
# factors. Basically there are certain exceptions which, when raised
# from the block, instead of bubbling up, are caught, and the block is
# re-run.
#
# Certain drivers, such as RackTest, have no support for asynchronous
# processes, these drivers run the block, and any error raised bubbles up
# immediately. This allows faster turn around in the case where an
# expectation fails.
#
# Only exceptions that are {Capybara::ElementNotFound} or any subclass
# thereof cause the block to be rerun. Drivers may specify additional
# exceptions which also cause reruns. This usually occurs when a node is
# manipulated which no longer exists on the page. For example, the
# Selenium driver specifies
# `Selenium::WebDriver::Error::ObsoleteElementError`.
#
# As long as any of these exceptions are thrown, the block is re-run,
# until a certain amount of time passes. The amount of time defaults to
# {Capybara.default_max_wait_time} and can be overridden through the `seconds`
# argument. This time is compared with the system time to see how much
# time has passed. On rubies/platforms which don't support access to a monotonic process clock
# if the return value of `Time.now` is stubbed out, Capybara will raise `Capybara::FrozenInTime`.
#
# @param [Integer] seconds (current sessions default_max_wait_time) Maximum number of seconds to retry this block
# @param [Array<Exception>] errors (driver.invalid_element_errors +
# [Capybara::ElementNotFound]) exception types that cause the block to be rerun
# @return [Object] The result of the given block
# @raise [Capybara::FrozenInTime] If the return value of `Time.now` appears stuck
#
def synchronize(seconds = nil, errors: nil)
return yield if session.synchronized
seconds = session_options.default_max_wait_time if [nil, true].include? seconds
interval = session_options.default_retry_interval
session.synchronized = true
timer = Capybara::Helpers.timer(expire_in: seconds)
begin
yield
rescue StandardError => e
session.raise_server_error!
raise e unless catch_error?(e, errors)
if driver.wait?
raise e if timer.expired?
sleep interval
reload if session_options.automatic_reload
else
old_base = @base
reload if session_options.automatic_reload
raise e if old_base == @base
end
retry
ensure
session.synchronized = false
end
end
# @api private
def find_css(css, **options)
if base.method(:find_css).arity == 1
base.find_css(css)
else
base.find_css(css, **options)
end
end
# @api private
def find_xpath(xpath, **options)
if base.method(:find_xpath).arity == 1
base.find_xpath(xpath)
else
base.find_xpath(xpath, **options)
end
end
# @api private
def session_options
session.config
end
def to_capybara_node
self
end
protected
def catch_error?(error, errors = nil)
errors ||= (driver.invalid_element_errors + [Capybara::ElementNotFound])
errors.any? { |type| error.is_a?(type) }
end
def driver
session.driver
end
end
end
end
|