capybara 2.13.0 → 2.18.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 (122) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +218 -18
  3. data/README.md +54 -23
  4. data/lib/capybara/config.rb +132 -0
  5. data/lib/capybara/cucumber.rb +1 -0
  6. data/lib/capybara/driver/base.rb +14 -0
  7. data/lib/capybara/dsl.rb +1 -3
  8. data/lib/capybara/helpers.rb +3 -3
  9. data/lib/capybara/minitest/spec.rb +14 -37
  10. data/lib/capybara/minitest.rb +95 -114
  11. data/lib/capybara/node/actions.rb +10 -10
  12. data/lib/capybara/node/base.rb +7 -2
  13. data/lib/capybara/node/element.rb +9 -3
  14. data/lib/capybara/node/finders.rb +92 -18
  15. data/lib/capybara/node/matchers.rb +21 -9
  16. data/lib/capybara/node/simple.rb +5 -0
  17. data/lib/capybara/queries/ancestor_query.rb +25 -0
  18. data/lib/capybara/queries/base_query.rb +12 -3
  19. data/lib/capybara/queries/current_path_query.rb +13 -9
  20. data/lib/capybara/queries/selector_query.rb +62 -23
  21. data/lib/capybara/queries/sibling_query.rb +25 -0
  22. data/lib/capybara/queries/text_query.rb +10 -5
  23. data/lib/capybara/queries/title_query.rb +1 -0
  24. data/lib/capybara/rack_test/browser.rb +13 -5
  25. data/lib/capybara/rack_test/driver.rb +6 -1
  26. data/lib/capybara/rack_test/form.rb +4 -3
  27. data/lib/capybara/rack_test/node.rb +1 -1
  28. data/lib/capybara/rspec/compound.rb +95 -0
  29. data/lib/capybara/rspec/matcher_proxies.rb +45 -0
  30. data/lib/capybara/rspec/matchers.rb +108 -7
  31. data/lib/capybara/rspec.rb +3 -1
  32. data/lib/capybara/selector/filter.rb +13 -41
  33. data/lib/capybara/selector/filter_set.rb +30 -4
  34. data/lib/capybara/selector/filters/base.rb +33 -0
  35. data/lib/capybara/selector/filters/expression_filter.rb +40 -0
  36. data/lib/capybara/selector/filters/node_filter.rb +27 -0
  37. data/lib/capybara/selector/selector.rb +36 -15
  38. data/lib/capybara/selector.rb +63 -42
  39. data/lib/capybara/selenium/driver.rb +177 -33
  40. data/lib/capybara/selenium/node.rb +106 -55
  41. data/lib/capybara/server.rb +6 -5
  42. data/lib/capybara/session/config.rb +114 -0
  43. data/lib/capybara/session/matchers.rb +15 -4
  44. data/lib/capybara/session.rb +178 -65
  45. data/lib/capybara/spec/fixtures/no_extension +1 -0
  46. data/lib/capybara/spec/public/test.js +18 -3
  47. data/lib/capybara/spec/session/accept_alert_spec.rb +9 -1
  48. data/lib/capybara/spec/session/accept_prompt_spec.rb +29 -1
  49. data/lib/capybara/spec/session/all_spec.rb +13 -1
  50. data/lib/capybara/spec/session/ancestor_spec.rb +85 -0
  51. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +24 -8
  52. data/lib/capybara/spec/session/assert_selector.rb +1 -1
  53. data/lib/capybara/spec/session/assert_text.rb +8 -0
  54. data/lib/capybara/spec/session/assert_title.rb +22 -9
  55. data/lib/capybara/spec/session/attach_file_spec.rb +8 -1
  56. data/lib/capybara/spec/session/check_spec.rb +4 -4
  57. data/lib/capybara/spec/session/choose_spec.rb +2 -2
  58. data/lib/capybara/spec/session/click_button_spec.rb +1 -1
  59. data/lib/capybara/spec/session/click_link_or_button_spec.rb +3 -3
  60. data/lib/capybara/spec/session/click_link_spec.rb +1 -1
  61. data/lib/capybara/spec/session/current_url_spec.rb +3 -3
  62. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +3 -3
  63. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +1 -1
  64. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +22 -0
  65. data/lib/capybara/spec/session/evaluate_script_spec.rb +1 -1
  66. data/lib/capybara/spec/session/fill_in_spec.rb +8 -2
  67. data/lib/capybara/spec/session/find_field_spec.rb +1 -0
  68. data/lib/capybara/spec/session/find_spec.rb +8 -6
  69. data/lib/capybara/spec/session/first_spec.rb +10 -5
  70. data/lib/capybara/spec/session/has_all_selectors_spec.rb +69 -0
  71. data/lib/capybara/spec/session/has_css_spec.rb +11 -0
  72. data/lib/capybara/spec/session/has_current_path_spec.rb +52 -7
  73. data/lib/capybara/spec/session/has_link_spec.rb +4 -4
  74. data/lib/capybara/spec/session/has_none_selectors_spec.rb +76 -0
  75. data/lib/capybara/spec/session/has_select_spec.rb +64 -6
  76. data/lib/capybara/spec/session/has_selector_spec.rb +1 -3
  77. data/lib/capybara/spec/session/has_text_spec.rb +5 -3
  78. data/lib/capybara/spec/session/has_title_spec.rb +4 -2
  79. data/lib/capybara/spec/session/has_xpath_spec.rb +5 -3
  80. data/lib/capybara/spec/session/node_spec.rb +50 -26
  81. data/lib/capybara/spec/session/refresh_spec.rb +28 -0
  82. data/lib/capybara/spec/session/reset_session_spec.rb +3 -3
  83. data/lib/capybara/spec/session/select_spec.rb +3 -2
  84. data/lib/capybara/spec/session/sibling_spec.rb +52 -0
  85. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  86. data/lib/capybara/spec/session/unselect_spec.rb +2 -2
  87. data/lib/capybara/spec/session/visit_spec.rb +56 -1
  88. data/lib/capybara/spec/session/window/become_closed_spec.rb +11 -11
  89. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +11 -9
  90. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +4 -4
  91. data/lib/capybara/spec/session/window/within_window_spec.rb +27 -2
  92. data/lib/capybara/spec/spec_helper.rb +28 -4
  93. data/lib/capybara/spec/test_app.rb +3 -1
  94. data/lib/capybara/spec/views/form.erb +27 -1
  95. data/lib/capybara/spec/views/initial_alert.erb +10 -0
  96. data/lib/capybara/spec/views/with_fixed_header_footer.erb +17 -0
  97. data/lib/capybara/spec/views/with_hover.erb +5 -0
  98. data/lib/capybara/spec/views/with_html.erb +33 -2
  99. data/lib/capybara/spec/views/with_js.erb +12 -0
  100. data/lib/capybara/spec/views/with_windows.erb +4 -0
  101. data/lib/capybara/version.rb +1 -1
  102. data/lib/capybara/window.rb +1 -1
  103. data/lib/capybara.rb +102 -124
  104. data/spec/capybara_spec.rb +43 -21
  105. data/spec/dsl_spec.rb +1 -0
  106. data/spec/filter_set_spec.rb +28 -0
  107. data/spec/minitest_spec.rb +9 -1
  108. data/spec/minitest_spec_spec.rb +19 -5
  109. data/spec/per_session_config_spec.rb +67 -0
  110. data/spec/result_spec.rb +20 -0
  111. data/spec/rspec/shared_spec_matchers.rb +148 -44
  112. data/spec/rspec/views_spec.rb +4 -0
  113. data/spec/rspec_matchers_spec.rb +46 -0
  114. data/spec/rspec_spec.rb +77 -0
  115. data/spec/selector_spec.rb +2 -1
  116. data/spec/selenium_spec_chrome.rb +25 -17
  117. data/spec/selenium_spec_firefox.rb +2 -1
  118. data/spec/selenium_spec_marionette.rb +18 -5
  119. data/spec/session_spec.rb +44 -0
  120. data/spec/shared_selenium_session.rb +72 -8
  121. data/spec/spec_helper.rb +4 -0
  122. metadata +55 -8
