capybara 2.13.0 → 2.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +82 -17
  3. data/README.md +29 -0
  4. data/lib/capybara.rb +81 -116
  5. data/lib/capybara/config.rb +121 -0
  6. data/lib/capybara/cucumber.rb +1 -0
  7. data/lib/capybara/driver/base.rb +6 -0
  8. data/lib/capybara/dsl.rb +1 -3
  9. data/lib/capybara/helpers.rb +3 -3
  10. data/lib/capybara/node/actions.rb +2 -2
  11. data/lib/capybara/node/base.rb +7 -2
  12. data/lib/capybara/node/element.rb +7 -1
  13. data/lib/capybara/node/finders.rb +13 -3
  14. data/lib/capybara/node/matchers.rb +15 -4
  15. data/lib/capybara/node/simple.rb +5 -0
  16. data/lib/capybara/queries/base_query.rb +8 -3
  17. data/lib/capybara/queries/selector_query.rb +11 -9
  18. data/lib/capybara/queries/text_query.rb +9 -4
  19. data/lib/capybara/rack_test/browser.rb +8 -5
  20. data/lib/capybara/rspec.rb +3 -1
  21. data/lib/capybara/rspec/matcher_proxies.rb +41 -0
  22. data/lib/capybara/rspec/matchers.rb +19 -5
  23. data/lib/capybara/selector.rb +13 -4
  24. data/lib/capybara/selector/selector.rb +3 -3
  25. data/lib/capybara/selenium/driver.rb +20 -6
  26. data/lib/capybara/selenium/node.rb +6 -2
  27. data/lib/capybara/server.rb +6 -5
  28. data/lib/capybara/session.rb +71 -14
  29. data/lib/capybara/session/config.rb +100 -0
  30. data/lib/capybara/spec/public/test.js +1 -1
  31. data/lib/capybara/spec/session/all_spec.rb +11 -0
  32. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +24 -8
  33. data/lib/capybara/spec/session/fill_in_spec.rb +6 -0
  34. data/lib/capybara/spec/session/find_field_spec.rb +1 -0
  35. data/lib/capybara/spec/session/find_spec.rb +4 -3
  36. data/lib/capybara/spec/session/has_selector_spec.rb +1 -3
  37. data/lib/capybara/spec/session/node_spec.rb +23 -17
  38. data/lib/capybara/spec/session/reset_session_spec.rb +1 -1
  39. data/lib/capybara/spec/session/window/become_closed_spec.rb +4 -4
  40. data/lib/capybara/spec/spec_helper.rb +22 -0
  41. data/lib/capybara/spec/views/form.erb +6 -1
  42. data/lib/capybara/spec/views/with_html.erb +1 -0
  43. data/lib/capybara/version.rb +1 -1
  44. data/lib/capybara/window.rb +1 -1
  45. data/spec/capybara_spec.rb +14 -2
  46. data/spec/dsl_spec.rb +1 -0
  47. data/spec/per_session_config_spec.rb +67 -0
  48. data/spec/rspec/shared_spec_matchers.rb +2 -2
  49. data/spec/rspec/views_spec.rb +4 -0
  50. data/spec/rspec_spec.rb +77 -0
  51. data/spec/session_spec.rb +44 -0
  52. data/spec/shared_selenium_session.rb +9 -0
  53. data/spec/spec_helper.rb +4 -0
  54. metadata +7 -3
@@ -5,13 +5,18 @@ module Capybara
5
5
  class TextQuery < BaseQuery
6
6
  def initialize(*args)
7
7
  @type = (args.first.is_a?(Symbol) || args.first.nil?) ? args.shift : nil
8
- @type = (Capybara.ignore_hidden_elements or Capybara.visible_text_only) ? :visible : :all if @type.nil?
9
- @expected_text, @options = args
8
+ # @type = (Capybara.ignore_hidden_elements or Capybara.visible_text_only) ? :visible : :all if @type.nil?
9
+ @options = if args.last.is_a?(Hash) then args.pop.dup else {} end
10
+ self.session_options = @options.delete(:session_options)
11
+
12
+ @type = (session_options.ignore_hidden_elements or session_options.visible_text_only) ? :visible : :all if @type.nil?
13
+
14
+ @expected_text = args.shift
10
15
  unless @expected_text.is_a?(Regexp)
11
16
  @expected_text = Capybara::Helpers.normalize_whitespace(@expected_text)
12
17
  end
