capybara 2.13.0 → 2.18.0

Sign up to get free protection for your applications and to get access to all the features.
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