@@ -14,13 +14,16 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
14
14
 
15
15
  def browser
16
16
  unless @browser
17
- if options[:browser].to_s == "firefox"
18
- options[:desired_capabilities] ||= Selenium::WebDriver::Remote::Capabilities.firefox
17
+ if firefox?
18
+ options[:desired_capabilities] ||= {}
19
19
  options[:desired_capabilities].merge!({ unexpectedAlertBehaviour: "ignore" })
20
20
  end
21
21
 
22
- @browser = Selenium::WebDriver.for(options[:browser], options.reject { |key,_val| SPECIAL_OPTIONS.include?(key) })
22
+ @processed_options = options.reject { |key,_val| SPECIAL_OPTIONS.include?(key) }
23
+ @browser = Selenium::WebDriver.for(options[:browser], @processed_options)
23
24
 
25
+ @w3c = ((defined?(Selenium::WebDriver::Remote::W3CCapabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3CCapabilities)) ||
26
+ (defined?(Selenium::WebDriver::Remote::W3C::Capabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3C::Capabilities)))
24
27
  main = Process.pid
25
28
  at_exit do
26
29
  # Store the exit status of the test run since it goes away after calling the at_exit proc...
@@ -33,17 +36,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
33
36
  end
34
37
 
35
38
  def initialize(app, options={})
