Per-session config and thread specific current_driver/session_name

This commit is contained in:
Thomas Walpole 2016-12-15 09:04:01 -08:00
parent 7f8914b605
commit 0270b7a4cf
36 changed files with 705 additions and 204 deletions

View File

@ -67,6 +67,7 @@ You can read more about the missing features [here](https://github.com/teamcapyb
- [Beware the XPath // trap](#beware-the-xpath--trap)
- [Configuring and adding drivers](#configuring-and-adding-drivers)
- [Gotchas:](#gotchas)
- ["Threadsafe" mode](#threadsafe)
- [Development](#development)
## <a name="key-benefits"></a>Key benefits
@ -1048,6 +1049,31 @@ additional info about how the underlying driver can be configured.
are testing for specific server errors and using multiple sessions make sure to test for the
errors using the initial session (usually :default)
## <a name="threadsafe"></a>"Threadsafe" mode - BETA - may change
In normal mode most of Capybara's configuration options are global settings which can cause issues
if using multiple sessions and wanting to change a setting for only one of the sessions. To provide
support for this type of usage Capybara now provides a "threadsafe" mode which can be enabled by setting
Capybara.threadsafe = true
This setting can only be changed before any sessions have been created. In "threadsafe" mode the following
behaviors of Capybara change
* Most options can now be set on a session. These can either be set at session creation time or after, and
default to the global options at the time of session creation. Options which are NOT session specific are
`app`, `reuse_server`, `default_driver`, `javascript_driver`, and (obviously) `threadsafe`. Any drivers and servers
registered through `register_driver` and `register_server` are also global.
my_session = Capybara::Session.new(:driver, some_app) do |config|
config.automatic_label_click = true # only set for my_session
end
my_session.config.default_max_wait_time = 10 # only set for my_session
Capybara.default_max_wait_time = 2 # will not change the default_max_wait in my_session
* `current_driver` and `session_name` are thread specific. This means that `using_session' and
`using_driver` also only affect the current thread.
## <a name="development"></a>Development
To set up a development environment, simply do:

View File

@ -2,6 +2,8 @@
require 'timeout'
require 'nokogiri'
require 'xpath'
require 'forwardable'
require 'capybara/config'
module Capybara
class CapybaraError < StandardError; end
@ -19,18 +21,41 @@ module Capybara
class WindowError < CapybaraError; end
class ReadOnlyElementError < CapybaraError; end
class << self
attr_reader :app_host, :default_host
attr_accessor :asset_host, :run_server, :always_include_port
attr_accessor :server_port, :exact, :match, :exact_options, :visible_text_only, :enable_aria_label
attr_accessor :default_selector, :default_max_wait_time, :ignore_hidden_elements
attr_accessor :save_path, :wait_on_first_by_default, :automatic_label_click, :automatic_reload
attr_reader :reuse_server
attr_accessor :raise_server_errors, :server_errors
attr_writer :default_driver, :current_driver, :javascript_driver, :session_name, :server_host
attr_reader :save_and_open_page_path
attr_accessor :exact_text
attr_accessor :app
extend Forwardable
# DelegateCapybara global configurations
# @!method app
# See {Capybara#configure}
# @!method reuse_server
# See {Capybara#configure}
# @!method threadsafe
# See {Capybara#configure}
# @!method server
# See {Capybara#configure}
# @!method default_driver
# See {Capybara#configure}
# @!method javascript_driver
# See {Capybara#configure}
Config::OPTIONS.each do |method|
def_delegators :config, method, "#{method}="
end
# Delegate Capybara global configurations
# @!method default_selector
# See {Capybara#configure}
# @!method default_max_wait_time
# See {Capybara#configure}
# @!method app_host
# See {Capybara#configure}
# @!method always_include_port
# See {Capybara#configure}
# @!method wait_on_first_by_default
# See {Capybara#configure}
SessionConfig::OPTIONS.each do |method|
def_delegators :config, method, "#{method}="
end
##
#
@ -58,6 +83,9 @@ module Capybara
# [automatic_label_click = Boolean] Whether Node#choose, Node#check, Node#uncheck will attempt to click the associated label element if the checkbox/radio button are non-visible (Default: false)
# [enable_aria_label = Boolean] Whether fields, links, and buttons will match against aria-label attribute (Default: false)
# [reuse_server = Boolean] Reuse the server thread between multiple sessions using the same app object (Default: true)
# [threadsafe = Boolean] Whether sessions can be configured individually (Default: false)
# [server = Symbol] The name of the registered server to use when running the app under test (Default: :webrick)
#
# === DSL Options
#
# when using capybara/dsl, the following options are also available:
@ -66,7 +94,7 @@ module Capybara
# [javascript_driver = Symbol] The name of a driver to use for JavaScript enabled tests. (Default: :selenium)
#
def configure
yield self
yield ConfigureDeprecator.new(config)
end
##
@ -163,45 +191,6 @@ module Capybara
@servers ||= {}
end
##
#
# Register a proc that Capybara will call to run the Rack application.
#
# Capybara.server do |app, port, host|
# require 'rack/handler/mongrel'
# Rack::Handler::Mongrel.run(app, :Port => port)
# end
#
# By default, Capybara will try to run webrick.
#
# @yield [app, port, host] This block receives a rack app, port, and host/ip and should run a Rack handler
#
def server(&block)
if block_given?
warn "DEPRECATED: Passing a block to Capybara::server is deprecated, please use Capybara::register_server instead"
@server = block
else
@server
end
end
##
#
# Set the server to use.
#
# Capybara.server = :webrick
#
# @param [Symbol] name Name of the server type to use
# @see register_server
#
def server=(name)
@server = if name.respond_to? :call
name
else
servers[name.to_sym]
end
end
##
#
# Wraps the given string, which should contain an HTML document or fragment
@ -251,29 +240,25 @@ module Capybara
servers[:webrick].call(app, port, server_host)
end
##
#
# @return [Symbol] The name of the driver to use by default
#
def default_driver
@default_driver || :rack_test
end
##
#
# @return [Symbol] The name of the driver currently in use
#
def current_driver
@current_driver || default_driver
if threadsafe
Thread.current['capybara_current_driver']
else
@current_driver
end || default_driver
end
alias_method :mode, :current_driver
##
#
# @return [Symbol] The name of the driver used when JavaScript is needed
#
def javascript_driver
@javascript_driver || :selenium
def current_driver=(name)
if threadsafe
Thread.current['capybara_current_driver'] = name
else
@current_driver = name
end
end
##
@ -281,7 +266,7 @@ module Capybara
# Use the default driver as the current driver
#
def use_default_driver
@current_driver = nil
self.current_driver = nil
end
##
@ -293,15 +278,7 @@ module Capybara
Capybara.current_driver = driver
yield
ensure
@current_driver = previous_driver
end
##
#
# @return [String] The IP address bound by default server
#
def server_host
@server_host || '127.0.0.1'
self.current_driver = previous_driver
end
##
@ -344,7 +321,19 @@ module Capybara
# @return [Symbol] The name of the currently used session.
#
def session_name
@session_name ||= :default
if threadsafe
Thread.current['capybara_session_name'] ||= :default
else
@session_name ||= :default
end
end
def session_name=(name)
if threadsafe
Thread.current['capybara_session_name'] = name
else
@session_name = name
end
end
##
@ -352,11 +341,19 @@ module Capybara
# Yield a block using a specific session name.
#
def using_session(name)
previous_session_name = self.session_name
previous_session_info = {
session_name: session_name,
current_driver: current_driver,
app: app
}
self.session_name = name
yield
ensure
self.session_name = previous_session_name
self.session_name = previous_session_info[:session_name]
if threadsafe
self.current_driver = previous_session_info[:current_driver]
self.app = previous_session_info[:app]
end
end
##
@ -374,32 +371,8 @@ module Capybara
end
end
# @deprecated Use default_max_wait_time instead
def default_wait_time
deprecate('default_wait_time', 'default_max_wait_time', true)
default_max_wait_time
end
# @deprecated Use default_max_wait_time= instead
def default_wait_time=(t)
deprecate('default_wait_time=', 'default_max_wait_time=')
self.default_max_wait_time = t
end
def save_and_open_page_path=(path)
warn "DEPRECATED: #save_and_open_page_path is deprecated, please use #save_path instead. \n"\
"Note: Behavior is slightly different with relative paths - see documentation" unless path.nil?
@save_and_open_page_path = path
end
def app_host=(url)
raise ArgumentError.new("Capybara.app_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
@app_host = url
end
def default_host=(url)
raise ArgumentError.new("Capybara.default_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
@default_host = url
def session_options
config.session_options
end
def included(base)
@ -407,18 +380,10 @@ module Capybara
warn "`include Capybara` is deprecated. Please use `include Capybara::DSL` instead."
end
def reuse_server=(bool)
warn "Capybara.reuse_server == false is a BETA feature and may change in a future version" unless bool
@reuse_server = bool
end
def deprecate(method, alternate_method, once=false)
@deprecation_notified ||= {}
warn "DEPRECATED: ##{method} is deprecated, please use ##{alternate_method} instead" unless once and @deprecation_notified[method]
@deprecation_notified[method]=true
end
private
def config
@config ||= Capybara::Config.new
end
def session_pool
@session_pool ||= {}

121
lib/capybara/config.rb Normal file
View File

@ -0,0 +1,121 @@
# frozen_string_literal: true
require 'forwardable'
require 'capybara/session/config'
module Capybara
class Config
extend Forwardable
OPTIONS = [:app, :reuse_server, :threadsafe, :default_wait_time, :server, :default_driver, :javascript_driver]
attr_accessor :app
attr_reader :reuse_server, :threadsafe
attr_reader :session_options
attr_writer :default_driver, :javascript_driver
SessionConfig::OPTIONS.each do |method|
def_delegators :session_options, method, "#{method}="
end
def initialize
@session_options = Capybara::SessionConfig.new
end
def reuse_server=(bool)
@reuse_server = bool
end
def threadsafe=(bool)
warn "Capybara.threadsafe == true is a BETA feature and may change in future minor versions" if bool
raise "Threadsafe setting cannot be changed once a session is created" if (bool != threadsafe) && Session.instance_created?
@threadsafe = bool
end
##
#
# Return the proc that Capybara will call to run the Rack application.
# The block returned receives a rack app, port, and host/ip and should run a Rack handler
# By default, Capybara will try to run webrick.
#
def server(&block)
if block_given?
warn "DEPRECATED: Passing a block to Capybara::server is deprecated, please use Capybara::register_server instead"
@server = block
else
@server
end
end
##
#
# Set the server to use.
#
# Capybara.server = :webrick
#
# @param [Symbol] name Name of the server type to use
# @see register_server
#
def server=(name)
@server = if name.respond_to? :call
name
else
Capybara.servers[name.to_sym]
end
end
##
#
# @return [Symbol] The name of the driver to use by default
#
def default_driver
@default_driver || :rack_test
end
##
#
# @return [Symbol] The name of the driver used when JavaScript is needed
#
def javascript_driver
@javascript_driver || :selenium
end
# @deprecated Use default_max_wait_time instead
def default_wait_time
deprecate('default_wait_time', 'default_max_wait_time', true)
default_max_wait_time
end
# @deprecated Use default_max_wait_time= instead
def default_wait_time=(t)
deprecate('default_wait_time=', 'default_max_wait_time=')
self.default_max_wait_time = t
end
def deprecate(method, alternate_method, once=false)
@deprecation_notified ||= {}
warn "DEPRECATED: ##{method} is deprecated, please use ##{alternate_method} instead" unless once and @deprecation_notified[method]
@deprecation_notified[method]=true
end
end
class ConfigureDeprecator
def initialize(config)
@config = config
end
def method_missing(m, *args, &block)
if @config.respond_to?(m)
@config.public_send(m, *args, &block)
elsif Capybara.respond_to?(m)
warn "Calling #{m} from Capybara.configure is deprecated - please call it on Capybara directly ( Capybara.#{m}(...) )"
Capybara.public_send(m, *args, &block)
else
super
end
end
def respond_to_missing?(m, include_private = false)
@config.respond_to_missing?(m, include_private) || Capybara.respond_to_missing?(m, include_private)
end
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class Capybara::Driver::Base
attr_writer :session_options
def current_url
raise NotImplementedError
end
@ -139,6 +141,10 @@ class Capybara::Driver::Base
false
end
def session_options
@session_options || Capybara.session_options
end
# @deprecated This method is being removed
def browser_initialized?
warn "DEPRECATED: #browser_initialized? is deprecated and will be removed in the next version of Capybara"

View File

@ -21,12 +21,10 @@ module Capybara
Capybara.using_session(name, &block)
end
##
#
# Shortcut to using a different wait time.
#
def using_wait_time(seconds, &block)
Capybara.using_wait_time(seconds, &block)
page.using_wait_time(seconds, &block)
end
##

View File

@ -46,11 +46,11 @@ module Capybara
# @param [String] html HTML code to inject into
# @return [String] The modified HTML code
#
def inject_asset_host(html)
if Capybara.asset_host && Nokogiri::HTML(html).css("base").empty?
def inject_asset_host(html, asset_host = Capybara.asset_host)
if asset_host && Nokogiri::HTML(html).css("base").empty?
match = html.match(/<head[^<]*?>/)
if match
return html.clone.insert match.end(0), "<base href='#{Capybara.asset_host}' />"
return html.clone.insert match.end(0), "<base href='#{asset_host}' />"
end
end

View File

@ -293,9 +293,9 @@ module Capybara
def _check_with_label(selector, checked, locator, options)
locator, options = nil, locator if locator.is_a? Hash
allow_label_click = options.delete(:allow_label_click) { Capybara.automatic_label_click }
allow_label_click = options.delete(:allow_label_click) { session_options.automatic_label_click }
synchronize(Capybara::Queries::BaseQuery::wait(options)) do
synchronize(Capybara::Queries::BaseQuery.wait(options, session_options.default_max_wait_time)) do
begin
el = find(selector, locator, options)
el.set(checked)

View File

@ -74,7 +74,7 @@ module Capybara
# @return [Object] The result of the given block
# @raise [Capybara::FrozenInTime] If the return value of `Time.now` appears stuck
#
def synchronize(seconds=Capybara.default_max_wait_time, options = {})
def synchronize(seconds=session_options.default_max_wait_time, options = {})
start_time = Capybara::Helpers.monotonic_time
if session.synchronized
@ -90,7 +90,7 @@ module Capybara
raise e if (Capybara::Helpers.monotonic_time - start_time) >= seconds
sleep(0.05)
raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead" if Capybara::Helpers.monotonic_time == start_time
reload if Capybara.automatic_reload
reload if session_options.automatic_reload
retry
ensure
session.synchronized = false
@ -114,6 +114,11 @@ module Capybara
query_scope
end
# @api private
def session_options
session.config
end
protected
def catch_error?(error, errors = nil)

View File

@ -55,7 +55,7 @@ module Capybara
# @return [String] The text of the element
#
def text(type=nil)
type ||= :all unless Capybara.ignore_hidden_elements or Capybara.visible_text_only
type ||= :all unless session_options.ignore_hidden_elements or session_options.visible_text_only
synchronize do
if type == :all
base.all_text

View File

@ -29,11 +29,16 @@ module Capybara
# @raise [Capybara::ElementNotFound] If the element can't be found before time expires
#
def find(*args, &optional_filter_block)
if args.last.is_a? Hash
args.last[:session_options] = session_options
else
args.push(session_options: session_options)
end
query = Capybara::Queries::SelectorQuery.new(*args, &optional_filter_block)
synchronize(query.wait) do
if (query.match == :smart or query.match == :prefer_exact) and query.supports_exact?
if (query.match == :smart or query.match == :prefer_exact)
result = query.resolve_for(self, true)
result = query.resolve_for(self, false) if result.empty? && !query.exact?
result = query.resolve_for(self, false) if result.empty? && query.supports_exact? && !query.exact?
else
result = query.resolve_for(self)
end
@ -208,6 +213,11 @@ module Capybara
# @return [Capybara::Result] A collection of found elements
#
def all(*args, &optional_filter_block)
if args.last.is_a? Hash
args.last[:session_options] = session_options
else
args.push(session_options: session_options)
end
query = Capybara::Queries::SelectorQuery.new(*args, &optional_filter_block)
synchronize(query.wait) do
result = query.resolve_for(self)
@ -233,7 +243,7 @@ module Capybara
# @return [Capybara::Node::Element] The found element or nil
#
def first(*args, &optional_filter_block)
if Capybara.wait_on_first_by_default
if session_options.wait_on_first_by_default
options = if args.last.is_a?(Hash) then args.pop.dup else {} end
args.push({minimum: 1}.merge(options))
end

View File

@ -114,8 +114,8 @@ module Capybara
#
def assert_all_of_selectors(*args, &optional_filter_block)
options = if args.last.is_a?(Hash) then args.pop.dup else {} end
selector = if args.first.is_a?(Symbol) then args.shift else Capybara.default_selector end
wait = options.fetch(:wait, Capybara.default_max_wait_time)
selector = if args.first.is_a?(Symbol) then args.shift else session_options.default_selector end
wait = options.fetch(:wait, session_options.default_max_wait_time)
synchronize(wait) do
args.each do |locator|
assert_selector(selector, locator, options, &optional_filter_block)
@ -140,8 +140,8 @@ module Capybara
#
def assert_none_of_selectors(*args, &optional_filter_block)
options = if args.last.is_a?(Hash) then args.pop.dup else {} end
selector = if args.first.is_a?(Symbol) then args.shift else Capybara.default_selector end
wait = options.fetch(:wait, Capybara.default_max_wait_time)
selector = if args.first.is_a?(Symbol) then args.shift else session_options.default_selector end
wait = options.fetch(:wait, session_options.default_max_wait_time)
synchronize(wait) do
args.each do |locator|
assert_no_selector(selector, locator, options, &optional_filter_block)
@ -680,6 +680,7 @@ module Capybara
private
def _verify_selector_result(query_args, optional_filter_block, &result_block)
_set_query_session_options(query_args)
query = Capybara::Queries::SelectorQuery.new(*query_args, &optional_filter_block)
synchronize(query.wait) do
result = query.resolve_for(self)
@ -689,6 +690,7 @@ module Capybara
end
def _verify_match_result(query_args, optional_filter_block, &result_block)
_set_query_session_options(query_args)
query = Capybara::Queries::MatchQuery.new(*query_args, &optional_filter_block)
synchronize(query.wait) do
result = query.resolve_for(self.query_scope)
@ -698,6 +700,7 @@ module Capybara
end
def _verify_text(query_args)
_set_query_session_options(query_args)
query = Capybara::Queries::TextQuery.new(*query_args)
synchronize(query.wait) do
count = query.resolve_for(self)
@ -706,6 +709,14 @@ module Capybara
return true
end
def _set_query_session_options(query_args)
if query_args.last.is_a? Hash
query_args.last[:session_options] = session_options
else
query_args.push(session_options: session_options)
end
query_args
end
end
end
end

View File

@ -173,6 +173,11 @@ module Capybara
def find_xpath(xpath)
native.xpath(xpath)
end
# @api private
def session_options
Capybara.session_options
end
end
end
end

View File

@ -6,13 +6,18 @@ module Capybara
COUNT_KEYS = [:count, :minimum, :maximum, :between]
attr_reader :options
attr_writer :session_options
def wait
self.class.wait(options)
def session_options
@session_options || Capybara.session_options
end
def self.wait(options)
options.fetch(:wait, Capybara.default_max_wait_time) || 0
def wait
self.class.wait(options, session_options.default_max_wait_time)
end
def self.wait(options, default=Capybara.default_max_wait_time)
options.fetch(:wait, default) || 0
end
##

View File

@ -9,11 +9,13 @@ module Capybara
def initialize(*args, &filter_block)
@options = if args.last.is_a?(Hash) then args.pop.dup else {} end
self.session_options = @options.delete(:session_options)
@filter_block = filter_block
if args[0].is_a?(Symbol)
@selector = Selector.all.fetch(args.shift) do |selector_type|
warn "Unknown selector type (:#{selector_type}), defaulting to :#{Capybara.default_selector} - This will raise an exception in a future version of Capybara"
raise ArgumentError, "Unknown selector type (:#{selector_type})"
nil
end
@locator = args.shift
@ -21,16 +23,16 @@ module Capybara
@selector = Selector.all.values.find { |s| s.match?(args[0]) }
@locator = args.shift
end
@selector ||= Selector.all[Capybara.default_selector]
@selector ||= Selector.all[session_options.default_selector]
warn "Unused parameters passed to #{self.class.name} : #{args.to_s}" unless args.empty?
# for compatibility with Capybara 2.0
if Capybara.exact_options and @selector == Selector.all[:option]
if session_options.exact_options and @selector == Selector.all[:option]
@options[:exact] = true
end
@expression = @selector.call(@locator, @options)
@expression = @selector.call(@locator, @options.merge(enable_aria_label: session_options.enable_aria_label))
warn_exact_usage
@ -89,12 +91,12 @@ module Capybara
end
end
res &&= Capybara.using_wait_time(0){ @filter_block.call(node)} unless @filter_block.nil?
res &&= node.session.using_wait_time(0){ @filter_block.call(node)} unless @filter_block.nil?
res
end
def visible
case (vis = options.fetch(:visible){ @selector.default_visibility })
case (vis = options.fetch(:visible){ @selector.default_visibility(session_options.ignore_hidden_elements) })
when true then :visible
when false then :all
else vis
@ -103,11 +105,11 @@ module Capybara
def exact?
return false if !supports_exact?
options.fetch(:exact, Capybara.exact)
options.fetch(:exact, session_options.exact)
end
def match
options.fetch(:match, Capybara.match)
options.fetch(:match, session_options.match)
end
def xpath(exact=nil)
@ -205,7 +207,7 @@ module Capybara
end
def exact_text
options.fetch(:exact_text, Capybara.exact_text)
options.fetch(:exact_text, session_options.exact_text)
end
end
end

View File

@ -5,13 +5,18 @@ module Capybara
class TextQuery < BaseQuery
def initialize(*args)
@type = (args.first.is_a?(Symbol) || args.first.nil?) ? args.shift : nil
@type = (Capybara.ignore_hidden_elements or Capybara.visible_text_only) ? :visible : :all if @type.nil?
@expected_text, @options = args
# @type = (Capybara.ignore_hidden_elements or Capybara.visible_text_only) ? :visible : :all if @type.nil?
@options = if args.last.is_a?(Hash) then args.pop.dup else {} end
self.session_options = @options.delete(:session_options)
@type = (session_options.ignore_hidden_elements or session_options.visible_text_only) ? :visible : :all if @type.nil?
@expected_text = args.shift
unless @expected_text.is_a?(Regexp)
@expected_text = Capybara::Helpers.normalize_whitespace(@expected_text)
end
@options ||= {}
@search_regexp = Capybara::Helpers.to_regexp(@expected_text, nil, exact?)
warn "Unused parameters passed to #{self.class.name} : #{args.to_s}" unless args.empty?
assert_valid_keys
end
@ -40,7 +45,7 @@ module Capybara
private
def exact?
options.fetch(:exact, Capybara.exact_text)
options.fetch(:exact, session_options.exact_text)
end
def build_message(report_on_invisible)

View File

@ -71,7 +71,7 @@ class Capybara::RackTest::Browser
end
def reset_host!
uri = URI.parse(Capybara.app_host || Capybara.default_host)
uri = URI.parse(driver.session_options.app_host || driver.session_options.default_host)
@current_scheme = uri.scheme
@current_host = uri.host
@current_port = uri.port

View File

@ -7,7 +7,7 @@ module Capybara
attr_reader :failure_message, :failure_message_when_negated
def wrap(actual)
if actual.respond_to?("has_selector?")
@context_el = if actual.respond_to?("has_selector?")
actual
else
Capybara.string(actual.to_s)
@ -33,6 +33,19 @@ module Capybara
@failure_message_when_negated = e.message
return false
end
def session_query_args
if @args.last.is_a? Hash
@args.last[:session_options] = session_options
else
@args.push(session_options: session_options)
end
@args
end
def session_options
@context_el ? @context_el.session_options : Capybara.session_options
end
end
class HaveSelector < Matcher
@ -55,7 +68,7 @@ module Capybara
end
def query
@query ||= Capybara::Queries::SelectorQuery.new(*@args, &@filter_block)
@query ||= Capybara::Queries::SelectorQuery.new(*session_query_args, &@filter_block)
end
end
@ -73,7 +86,7 @@ module Capybara
end
def query
@query ||= Capybara::Queries::MatchQuery.new(*@args)
@query ||= Capybara::Queries::MatchQuery.new(*session_query_args, &@filter_block)
end
end
@ -155,11 +168,12 @@ module Capybara
class BecomeClosed
def initialize(options)
@wait_time = Capybara::Queries::BaseQuery.wait(options)
@options = options
end
def matches?(window)
@window = window
@wait_time = Capybara::Queries::BaseQuery.wait(@options, window.session.config.default_max_wait_time)
start_time = Capybara::Helpers.monotonic_time
while window.exists?
return false if (Capybara::Helpers.monotonic_time - start_time) > @wait_time

View File

@ -139,7 +139,7 @@ Capybara.add_selector(:link) do
XPath.string.n.is(locator) |
XPath.attr(:title).is(locator) |
XPath.descendant(:img)[XPath.attr(:alt).is(locator)]
matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
matchers |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
xpath = xpath[matchers]
end
xpath = [:title].inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
@ -185,14 +185,14 @@ Capybara.add_selector(:button) do
unless locator.nil?
locator = locator.to_s
locator_matches = XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
locator_matches |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
locator_matches |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
input_btn_xpath = input_btn_xpath[locator_matches]
btn_xpath = btn_xpath[locator_matches | XPath.string.n.is(locator) | XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
alt_matches = XPath.attr(:alt).is(locator)
alt_matches |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
alt_matches |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
image_btn_xpath = image_btn_xpath[alt_matches]
end

View File

@ -201,9 +201,9 @@ module Capybara
@default_visibility = default_visibility
end
def default_visibility
def default_visibility(fallback = Capybara.ignore_hidden_elements)
if @default_visibility.nil?
Capybara.ignore_hidden_elements
fallback
else
@default_visibility
end
@ -219,7 +219,7 @@ module Capybara
XPath.attr(:name).equals(locator) |
XPath.attr(:placeholder).equals(locator) |
XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))
attr_matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
attr_matchers |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
locate_xpath = locate_xpath[attr_matchers]
locate_xpath += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)

View File

@ -320,7 +320,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
# Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
# Actual wait time may be longer than specified
wait = Selenium::WebDriver::Wait.new(
timeout: (options[:wait] || Capybara.default_max_wait_time),
timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0 ,
ignore: Selenium::WebDriver::Error::NoAlertPresentError)
begin
wait.until do

View File

@ -25,9 +25,10 @@ module Capybara
attr_accessor :error
def initialize(app)
def initialize(app, server_errors)
@app = app
@counter = Counter.new
@server_errors = server_errors
end
def pending_requests?
@ -41,7 +42,7 @@ module Capybara
@counter.increment
begin
@app.call(env)
rescue *Capybara.server_errors => e
rescue *@server_errors => e
@error = e unless @error
raise e
ensure
@ -59,10 +60,10 @@ module Capybara
attr_reader :app, :port, :host
def initialize(app, port=Capybara.server_port, host=Capybara.server_host)
def initialize(app, port=Capybara.server_port, host=Capybara.server_host, server_errors=Capybara.server_errors)
@app = app
@server_thread = nil # suppress warnings
@host, @port = host, port
@host, @port, @server_errors = host, port, server_errors
@port ||= Capybara::Server.ports[port_key]
@port ||= find_available_port(host)
end
@ -112,7 +113,7 @@ module Capybara
private
def middleware
@middleware ||= Middleware.new(app)
@middleware ||= Middleware.new(app, @server_errors)
end
def port_key

View File

@ -17,6 +17,14 @@ module Capybara
# session = Capybara::Session.new(:culerity)
# session.visit('http://www.google.com')
#
# When Capybara.threadsafe == true the sessions options will be initially set to the
# current values of the global options and a configuration block can be passed to the session initializer.
# For available options see {Capybara::SessionConfig::OPTIONS}
#
# session = Capybara::Session.new(:driver, MyRackApp) do |config|
# config.app_host = "http://my_host.dev"
# end
#
# Session provides a number of methods for controlling the navigation of the page, such as +visit+,
# +current_path, and so on. It also delegate a number of methods to a Capybara::Document, representing
# the current HTML document. This allows interaction:
@ -69,10 +77,15 @@ module Capybara
def initialize(mode, app=nil)
raise TypeError, "The second parameter to Session::new should be a rack app if passed." if app && !app.respond_to?(:call)
@@instance_created = true
@mode = mode
@app = app
if Capybara.run_server and @app and driver.needs_server?
@server = Capybara::Server.new(@app).boot
if block_given?
raise "A configuration block is only accepted when Capybara.threadsafe == true" unless Capybara.threadsafe
yield config if block_given?
end
if config.run_server and @app and driver.needs_server?
@server = Capybara::Server.new(@app, config.server_port, config.server_host, config.server_errors).boot
else
@server = nil
end
@ -85,7 +98,9 @@ module Capybara
other_drivers = Capybara.drivers.keys.map { |key| key.inspect }
raise Capybara::DriverNotFoundError, "no driver called #{mode.inspect} was found, available drivers: #{other_drivers.join(', ')}"
end
Capybara.drivers[mode].call(app)
driver = Capybara.drivers[mode].call(app)
driver.session_options = config
driver
end
end
@ -126,7 +141,7 @@ module Capybara
if @server and @server.error
# Force an explanation for the error being raised as the exception cause
begin
if Capybara.raise_server_errors
if config.raise_server_errors
raise CapybaraError, "Your application server raised an error - It has been raised in your test code because Capybara.raise_server_errors == true"
end
rescue CapybaraError
@ -235,10 +250,10 @@ module Capybara
visit_uri = URI.parse(visit_uri.to_s)
uri_base = if @server
visit_uri.port = @server.port if Capybara.always_include_port && (visit_uri.port == visit_uri.default_port)
URI.parse(Capybara.app_host || "http://#{@server.host}:#{@server.port}")
visit_uri.port = @server.port if config.always_include_port && (visit_uri.port == visit_uri.default_port)
URI.parse(config.app_host || "http://#{@server.host}:#{@server.port}")
else
Capybara.app_host && URI.parse(Capybara.app_host)
config.app_host && URI.parse(config.app_host)
end
# TODO - this is only for compatability with previous 2.x behavior that concatenated
@ -481,7 +496,7 @@ module Capybara
driver.switch_to_window(window.handle)
window
else
wait_time = Capybara::Queries::BaseQuery.wait(options)
wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
document.synchronize(wait_time, errors: [Capybara::WindowError]) do
original_window_handle = driver.current_window_handle
begin
@ -578,7 +593,7 @@ module Capybara
old_handles = driver.window_handles
block.call
wait_time = Capybara::Queries::BaseQuery.wait(options)
wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
document.synchronize(wait_time, errors: [Capybara::WindowError]) do
opened_handles = (driver.window_handles - old_handles)
if opened_handles.size != 1
@ -701,7 +716,7 @@ module Capybara
#
def save_page(path = nil)
path = prepare_path(path, 'html')
File.write(path, Capybara::Helpers.inject_asset_host(body), mode: 'wb')
File.write(path, Capybara::Helpers.inject_asset_host(body, config.asset_host), mode: 'wb')
path
end
@ -786,7 +801,49 @@ module Capybara
scope
end
##
#
# Yield a block using a specific wait time
#
def using_wait_time(seconds)
if Capybara.threadsafe
begin
previous_wait_time = config.default_max_wait_time
config.default_max_wait_time = seconds
yield
ensure
config.default_max_wait_time = previous_wait_time
end
else
Capybara.using_wait_time(seconds) { yield }
end
end
##
#
# Accepts a block to set the configuration options if Capybara.threadsafe == true. Note that some options only have an effect
# if set at initialization time, so look at the configuration block that can be passed to the initializer too
#
def configure
raise "Session configuration is only supported when Capybara.threadsafe == true" unless Capybara.threadsafe
yield config
end
def self.instance_created?
@@instance_created
end
def config
@config ||= if Capybara.threadsafe
Capybara.session_options.dup
else
Capybara::ReadOnlySessionConfig.new(Capybara.session_options)
end
end
private
@@instance_created = false
def accept_modal(type, text_or_options, options, &blk)
driver.accept_modal(type, modal_options(text_or_options, options), &blk)
end
@ -798,7 +855,7 @@ module Capybara
def modal_options(text_or_options, options)
text_or_options, options = nil, text_or_options if text_or_options.is_a?(Hash)
options[:text] ||= text_or_options unless text_or_options.nil?
options[:wait] ||= Capybara.default_max_wait_time
options[:wait] ||= config.default_max_wait_time
options
end
@ -814,10 +871,10 @@ module Capybara
end
def prepare_path(path, extension)
if Capybara.save_path || Capybara.save_and_open_page_path.nil?
path = File.expand_path(path || default_fn(extension), Capybara.save_path)
if config.save_path || config.save_and_open_page_path.nil?
path = File.expand_path(path || default_fn(extension), config.save_path)
else
path = File.expand_path(default_fn(extension), Capybara.save_and_open_page_path) if path.nil?
path = File.expand_path(default_fn(extension), config.save_and_open_page_path) if path.nil?
end
FileUtils.mkdir_p(File.dirname(path))
path

View File

@ -0,0 +1,100 @@
# frozen_string_literal: true
require 'delegate'
module Capybara
class SessionConfig
OPTIONS = [:always_include_port, :run_server, :default_selector, :default_max_wait_time, :ignore_hidden_elements,
:automatic_reload, :match, :exact, :exact_text, :raise_server_errors, :visible_text_only, :wait_on_first_by_default,
:automatic_label_click, :enable_aria_label, :save_path, :exact_options, :asset_host, :default_host, :app_host,
:save_and_open_page_path, :server_host, :server_port, :server_errors]
attr_accessor *OPTIONS
##
#@!method always_include_port
# See {Capybara#configure}
#@!method run_server
# See {Capybara#configure}
#@!method default_selector
# See {Capybara#configure}
#@!method default_max_wait_time
# See {Capybara#configure}
#@!method ignore_hidden_elements
# See {Capybara#configure}
#@!method automatic_reload
# See {Capybara#configure}
#@!method match
# See {Capybara#configure}
#@!method exact
# See {Capybara#configure}
#@!method raise_server_errors
# See {Capybara#configure}
#@!method visible_text_only
# See {Capybara#configure}
#@!method wait_on_first_by_default
# See {Capybara#configure}
#@!method automatic_label_click
# See {Capybara#configure}
#@!method enable_aria_label
# See {Capybara#configure}
#@!method save_path
# See {Capybara#configure}
#@!method exact_options
# See {Capybara#configure}
#@!method asset_host
# See {Capybara#configure}
#@!method default_host
# See {Capybara#configure}
#@!method app_host
# See {Capybara#configure}
#@!method save_and_open_page_path
# See {Capybara#configure}
#@!method server_host
# See {Capybara#configure}
#@!method server_port
# See {Capybara#configure}
#@!method server_errors
# See {Capybara#configure}
##
#
# @return [String] The IP address bound by default server
#
def server_host
@server_host || '127.0.0.1'
end
def server_errors=(errors)
(@server_errors ||= []).replace(errors.dup)
end
def app_host=(url)
raise ArgumentError.new("Capybara.app_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
@app_host = url
end
def default_host=(url)
raise ArgumentError.new("Capybara.default_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
@default_host = url
end
def save_and_open_page_path=(path)
warn "DEPRECATED: #save_and_open_page_path is deprecated, please use #save_path instead. \n"\
"Note: Behavior is slightly different with relative paths - see documentation" unless path.nil?
@save_and_open_page_path = path
end
def initialize_copy(other)
super
@server_errors = @server_errors.dup
end
end
class ReadOnlySessionConfig < SimpleDelegator
SessionConfig::OPTIONS.each do |m|
define_method "#{m}=" do |val|
raise "Per session settings are only supported when Capybara.threadsafe == true"
end
end
end
end

View File

@ -58,7 +58,7 @@ $(function() {
$('#change-title').click(function() {
setTimeout(function() {
$('title').text('changed title')
}, 250)
}, 400)
});
$('#click-test').on({
dblclick: function() {

View File

@ -66,6 +66,17 @@ Capybara::SpecHelper.spec "#all" do
Capybara.ignore_hidden_elements = false
expect(@session.all(:css, "a.simple").size).to eq(2)
end
context "with per session config", requires: [:psc] do
it "should use the sessions ignore_hidden_elements", psc: true do
Capybara.ignore_hidden_elements = true
@session.config.ignore_hidden_elements = false
expect(Capybara.ignore_hidden_elements).to eq(true)
expect(@session.all(:css, "a.simple").size).to eq(2)
@session.config.ignore_hidden_elements = true
expect(@session.all(:css, "a.simple").size).to eq(1)
end
end
end
context 'with element count filters' do

View File

@ -18,10 +18,18 @@ Capybara::SpecHelper.spec '#assert_all_of_selectors' do
@session.assert_all_of_selectors("p a#foo", "h2#h2two", "h2#h2one" )
end
it "should respect scopes" do
@session.within "//p[@id='first']" do
@session.assert_all_of_selectors(".//a[@id='foo']")
expect { @session.assert_all_of_selectors(".//a[@id='red']") }.to raise_error(Capybara::ElementNotFound)
context "should respect scopes" do
it "when used with `within`" do
@session.within "//p[@id='first']" do
@session.assert_all_of_selectors(".//a[@id='foo']")
expect { @session.assert_all_of_selectors(".//a[@id='red']") }.to raise_error(Capybara::ElementNotFound)
end
end
it "when called on elements" do
el = @session.find "//p[@id='first']"
el.assert_all_of_selectors(".//a[@id='foo']")
expect { el.assert_all_of_selectors(".//a[@id='red']") }.to raise_error(Capybara::ElementNotFound)
end
end
@ -65,10 +73,18 @@ Capybara::SpecHelper.spec '#assert_none_of_selectors' do
expect { @session.assert_none_of_selectors("abbr", "p a#foo") }.to raise_error(Capybara::ElementNotFound)
end
it "should respect scopes" do
@session.within "//p[@id='first']" do
expect { @session.assert_none_of_selectors(".//a[@id='foo']") }.to raise_error(Capybara::ElementNotFound)
@session.assert_none_of_selectors(".//a[@id='red']")
context "should respect scopes" do
it "when used with `within`" do
@session.within "//p[@id='first']" do
expect { @session.assert_none_of_selectors(".//a[@id='foo']") }.to raise_error(Capybara::ElementNotFound)
@session.assert_none_of_selectors(".//a[@id='red']")
end
end
it "when called on an element" do
el = @session.find "//p[@id='first']"
expect { el.assert_none_of_selectors(".//a[@id='foo']") }.to raise_error(Capybara::ElementNotFound)
el.assert_none_of_selectors(".//a[@id='red']")
end
end

View File

@ -421,8 +421,9 @@ Capybara::SpecHelper.spec '#find' do
end
end
it "should warn if selector type is unknown" do
expect_any_instance_of(Kernel).to receive(:warn).with(/^Unknown selector type/)
@session.find(:unknown, '//h1')
it "should raise if selector type is unknown" do
expect do
@session.find(:unknown, '//h1')
end.to raise_error(ArgumentError)
end
end

View File

@ -88,7 +88,7 @@ Capybara::SpecHelper.spec '#reset_session!' do
end
it "raises configured errors caught inside the server", requires: [:server] do
prev_errors = Capybara.server_errors
prev_errors = Capybara.server_errors.dup
Capybara.server_errors = [LoadError]
quietly { @session.visit("/error") }

View File

@ -60,18 +60,18 @@ Capybara::SpecHelper.spec '#become_closed', requires: [:windows, :js] do
end
context 'with not_to' do
it 'should raise error if default_max_wait_time is more than timeout' do
it "should not raise error if window doesn't close before default_max_wait_time" do
@session.within_window @other_window do
@session.execute_script('setTimeout(function(){ window.close(); }, 700);')
@session.execute_script('setTimeout(function(){ window.close(); }, 1000);')
end
Capybara.using_wait_time 0.4 do
Capybara.using_wait_time 0.3 do
expect do
expect(@other_window).not_to become_closed
end
end
end
it 'should raise error if default_max_wait_time is more than timeout' do
it 'should raise error if window closes before default_max_wait_time' do
@session.within_window @other_window do
@session.execute_script('setTimeout(function(){ window.close(); }, 700);')
end

View File

@ -39,6 +39,7 @@ module Capybara
Capybara.match = :smart
Capybara.wait_on_first_by_default = false
Capybara.enable_aria_label = false
reset_threadsafe
end
def filter(requires, metadata)
@ -64,9 +65,19 @@ module Capybara
before do
@session = session
end
after do
@session.reset_session!
end
before :each, psc: true do
SpecHelper.reset_threadsafe(true, @session)
end
after psc: true do
SpecHelper.reset_threadsafe(false, @session)
end
specs.each do |spec_name, spec_options, block|
describe spec_name, spec_options do
class_eval(&block)
@ -74,6 +85,13 @@ module Capybara
end
end
end
def reset_threadsafe(bool = false, session = nil)
Capybara::Session.class_variable_set(:@@instance_created, false) # Work around limit on when threadsafe can be changed
Capybara.threadsafe = bool
session = session.current_session if session.respond_to?(:current_session)
session.instance_variable_set(:@config, nil) if session
end
end # class << self
def silence_stream(stream)

View File

@ -115,7 +115,7 @@ module Capybara
private
def wait_for_stable_size(seconds=Capybara.default_max_wait_time)
def wait_for_stable_size(seconds=session.config.default_max_wait_time)
res = yield if block_given?
prev_size = size
start_time = Capybara::Helpers.monotonic_time

View File

@ -14,8 +14,8 @@ RSpec.describe Capybara do
end
it "should be accesible as the deprecated default_wait_time" do
expect(Capybara).to receive(:warn).ordered.with('DEPRECATED: #default_wait_time= is deprecated, please use #default_max_wait_time= instead')
expect(Capybara).to receive(:warn).ordered.with('DEPRECATED: #default_wait_time is deprecated, please use #default_max_wait_time instead')
expect(Capybara.send(:config)).to receive(:warn).ordered.with('DEPRECATED: #default_wait_time= is deprecated, please use #default_max_wait_time= instead')
expect(Capybara.send(:config)).to receive(:warn).ordered.with('DEPRECATED: #default_wait_time is deprecated, please use #default_max_wait_time instead')
@previous_default_time = Capybara.default_max_wait_time
Capybara.default_wait_time = 5
expect(Capybara.default_wait_time).to eq(5)
@ -114,6 +114,18 @@ RSpec.describe Capybara do
expect { Capybara.default_host = "http://www.example.com" }.not_to raise_error
end
end
describe "configure" do
it 'deprecates calling non configuration option methods in configure' do
expect_any_instance_of(Kernel).to receive(:warn).
with('Calling register_driver from Capybara.configure is deprecated - please call it on Capybara directly ( Capybara.register_driver(...) )')
Capybara.configure do |config|
config.register_driver(:random_name) do
#just a random block
end
end
end
end
end
RSpec.describe Capybara::Session do

View File

@ -16,6 +16,7 @@ Capybara::SpecHelper.run_specs TestClass.new, "DSL", capybara_skip: [
:server,
:hover,
:about_scheme,
:psc
]
RSpec.describe Capybara::DSL do

View File

@ -0,0 +1,67 @@
# frozen_string_literal: true
require 'spec_helper'
require 'capybara/dsl'
RSpec.describe Capybara::SessionConfig do
describe "threadsafe" do
it "defaults to global session options" do
Capybara.threadsafe = true
session = Capybara::Session.new(:rack_test, TestApp)
[:default_host, :app_host, :save_and_open_page_path,
:always_include_port, :run_server, :default_selector, :default_max_wait_time, :ignore_hidden_elements,
:automatic_reload, :match, :exact, :raise_server_errors, :visible_text_only, :wait_on_first_by_default,
:automatic_label_click, :enable_aria_label,
:save_path, :exact_options, :asset_host].each do |m|
expect(session.config.public_send(m)).to eq Capybara.public_send(m)
end
end
it "doesn't change global session when changed" do
Capybara.threadsafe = true
host = "http://my.example.com"
session = Capybara::Session.new(:rack_test, TestApp) do |config|
config.default_host = host
config.automatic_label_click = !config.automatic_label_click
config.server_errors << ArgumentError
end
expect(Capybara.default_host).not_to eq host
expect(session.config.default_host).to eq host
expect(Capybara.automatic_label_click).not_to eq session.config.automatic_label_click
expect(Capybara.server_errors).not_to eq session.config.server_errors
end
it "doesn't allow session configuration block when false" do
Capybara.threadsafe = false
expect do
Capybara::Session.new(:rack_test, TestApp) { |config| }
end.to raise_error "A configuration block is only accepted when Capybara.threadsafe == true"
end
it "doesn't allow session config when false" do
Capybara.threadsafe = false
session = Capybara::Session.new(:rack_test, TestApp)
expect { session.config.default_selector = :title }.to raise_error /Per session settings are only supported when Capybara.threadsafe == true/
expect do
session.configure do |config|
config.exact = true
end
end.to raise_error /Session configuration is only supported when Capybara.threadsafe == true/
end
it "uses the config from the session" do
Capybara.threadsafe = true
session = Capybara::Session.new(:rack_test, TestApp) do |config|
config.default_selector = :link
end
session.visit('/with_html')
expect(session.find('foo').tag_name).to eq 'a'
end
it "won't change threadsafe once a session is created" do
Capybara.threadsafe = true
Capybara.threadsafe = false
session = Capybara::Session.new(:rack_test, TestApp)
expect { Capybara.threadsafe = true }.to raise_error /Threadsafe setting cannot be changed once a session is created/
end
end
end

View File

@ -555,8 +555,8 @@ RSpec.shared_examples Capybara::RSpecMatchers do |session, mode|
it "doesn't wait if wait time is less than timeout" do
@session.click_link("Change title")
using_wait_time 0 do
expect(@session).not_to have_title('changed title')
using_wait_time 3 do
expect(@session).not_to have_title('changed title', wait: 0)
end
end
end

View File

@ -7,4 +7,48 @@ RSpec.describe Capybara::Session do
Capybara::Session.new(:unknown, { random: "hash"})
end.to raise_error TypeError, "The second parameter to Session::new should be a rack app if passed."
end
context "current_driver" do
it "is global when threadsafe false" do
Capybara.threadsafe = false
Capybara.current_driver = :selenium
thread = Thread.new do
Capybara.current_driver = :random
end
thread.join
expect(Capybara.current_driver).to eq :random
end
it "is thread specific threadsafe true" do
Capybara.threadsafe = true
Capybara.current_driver = :selenium
thread = Thread.new do
Capybara.current_driver = :random
end
thread.join
expect(Capybara.current_driver).to eq :selenium
end
end
context "session_name" do
it "is global when threadsafe false" do
Capybara.threadsafe = false
Capybara.session_name = "sess1"
thread = Thread.new do
Capybara.session_name = "sess2"
end
thread.join
expect(Capybara.session_name).to eq "sess2"
end
it "is thread specific when threadsafe true" do
Capybara.threadsafe = true
Capybara.session_name = "sess1"
thread = Thread.new do
Capybara.session_name = "sess2"
end
thread.join
expect(Capybara.session_name).to eq "sess1"
end
end
end