13
- @options ||= {}
14
18
  @search_regexp = Capybara::Helpers.to_regexp(@expected_text, nil, exact?)
19
+ warn "Unused parameters passed to #{self.class.name} : #{args.to_s}" unless args.empty?
15
20
  assert_valid_keys
16
21
  end
17
22
 
@@ -40,7 +45,7 @@ module Capybara
40
45
  private
41
46
 
42
47
  def exact?
43
- options.fetch(:exact, Capybara.exact_text)
48
+ options.fetch(:exact, session_options.exact_text)
44
49
  end
45
50
 
46
51
  def build_message(report_on_invisible)
@@ -45,10 +45,13 @@ class Capybara::RackTest::Browser
45
45
  def process(method, path, attributes = {}, env = {})
46
46
  new_uri = URI.parse(path)
47
47
  method.downcase! unless method.is_a? Symbol
48
-
49
- new_uri.path = request_path if path.start_with?("?")
50
- new_uri.path = "/" if new_uri.path.empty?
51
- new_uri.path = request_path.sub(%r(/[^/]*$), '/') + new_uri.path unless new_uri.path.start_with?('/')
48
+ if path.empty?
49
+ new_uri.path = request_path
50
+ else
51
+ new_uri.path = request_path if path.start_with?("?")
52
+ new_uri.path = "/" if new_uri.path.empty?
53
+ new_uri.path = request_path.sub(%r(/[^/]*$), '/') + new_uri.path unless new_uri.path.start_with?('/')
54
+ end
52
55
  new_uri.scheme ||= @current_scheme
53
56
  new_uri.host ||= @current_host
54
57
  new_uri.port ||= @current_port unless new_uri.default_port == @current_port
@@ -68,7 +71,7 @@ class Capybara::RackTest::Browser
68
71
  end
69
72
 
70
73
  def reset_host!
71
- uri = URI.parse(Capybara.app_host || Capybara.default_host)
74
+ uri = URI.parse(driver.session_options.app_host || driver.session_options.default_host)
72
75
  @current_scheme = uri.scheme
73
76
  @current_host = uri.host
74
77
  @current_port = uri.port
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
- require 'capybara/dsl'
3
2
  require 'rspec/core'
3
+ require 'capybara/dsl'
4
4
  require 'capybara/rspec/matchers'
5
5
  require 'capybara/rspec/features'
6
+ require 'capybara/rspec/matcher_proxies'
6
7
 
7
8
  RSpec.configure do |config|
8
9
  config.include Capybara::DSL, :type => :feature
@@ -22,6 +23,7 @@ RSpec.configure do |config|
22
23
  Capybara.use_default_driver
23
24
  end
24
25
  end
26
+
25
27
  config.before do
26
28
  if self.class.include?(Capybara::DSL)
27
29
  example = fetch_current_example.call(self)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ module Capybara
3
+ module RSpecMatcherProxies
4
+ def all(*args)
5
+ if defined?(::RSpec::Matchers::BuiltIn::All) && args.first.respond_to?(:matches?)
6
+ ::RSpec::Matchers::BuiltIn::All.new(*args)
7
+ else
8
+ find_all(*args)
9
+ end
10
+ end
11
+
12
+ def within(*args)
13
+ if block_given?
14
+ within_element(*args, &Proc.new)
15
+ else
16
+ be_within(*args)
17
+ end
18
+ end
19
+ end
20
+
21
+ module DSL
22
+ def self.included(base)
23
+ warn "including Capybara::DSL in the global scope is not recommended!" if base == Object
24
+
25
+ if defined?(::RSpec::Matchers) && base.include?(::RSpec::Matchers)
26
+ base.send(:include, ::Capybara::RSpecMatcherProxies)
27
+ end
28
+
29
+ super
30
+ end
31
+ end
32
+ end
33
+
34
+ if defined?(::RSpec::Matchers)
35
+ module ::RSpec::Matchers
36
+ def self.included(base)
37
+ base.send(:include, ::Capybara::RSpecMatcherProxies) if base.include?(::Capybara::DSL)
38
+ super
39
+ end
40
+ end
41
+ end
@@ -7,7 +7,7 @@ module Capybara
7
7
  attr_reader :failure_message, :failure_message_when_negated
8
8
 
9
9
  def wrap(actual)
10
- if actual.respond_to?("has_selector?")
10
+ @context_el = if actual.respond_to?("has_selector?")
11
11
  actual
12
12
  else
13
13
  Capybara.string(actual.to_s)