36
- begin
37
- require 'selenium-webdriver'
38
- rescue LoadError => e
39
- if e.message =~ /selenium-webdriver/
40
- 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."
41
- else
42
- raise e
43
- end
44
- end
45
-
46
-
39
+ load_selenium
40
+ @session = nil
47
41
  @app = app
48
42
  @browser = nil
49
43
  @exit_status = nil
@@ -55,6 +49,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
55
49
  browser.navigate.to(path)
56
50
  end
57
51
 
52
+ def refresh
53
+ accept_modal(nil, wait: 0.1) do
54
+ browser.navigate.refresh
55
+ end
56
+ rescue Capybara::ModalNotFound
57
+ end
58
+
58
59
  def go_back
59
60
  browser.navigate.back
60
61
  end
@@ -95,6 +96,12 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
95
96
  unwrap_script_result(result)
96
97
  end
97
98
 
99
+ def evaluate_async_script(script, *args)
100
+ browser.manage.timeouts.script_timeout = Capybara.default_max_wait_time
101
+ result = browser.execute_async_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg} )
102
+ unwrap_script_result(result)
103
+ end
104
+
98
105
  def save_screenshot(path, _options={})
99
106
  browser.save_screenshot(path)
100
107
  end
@@ -138,14 +145,14 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
138
145
  raise Capybara::ExpectationNotMet.new('Timed out waiting for Selenium session reset') if (Capybara::Helpers.monotonic_time - start_time) >= 10
139
146
  sleep 0.05
140
147
  end
141
- rescue Selenium::WebDriver::Error::UnhandledAlertError
148
+ rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
142
149
  # This error is thrown if an unhandled alert is on the page
143
150
  # Firefox appears to automatically dismiss this alert, chrome does not
144
151
  # We'll try to accept it
145
152
  begin
146
153
  @browser.switch_to.alert.accept
147
154
  sleep 0.25 # allow time for the modal to be handled
148
- rescue Selenium::WebDriver::Error::NoAlertPresentError
155
+ rescue modal_error
149
156
  # The alert is now gone - nothing to do
150
157
  end
151
158
  # try cleaning up the browser again
@@ -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
 
