capybara 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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