@@ -33,6 +33,19 @@ module Capybara
33
33
  @failure_message_when_negated = e.message
34
34
  return false
35
35
  end
36
+
37
+ def session_query_args
38
+ if @args.last.is_a? Hash
39
+ @args.last[:session_options] = session_options
40
+ else
41
+ @args.push(session_options: session_options)
42
+ end
43
+ @args
44
+ end
45
+
46
+ def session_options
47
+ @context_el ? @context_el.session_options : Capybara.session_options
48
+ end
36
49
  end
37
50
 
38
51
  class HaveSelector < Matcher
@@ -55,7 +68,7 @@ module Capybara
55
68
  end
56
69
 
57
70
  def query
58
- @query ||= Capybara::Queries::SelectorQuery.new(*@args, &@filter_block)
71
+ @query ||= Capybara::Queries::SelectorQuery.new(*session_query_args, &@filter_block)
59
72
  end
60
73
  end
61
74
 
@@ -73,7 +86,7 @@ module Capybara
73
86
  end
74
87
 
75
88
  def query
76
- @query ||= Capybara::Queries::MatchQuery.new(*@args)
89
+ @query ||= Capybara::Queries::MatchQuery.new(*session_query_args, &@filter_block)
77
90
  end
78
91
  end
79
92
 
@@ -155,11 +168,12 @@ module Capybara
155
168
 
156
169
  class BecomeClosed
157
170
  def initialize(options)
158
- @wait_time = Capybara::Queries::BaseQuery.wait(options)
171
+ @options = options
159
172
  end
160
173
 
161
174
  def matches?(window)
162
175
  @window = window
176
+ @wait_time = Capybara::Queries::BaseQuery.wait(@options, window.session.config.default_max_wait_time)
163
177
  start_time = Capybara::Helpers.monotonic_time
164
178
  while window.exists?
165
179
  return false if (Capybara::Helpers.monotonic_time - start_time) > @wait_time
@@ -267,4 +281,4 @@ module Capybara
267
281
  BecomeClosed.new(options)
268
282
  end
269
283
  end
270
- end
284
+ end
@@ -139,7 +139,7 @@ Capybara.add_selector(:link) do
139
139
  XPath.string.n.is(locator) |
140
140
  XPath.attr(:title).is(locator) |
141
141
  XPath.descendant(:img)[XPath.attr(:alt).is(locator)]
142
- matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
142
+ matchers |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
143
143
  xpath = xpath[matchers]
144
144
  end
145
145
  xpath = [:title].inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
@@ -185,14 +185,14 @@ Capybara.add_selector(:button) do
185
185
  unless locator.nil?
186
186
  locator = locator.to_s
187
187
  locator_matches = XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
188
- locator_matches |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
188
+ locator_matches |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
189
189
 
190
190
  input_btn_xpath = input_btn_xpath[locator_matches]
191
191
 
192
192
  btn_xpath = btn_xpath[locator_matches | XPath.string.n.is(locator) | XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
193
193
 
194
194
  alt_matches = XPath.attr(:alt).is(locator)
195
- alt_matches |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
195
+ alt_matches |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
196
196
  image_btn_xpath = image_btn_xpath[alt_matches]
197
197
  end
198
198
 
@@ -237,14 +237,23 @@ end
237
237
  # @filter [String] :name Matches the name attribute
238
238
  # @filter [String] :placeholder Matches the placeholder attribute
239
239
  # @filter [String] :with Matches the current value of the field
240
+ # @filter [String] :type Matches the type attribute of the field or element type for 'textarea'
240
241
  # @filter [String, Array<String>] :class Matches the class(es) provided
241
242
  # @filter [Boolean] :disabled Match disabled field?
242
243
  # @filter [Boolean] :multiple Match fields that accept multiple values
243
244
  #
244
245
  Capybara.add_selector(:fillable_field) do
245
246
  label "field"
246
- xpath(:name, :placeholder) do |locator, options|
247
+ xpath(:name, :placeholder, :type) do |locator, options|
247
248
  xpath = XPath.descendant(:input, :textarea)[~XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
249
+ if options[:type]
250
+ type=options[:type].to_s
251
+ if ['textarea'].include?(type)
252
+ xpath = XPath.descendant(type.to_sym)
253
+ else
254
+ xpath = xpath[XPath.attr(:type).equals(type)]
255
+ end
256
+ end
248
257
  locate_field(xpath, locator, options)
249
258
  end
250
259
 
@@ -201,9 +201,9 @@ module Capybara
201
201
  @default_visibility = default_visibility
202
202
  end
203
203
 
204
- def default_visibility
204
+ def default_visibility(fallback = Capybara.ignore_hidden_elements)
205
205
  if @default_visibility.nil?
206
- Capybara.ignore_hidden_elements
206
+ fallback
207
207
  else
208
208
  @default_visibility
209
209
  end
@@ -219,7 +219,7 @@ module Capybara
219
219
  XPath.attr(:name).equals(locator) |
220
220
  XPath.attr(:placeholder).equals(locator) |
221
221
  XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))