@@ -222,7 +234,9 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
222
234
  def accept_modal(_type, options={})
223
235
  yield if block_given?
224
236
  modal = find_modal(options)
237
+
225
238
  modal.send_keys options[:with] if options[:with]
239
+
226
240
  message = modal.text
227
241
  modal.accept
228
242
  message
@@ -238,7 +252,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
238
252
 
239
253
  def quit
240
254
  @browser.quit if @browser
241
- rescue Errno::ECONNREFUSED
255
+ rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
242
256
  # Browser must have already gone
243
257
  rescue Selenium::WebDriver::Error::UnknownError => e
244
258
  unless silenced_unknown_error_message?(e.message) # Most likely already gone
@@ -250,10 +264,15 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
250
264
  end
251
265
 
252
266
  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
267
+ [::Selenium::WebDriver::Error::StaleElementReferenceError,
268
+ ::Selenium::WebDriver::Error::UnhandledError,
269
+ ::Selenium::WebDriver::Error::ElementNotVisibleError,
270
+ ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
271
+ ::Selenium::WebDriver::Error::ElementNotInteractableError,
272
+ ::Selenium::WebDriver::Error::ElementClickInterceptedError,
273
+ ::Selenium::WebDriver::Error::InvalidElementStateError,
274
+ ::Selenium::WebDriver::Error::ElementNotSelectableError,
275
+ ]
257
276
  end
258
277
 
259
278
  def no_such_window_error
@@ -261,6 +280,40 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
261
280
  end
262
281
 
263
282
  # @api private
283
+ def marionette?
284
+ firefox? && browser && @w3c
285
+ end
286
+
287
+ # @api private
288
+ def firefox?
289
+ browser_name == "firefox"
290
+ end
291
+
292
+ # @api private
293
+ def chrome?
294
+ browser_name == "chrome"
295
+ end
296
+
297
+ # @deprecated This method is being removed
298
+ def browser_initialized?
299
+ super && !@browser.nil?
300
+ end
301
+
302
+ private
303
+
304
+ # @api private
305
+ def browser_name
306
+ options[:browser].to_s
307
+ end
308
+
309
+ def modal_error
310
+ if defined?(Selenium::WebDriver::Error::NoSuchAlertError)
311
+ Selenium::WebDriver::Error::NoSuchAlertError
312
+ else
313
+ Selenium::WebDriver::Error::NoAlertPresentError
314
+ end
315
+ end
316
+
264
317
  def find_window(locator)
265
318
  handles = browser.window_handles
266
319
  return locator if handles.include? locator
@@ -278,18 +331,60 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
278
331
  raise Capybara::ElementNotFound, "Could not find a window identified by #{locator}"
279
332
  end
280
333
 
281
- #@api private
282
- def marionette?
283
- (options[:browser].to_s == "firefox") && browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3CCapabilities)
284
- end
334
+ def insert_modal_handlers(accept, response_text)
335
+ prompt_response = if accept
336
+ if response_text.nil?
337
+ "default_text"
338
+ else
339
+ "'#{response_text.gsub("\\", "\\\\\\").gsub("'", "\\\\'")}'"
340
+ end
341
+ else
342
+ 'null'
343
+ end
285
344
 