222
- attr_matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
222
+ attr_matchers |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
223
223
 
224
224
  locate_xpath = locate_xpath[attr_matchers]
225
225
  locate_xpath += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
@@ -35,6 +35,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
35
35
  def initialize(app, options={})
36
36
  begin
37
37
  require 'selenium-webdriver'
38
+ # Fix for selenium-webdriver 3.4.0 which misnamed these
39
+ if !defined?(::Selenium::WebDriver::Error::ElementNotInteractableError)
40
+ ::Selenium::WebDriver::Error.const_set('ElementNotInteractableError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
41
+ end
42
+ if !defined?(::Selenium::WebDriver::Error::ElementClickInterceptedError)
43
+ ::Selenium::WebDriver::Error.const_set('ElementClickInterceptedError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
44
+ end
38
45
  rescue LoadError => e
39
46
  if e.message =~ /selenium-webdriver/
40
47
  raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
@@ -185,7 +192,12 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
185
192
 
186
193
  def resize_window_to(handle, width, height)
187
194
  within_given_window(handle) do
188
- browser.manage.window.resize_to(width, height)
195
+ # Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
196
+ if marionette? && (window_size(handle) == [width, height])
197
+ {}
198
+ else
199
+ browser.manage.window.resize_to(width, height)
200
+ end
189
201
  end
190
202
  end
191
203
 
@@ -250,10 +262,12 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
250
262
  end
251
263
 
252
264
  def invalid_element_errors
253
- [Selenium::WebDriver::Error::StaleElementReferenceError,
254
- Selenium::WebDriver::Error::UnhandledError,
255
- Selenium::WebDriver::Error::ElementNotVisibleError,
256
- Selenium::WebDriver::Error::InvalidSelectorError] # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
265
+ [::Selenium::WebDriver::Error::StaleElementReferenceError,
266
+ ::Selenium::WebDriver::Error::UnhandledError,
267
+ ::Selenium::WebDriver::Error::ElementNotVisibleError,
268
+ ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
269
+ ::Selenium::WebDriver::Error::ElementNotInteractableError,
270
+ ::Selenium::WebDriver::Error::ElementClickInterceptedError]
257
271
  end
258
272
 
259
273
  def no_such_window_error
@@ -306,7 +320,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
306
320
  # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
307
321
  # Actual wait time may be longer than specified
308
322
  wait = Selenium::WebDriver::Wait.new(
309
- timeout: (options[:wait] || Capybara.default_max_wait_time),
323
+ timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0 ,
310
324
  ignore: Selenium::WebDriver::Error::NoAlertPresentError)
311
325
  begin
312
326
  wait.until do
@@ -77,16 +77,20 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
77
77
  elsif native.attribute('isContentEditable')
78
78
  #ensure we are focused on the element
79
79
  native.click
80
+
80
81
  script = <<-JS
81
82
  var range = document.createRange();
83
+ var sel = window.getSelection();
82
84
  arguments[0].focus();
83
85
  range.selectNodeContents(arguments[0]);
84
- window.getSelection().addRange(range);
86
+ sel.removeAllRanges();
87
+ sel.addRange(range);
85
88
  JS
86
89
  driver.execute_script script, self
90
+
87
91
  if (driver.options[:browser].to_s == "chrome") ||
88
92
  (driver.options[:browser].to_s == "firefox" && !driver.marionette?)
89
- # chromedriver raises a can't focus element if we use native.send_keys
93
+ # chromedriver raises a can't focus element for child elements if we use native.send_keys
90
94
  # we've already focused it so just use action api
91
95
  driver.browser.action.send_keys(value.to_s).perform
92
96
  else
@@ -25,9 +25,10 @@ module Capybara
25
25
 
26
26
  attr_accessor :error
27
27
 
28
- def initialize(app)
28
+ def initialize(app, server_errors)
29
29
  @app = app
30
30
  @counter = Counter.new
31
+ @server_errors = server_errors
31
32
  end
32
33
 
33
34
  def pending_requests?
@@ -41,7 +42,7 @@ module Capybara
41
42
  @counter.increment
42
43
  begin
43
44
  @app.call(env)
44
- rescue *Capybara.server_errors => e
45
+ rescue *@server_errors => e
45
46
  @error = e unless @error
46
47
  raise e
47
48
  ensure
@@ -59,10 +60,10 @@ module Capybara
59
60
 
60
61
  attr_reader :app, :port, :host
61
62
 
62
- def initialize(app, port=Capybara.server_port, host=Capybara.server_host)
63
+ def initialize(app, port=Capybara.server_port, host=Capybara.server_host, server_errors=Capybara.server_errors)
63
64
  @app = app
64
65
  @server_thread = nil # suppress warnings
65
- @host, @port = host, port
66
+ @host, @port, @server_errors = host, port, server_errors
66
67
  @port ||= Capybara::Server.ports[port_key]
67
68
  @port ||= find_available_port(host)
68
69
  end
@@ -112,7 +113,7 @@ module Capybara
112
113
  private
113
114
 
114
115
  def middleware
115
- @middleware ||= Middleware.new(app)
116
+ @middleware ||= Middleware.new(app, @server_errors)
116
117
  end
117
118
 
118
119
  def port_key
@@ -17,6 +17,14 @@ module Capybara
17
17
  # session = Capybara::Session.new(:culerity)
18
18
  # session.visit('http://www.google.com')
19
19
  #
20
+ # When Capybara.threadsafe == true the sessions options will be initially set to the
21
+ # current values of the global options and a configuration block can be passed to the session initializer.
22
+ # For available options see {Capybara::SessionConfig::OPTIONS}
23
+ #
24
+ # session = Capybara::Session.new(:driver, MyRackApp) do |config|
25
+ # config.app_host = "http://my_host.dev"
26
+ # end
27
+ #
20
28
  # Session provides a number of methods for controlling the navigation of the page, such as +visit+,
21
29
  # +current_path, and so on. It also delegate a number of methods to a Capybara::Document, representing
22
30
  # the current HTML document. This allows interaction:
@@ -69,10 +77,15 @@ module Capybara
69
77
 
70
78
  def initialize(mode, app=nil)
71
79
  raise TypeError, "The second parameter to Session::new should be a rack app if passed." if app && !app.respond_to?(:call)
80
+ @@instance_created = true
72
81
  @mode = mode
73
82
  @app = app
74
- if Capybara.run_server and @app and driver.needs_server?
75
- @server = Capybara::Server.new(@app).boot
83
+ if block_given?
84
+ raise "A configuration block is only accepted when Capybara.threadsafe == true" unless Capybara.threadsafe
85
+ yield config if block_given?
86
+ end
87
+ if config.run_server and @app and driver.needs_server?
88
+ @server = Capybara::Server.new(@app, config.server_port, config.server_host, config.server_errors).boot
76
89
  else
77
90
  @server = nil
78
91
  end
@@ -85,7 +98,9 @@ module Capybara
85
98
  other_drivers = Capybara.drivers.keys.map { |key| key.inspect }
86
99
  raise Capybara::DriverNotFoundError, "no driver called #{mode.inspect} was found, available drivers: #{other_drivers.join(', ')}"
87
100
  end
88
- Capybara.drivers[mode].call(app)
101
+ driver = Capybara.drivers[mode].call(app)
102
+ driver.session = self if driver.respond_to?(:session=)
103
+ driver
89
104
  end
90
105
  end
91
106
 
@@ -126,7 +141,7 @@ module Capybara
126
141
  if @server and @server.error
127
142
  # Force an explanation for the error being raised as the exception cause
128
143
  begin
129
- if Capybara.raise_server_errors
144
+ if config.raise_server_errors
130
145
  raise CapybaraError, "Your application server raised an error - It has been raised in your test code because Capybara.raise_server_errors == true"
131
146
  end
132
147
  rescue CapybaraError
@@ -235,10 +250,10 @@ module Capybara
235
250
  visit_uri = URI.parse(visit_uri.to_s)
236
251
 
237
252
  uri_base = if @server
238
- visit_uri.port = @server.port if Capybara.always_include_port && (visit_uri.port == visit_uri.default_port)
239
- URI.parse(Capybara.app_host || "http://#{@server.host}:#{@server.port}")
253
+ visit_uri.port = @server.port if config.always_include_port && (visit_uri.port == visit_uri.default_port)
254
+ URI.parse(config.app_host || "http://#{@server.host}:#{@server.port}")
240
255
  else
241
- Capybara.app_host && URI.parse(Capybara.app_host)
256
+ config.app_host && URI.parse(config.app_host)
242
257
  end
243
258
 
244
259
  # TODO - this is only for compatability with previous 2.x behavior that concatenated
@@ -481,7 +496,7 @@ module Capybara
481
496
  driver.switch_to_window(window.handle)
482
497
  window
483
498
  else
484
- wait_time = Capybara::Queries::BaseQuery.wait(options)
499
+ wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
485
500
  document.synchronize(wait_time, errors: [Capybara::WindowError]) do
486
501
  original_window_handle = driver.current_window_handle
487
502
  begin
@@ -578,7 +593,7 @@ module Capybara
578
593
  old_handles = driver.window_handles
579
594
  block.call
580
595
 
581
- wait_time = Capybara::Queries::BaseQuery.wait(options)
596
+ wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
582
597
  document.synchronize(wait_time, errors: [Capybara::WindowError]) do
583
598
  opened_handles = (driver.window_handles - old_handles)
584
599
  if opened_handles.size != 1
@@ -701,7 +716,7 @@ module Capybara
701
716
  #
702
717
  def save_page(path = nil)
703
718
  path = prepare_path(path, 'html')
704
- File.write(path, Capybara::Helpers.inject_asset_host(body), mode: 'wb')
719
+ File.write(path, Capybara::Helpers.inject_asset_host(body, config.asset_host), mode: 'wb')
705
720
  path
706
721
  end
707
722
 
@@ -786,7 +801,49 @@ module Capybara
786
801
  scope
787
802
  end
788
803
 
804
+ ##
805
+ #
806
+ # Yield a block using a specific wait time
807
+ #
808
+ def using_wait_time(seconds)
809
+ if Capybara.threadsafe
810
+ begin
811
+ previous_wait_time = config.default_max_wait_time
812
+ config.default_max_wait_time = seconds
813
+ yield
814
+ ensure
815
+ config.default_max_wait_time = previous_wait_time
816
+ end
817
+ else
818
+ Capybara.using_wait_time(seconds) { yield }
819
+ end
820
+ end
821
+
822
+ ##
823
+ #
824
+ # Accepts a block to set the configuration options if Capybara.threadsafe == true. Note that some options only have an effect
825
+ # if set at initialization time, so look at the configuration block that can be passed to the initializer too
826
+ #
827
+ def configure
828
+ raise "Session configuration is only supported when Capybara.threadsafe == true" unless Capybara.threadsafe
829
+ yield config
830
+ end
831
+
832
+ def self.instance_created?
833
+ @@instance_created
834
+ end
835
+
836
+ def config
837
+ @config ||= if Capybara.threadsafe
838
+ Capybara.session_options.dup
839
+ else
840
+ Capybara::ReadOnlySessionConfig.new(Capybara.session_options)
841
+ end
842
+ end
789
843
  private
844
+
845
+ @@instance_created = false
846
+
790
847
  def accept_modal(type, text_or_options, options, &blk)
791
848
  driver.accept_modal(type, modal_options(text_or_options, options), &blk)
792
849
  end
@@ -798,7 +855,7 @@ module Capybara
798
855
  def modal_options(text_or_options, options)
799
856
  text_or_options, options = nil, text_or_options if text_or_options.is_a?(Hash)
800
857
  options[:text] ||= text_or_options unless text_or_options.nil?
801
- options[:wait] ||= Capybara.default_max_wait_time
858
+ options[:wait] ||= config.default_max_wait_time
802
859
  options
803
860
  end
804
861
 
@@ -814,10 +871,10 @@ module Capybara
814
871
  end
815
872
 
816
873
  def prepare_path(path, extension)
817
- if Capybara.save_path || Capybara.save_and_open_page_path.nil?
818
- path = File.expand_path(path || default_fn(extension), Capybara.save_path)
874
+ if config.save_path || config.save_and_open_page_path.nil?
875
+ path = File.expand_path(path || default_fn(extension), config.save_path)
819
876
  else
820
- path = File.expand_path(default_fn(extension), Capybara.save_and_open_page_path) if path.nil?
877
+ path = File.expand_path(default_fn(extension), config.save_and_open_page_path) if path.nil?
821
878
  end
822
879
  FileUtils.mkdir_p(File.dirname(path))
823
880
  path