286
- # @deprecated This method is being removed
287
- def browser_initialized?
288
- super && !@browser.nil?
345
+ script = <<-JS
346
+ if (typeof window.capybara === 'undefined') {
347
+ window.capybara = {
348
+ modal_handlers: [],
349
+ current_modal_status: function() {
350
+ return [this.modal_handlers[0].called, this.modal_handlers[0].modal_text];
351
+ },
352
+ add_handler: function(handler) {
353
+ this.modal_handlers.unshift(handler);
354
+ },
355
+ remove_handler: function(handler) {
356
+ window.alert = handler.alert;
357
+ window.confirm = handler.confirm;
358
+ window.prompt = handler.prompt;
359
+ },
360
+ handler_called: function(handler, str) {
361
+ handler.called = true;
362
+ handler.modal_text = str;
363
+ this.remove_handler(handler);
364
+ }
365
+ };
366
+ };
367
+
368
+ var modal_handler = {
369
+ prompt: window.prompt,
370
+ confirm: window.confirm,
371
+ alert: window.alert,
372
+ called: false
373
+ }
374
+ window.capybara.add_handler(modal_handler);
375
+
376
+ window.alert = window.confirm = function(str = "") {
377
+ window.capybara.handler_called(modal_handler, str.toString());
378
+ return #{accept ? 'true' : 'false'};
379
+ }
380
+ window.prompt = function(str = "", default_text = "") {
381
+ window.capybara.handler_called(modal_handler, str.toString());
382
+ return #{prompt_response};
383
+ }
384
+ JS
385
+ execute_script script
289
386
  end
290
387
 
291
- private
292
-
293
388
  def within_given_window(handle)
294
389
  original_handle = self.current_window_handle
295
390
  if handle == original_handle
@@ -306,8 +401,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
306
401
  # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
307
402
  # Actual wait time may be longer than specified
308
403
  wait = Selenium::WebDriver::Wait.new(
309
- timeout: (options[:wait] || Capybara.default_max_wait_time),
310
- ignore: Selenium::WebDriver::Error::NoAlertPresentError)
404
+ timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0 ,
405
+ ignore: modal_error)
311
406
  begin
312
407
  wait.until do
313
408
  alert = @browser.switch_to.alert
@@ -319,6 +414,36 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
319
414
  end
320
415
  end
321
416
 
417
+ def find_headless_modal(options={})
418
+ # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
419
+ # Actual wait time may be longer than specified
420
+ wait = Selenium::WebDriver::Wait.new(
421
+ timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0 ,
422
+ ignore: modal_error)
423
+ begin
424
+ wait.until do
425
+ called, alert_text = evaluate_script('window.capybara && window.capybara.current_modal_status()')
426
+ if called
427
+ execute_script('window.capybara && window.capybara.modal_handlers.shift()')
428
+ regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
429
+ if alert_text.match(regexp)
430
+ alert_text
431
+ else
432
+ raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
433
+ end
434
+ elsif called.nil?
435
+ # page changed so modal_handler data has gone away
436
+ warn "Can't verify modal text when page change occurs - ignoring" if options[:text]
437
+ ""
438
+ else
439
+ nil
440
+ end
441
+ end
442
+ rescue Selenium::WebDriver::Error::TimeOutError
443
+ raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
444
+ end
445
+ end
446
+
322
447
  def silenced_unknown_error_message?(msg)
323
448
  silenced_unknown_error_messages.any? { |r| msg =~ r }
324
449
  end
@@ -339,4 +464,23 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
339
464
  arg
340
465
  end
341
466
  end
467
+
468
+ def load_selenium
469
+ begin
470
+ require 'selenium-webdriver'
471
+ # Fix for selenium-webdriver 3.4.0 which misnamed these
472
+ if !defined?(::Selenium::WebDriver::Error::ElementNotInteractableError)
473
+ ::Selenium::WebDriver::Error.const_set('ElementNotInteractableError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
474
+ end
475
+ if !defined?(::Selenium::WebDriver::Error::ElementClickInterceptedError)
476
+ ::Selenium::WebDriver::Error.const_set('ElementClickInterceptedError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
477
+ end
478
+ rescue LoadError => e
479
+ if e.message =~ /selenium-webdriver/
480
+ 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."
481
+ else
482
+ raise e
483
+ end
484
+ end
485
+ end
342
486
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  class Capybara::Selenium::Node < Capybara::Driver::Node
3
+
3
4
  def visible_text
4
5
  # Selenium doesn't normalize Unicode whitespace.
5
6
  Capybara::Helpers.normalize_whitespace(native.text)
@@ -18,7 +19,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
18
19
 
19
20
  def value
20
21
  if tag_name == "select" and multiple?
21
- native.find_elements(:xpath, ".//option").select { |n| n.selected? }.map { |n| n[:value] || n.text }
22
+ native.find_elements(:css, "option:checked").map { |n| n[:value] || n.text }
22
23
  else
23
24
  native[:value]
24
25
  end
@@ -38,61 +39,54 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
38
39
  def set(value, options={})
39
40
  tag_name = self.tag_name
40
41
  type = self[:type]
42
+
41
43
  if (Array === value) && !multiple?
42
44
  raise ArgumentError.new "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
43
45
  end
44
- if tag_name == 'input' and type == 'radio'
45
- click
46
- elsif tag_name == 'input' and type == 'checkbox'
47
- click if value ^ native.attribute('checked').to_s.eql?("true")
48
- elsif tag_name == 'input' and type == 'file'
49
- path_names = value.to_s.empty? ? [] : value
50
- if driver.options[:browser].to_s == "chrome"
51
- native.send_keys(Array(path_names).join("\n"))
46
+
47
+ case tag_name
48
+ when 'input'
49
+ case type
50
+ when 'radio'
51
+ click
52
+ when 'checkbox'
53
+ click if value ^ native.attribute('checked').to_s.eql?("true")
54
+ when 'file'
55
+ path_names = value.to_s.empty? ? [] : value
56
+ if driver.chrome?
57
+ native.send_keys(Array(path_names).join("\n"))
58
+ else
59
+ native.send_keys(*path_names)
60
+ end
52
61
  else
53
- native.send_keys(*path_names)
62
+ set_text(value, options)
54
63
  end
55
- elsif tag_name == 'textarea' or tag_name == 'input'
56
- if readonly?
57
- warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
58
- elsif value.to_s.empty?
59
- native.clear
60
- else
61
- if options[:clear] == :backspace
62
- # Clear field by sending the correct number of backspace keys.
63
- backspaces = [:backspace] * self.value.to_s.length
64
- native.send_keys(*(backspaces + [value.to_s]))
65
- elsif options[:clear] == :none
66
- native.send_keys(value.to_s)
67
- elsif options[:clear].is_a? Array
68
- native.send_keys(*options[:clear], value.to_s)
64
+ when 'textarea'
65
+ set_text(value, options)
66
+ else
67
+ if content_editable?
68
+ #ensure we are focused on the element
69
+ click
70
+
71
+ script = <<-JS
72
+ var range = document.createRange();
73
+ var sel = window.getSelection();
74
+ arguments[0].focus();
75
+ range.selectNodeContents(arguments[0]);
76
+ sel.removeAllRanges();
77
+ sel.addRange(range);
78
+ JS
79
+ driver.execute_script script, self
80
+
81
+ if driver.chrome? || driver.firefox?
82
+ # chromedriver raises a can't focus element for child elements if we use native.send_keys
83
+ # we've already focused it so just use action api
84
+ driver.browser.action.send_keys(value.to_s).perform
69
85
  else
70
- # Clear field by JavaScript assignment of the value property.
71
- # Script can change a readonly element which user input cannot, so
72
- # don't execute if readonly.
73
- driver.execute_script "arguments[0].value = ''", self
86
+ # action api is really slow here just use native.send_keys
74
87
  native.send_keys(value.to_s)
75
88
  end
76
89
  end
77
- elsif native.attribute('isContentEditable')
78
- #ensure we are focused on the element
79
- native.click
80
- script = <<-JS
81
- var range = document.createRange();
82
- arguments[0].focus();
83
- range.selectNodeContents(arguments[0]);
84
- window.getSelection().addRange(range);
85
- JS
86
- driver.execute_script script, self
87
- if (driver.options[:browser].to_s == "chrome") ||
88
- (driver.options[:browser].to_s == "firefox" && !driver.marionette?)
89
- # chromedriver raises a can't focus element if we use native.send_keys
90
- # we've already focused it so just use action api
91
- driver.browser.action.send_keys(value.to_s).perform
92
- else
93
- # action api is really slow here just use native.send_keys
94
- native.send_keys(value.to_s)
95
- end
96
90
  end
97
91
  end
98
92
 
@@ -101,22 +95,33 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
101
95
  end
102
96
 
103
97
  def unselect_option
104
- if select_node['multiple'] != 'multiple' and select_node['multiple'] != 'true'
105
- raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
106
- end
98
+ raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box." if !select_node.multiple?
107
99
  native.click if selected?
108
100
  end
109
101
 
110
102
  def click
111
103
  native.click
104
+ rescue => e
105
+ if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
106
+ e.message =~ /Other element would receive the click/
107
+ begin
108
+ driver.execute_script("arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'})", self)
109
+ rescue
110
+ end
111
+ end
112
+ raise e
112
113
  end
113
114
 
114
115
  def right_click
115
- driver.browser.action.context_click(native).perform
116
+ scroll_if_needed do
117
+ driver.browser.action.context_click(native).perform
118
+ end
116
119
  end
117
120
 
118
121
  def double_click
119
- driver.browser.action.double_click(native).perform
122
+ scroll_if_needed do
123
+ driver.browser.action.double_click(native).perform
124
+ end
120
125
  end
121
126
 
122
127
  def send_keys(*args)
@@ -124,11 +129,15 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
124
129
  end
125
130
 
126
131
  def hover
127
- driver.browser.action.move_to(native).perform
132
+ scroll_if_needed do
133
+ driver.browser.action.move_to(native).perform
134
+ end
128
135
  end
129
136
 
130
137
  def drag_to(element)
131
- driver.browser.action.drag_and_drop(native, element.native).perform
138
+ scroll_if_needed do
139
+ driver.browser.action.drag_and_drop(native, element.native).perform
140
+ end
132
141
  end
133
142
 
134
143
  def tag_name
@@ -169,6 +178,10 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
169
178
  multiple and multiple != "false"
170
179
  end
171
180
 
181
+ def content_editable?
182
+ native.attribute('isContentEditable')
183
+ end
184
+
172
185
  def find_xpath(locator)
173
186
  native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
174
187
  end
@@ -208,6 +221,44 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
208
221
  private
209
222
  # a reference to the select node if this is an option node
210
223
  def select_node
211
- find_xpath('./ancestor::select').first
224
+ find_xpath('./ancestor::select[1]').first
225
+ end
226
+
227
+ def set_text(value, options)
228
+ if readonly?
229
+ warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
230
+ elsif value.to_s.empty? && options[:clear].nil?
231
+ native.clear
232
+ else
233
+ if options[:clear] == :backspace
234
+ # Clear field by sending the correct number of backspace keys.
235
+ backspaces = [:backspace] * self.value.to_s.length
236
+ native.send_keys(*(backspaces + [value.to_s]))
237
+ elsif options[:clear] == :none
238
+ native.send_keys(value.to_s)
239
+ elsif options[:clear].is_a? Array
240
+ native.send_keys(*options[:clear], value.to_s)
241
+ else
242
+ # Clear field by JavaScript assignment of the value property.
243
+ # Script can change a readonly element which user input cannot, so
244
+ # don't execute if readonly.
245
+ driver.execute_script "arguments[0].value = ''", self
246
+ native.send_keys(value.to_s)
247
+ end
248
+ end
249
+ end
250
+
251
+ def scroll_if_needed(&block)
252
+ block.call
253
+ rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
254
+ script = <<-JS
255
+ try {
256
+ arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
257
+ } catch(e) {
258
+ arguments[0].scrollIntoView(true);
259
+ }
260
+ JS
261
+ driver.execute_script(script, self)
262
+ block.call
212
263
  end
213
264
  end
@@ -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