capybara 3.29.0 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +91 -1
  3. data/README.md +10 -3
  4. data/lib/capybara.rb +17 -7
  5. data/lib/capybara/config.rb +7 -3
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/dsl.rb +10 -2
  8. data/lib/capybara/helpers.rb +3 -1
  9. data/lib/capybara/minitest.rb +232 -144
  10. data/lib/capybara/minitest/spec.rb +153 -97
  11. data/lib/capybara/node/actions.rb +35 -35
  12. data/lib/capybara/node/document.rb +2 -2
  13. data/lib/capybara/node/document_matchers.rb +3 -3
  14. data/lib/capybara/node/element.rb +23 -16
  15. data/lib/capybara/node/finders.rb +17 -11
  16. data/lib/capybara/node/matchers.rb +64 -51
  17. data/lib/capybara/node/simple.rb +4 -2
  18. data/lib/capybara/queries/ancestor_query.rb +1 -1
  19. data/lib/capybara/queries/base_query.rb +2 -1
  20. data/lib/capybara/queries/selector_query.rb +25 -5
  21. data/lib/capybara/queries/sibling_query.rb +1 -1
  22. data/lib/capybara/queries/style_query.rb +1 -1
  23. data/lib/capybara/queries/text_query.rb +6 -0
  24. data/lib/capybara/rack_test/browser.rb +7 -2
  25. data/lib/capybara/rack_test/driver.rb +1 -1
  26. data/lib/capybara/rack_test/form.rb +1 -1
  27. data/lib/capybara/rack_test/node.rb +34 -9
  28. data/lib/capybara/registration_container.rb +44 -0
  29. data/lib/capybara/registrations/servers.rb +1 -1
  30. data/lib/capybara/result.rb +29 -5
  31. data/lib/capybara/rspec/matcher_proxies.rb +4 -4
  32. data/lib/capybara/rspec/matchers.rb +27 -27
  33. data/lib/capybara/rspec/matchers/base.rb +12 -6
  34. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  35. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  36. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  37. data/lib/capybara/rspec/matchers/have_selector.rb +15 -7
  38. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  39. data/lib/capybara/rspec/matchers/have_text.rb +3 -3
  40. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  41. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  42. data/lib/capybara/rspec/matchers/match_style.rb +2 -2
  43. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  44. data/lib/capybara/selector.rb +34 -17
  45. data/lib/capybara/selector/css.rb +1 -1
  46. data/lib/capybara/selector/definition.rb +7 -6
  47. data/lib/capybara/selector/definition/button.rb +8 -2
  48. data/lib/capybara/selector/definition/checkbox.rb +2 -2
  49. data/lib/capybara/selector/definition/css.rb +3 -1
  50. data/lib/capybara/selector/definition/datalist_input.rb +1 -1
  51. data/lib/capybara/selector/definition/datalist_option.rb +1 -1
  52. data/lib/capybara/selector/definition/element.rb +1 -1
  53. data/lib/capybara/selector/definition/field.rb +1 -1
  54. data/lib/capybara/selector/definition/file_field.rb +1 -1
  55. data/lib/capybara/selector/definition/fillable_field.rb +2 -2
  56. data/lib/capybara/selector/definition/label.rb +4 -2
  57. data/lib/capybara/selector/definition/link.rb +8 -0
  58. data/lib/capybara/selector/definition/radio_button.rb +2 -2
  59. data/lib/capybara/selector/definition/select.rb +32 -13
  60. data/lib/capybara/selector/definition/table.rb +6 -3
  61. data/lib/capybara/selector/filter_set.rb +11 -9
  62. data/lib/capybara/selector/filters/base.rb +6 -1
  63. data/lib/capybara/selector/filters/locator_filter.rb +1 -1
  64. data/lib/capybara/selector/selector.rb +8 -2
  65. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  66. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  67. data/lib/capybara/selenium/driver.rb +22 -11
  68. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +8 -10
  69. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +7 -9
  70. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -2
  71. data/lib/capybara/selenium/extensions/html5_drag.rb +30 -13
  72. data/lib/capybara/selenium/node.rb +97 -18
  73. data/lib/capybara/selenium/nodes/chrome_node.rb +11 -14
  74. data/lib/capybara/selenium/nodes/edge_node.rb +4 -2
  75. data/lib/capybara/selenium/nodes/firefox_node.rb +4 -4
  76. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  77. data/lib/capybara/selenium/patches/logs.rb +3 -5
  78. data/lib/capybara/server.rb +15 -3
  79. data/lib/capybara/server/checker.rb +1 -1
  80. data/lib/capybara/server/middleware.rb +20 -10
  81. data/lib/capybara/session.rb +43 -26
  82. data/lib/capybara/session/config.rb +9 -3
  83. data/lib/capybara/session/matchers.rb +6 -6
  84. data/lib/capybara/spec/public/test.js +69 -6
  85. data/lib/capybara/spec/session/all_spec.rb +60 -5
  86. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  87. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  88. data/lib/capybara/spec/session/click_button_spec.rb +16 -0
  89. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  90. data/lib/capybara/spec/session/find_spec.rb +31 -8
  91. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  92. data/lib/capybara/spec/session/has_css_spec.rb +12 -9
  93. data/lib/capybara/spec/session/has_current_path_spec.rb +2 -2
  94. data/lib/capybara/spec/session/has_field_spec.rb +16 -0
  95. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  96. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  97. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  98. data/lib/capybara/spec/session/has_text_spec.rb +35 -0
  99. data/lib/capybara/spec/session/node_spec.rb +160 -29
  100. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  101. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  102. data/lib/capybara/spec/session/selectors_spec.rb +15 -2
  103. data/lib/capybara/spec/session/window/window_spec.rb +7 -7
  104. data/lib/capybara/spec/spec_helper.rb +2 -2
  105. data/lib/capybara/spec/test_app.rb +14 -18
  106. data/lib/capybara/spec/views/form.erb +18 -2
  107. data/lib/capybara/spec/views/with_dragula.erb +3 -1
  108. data/lib/capybara/spec/views/with_html.erb +2 -2
  109. data/lib/capybara/spec/views/with_js.erb +1 -0
  110. data/lib/capybara/version.rb +1 -1
  111. data/spec/capybara_spec.rb +1 -1
  112. data/spec/dsl_spec.rb +16 -3
  113. data/spec/minitest_spec.rb +1 -1
  114. data/spec/minitest_spec_spec.rb +46 -46
  115. data/spec/rack_test_spec.rb +13 -1
  116. data/spec/regexp_dissassembler_spec.rb +40 -36
  117. data/spec/result_spec.rb +43 -32
  118. data/spec/rspec/features_spec.rb +1 -0
  119. data/spec/rspec/shared_spec_matchers.rb +68 -56
  120. data/spec/rspec_spec.rb +4 -4
  121. data/spec/selector_spec.rb +1 -1
  122. data/spec/selenium_spec_chrome.rb +9 -6
  123. data/spec/selenium_spec_chrome_remote.rb +2 -0
  124. data/spec/selenium_spec_firefox.rb +7 -2
  125. data/spec/server_spec.rb +65 -31
  126. data/spec/session_spec.rb +1 -1
  127. data/spec/shared_selenium_node.rb +21 -3
  128. data/spec/shared_selenium_session.rb +33 -14
  129. data/spec/spec_helper.rb +1 -1
  130. metadata +6 -4
@@ -1 +1 @@
1
- (function(){function u(e){var t=e.tagName.toUpperCase();if("OPTION"==t)return!0;if("INPUT"!=t)return!1;var r=e.type.toLowerCase();return"checkbox"==r||"radio"==r}function s(e){var t="selected",r=e.type&&e.type.toLowerCase();return"checkbox"!=r&&"radio"!=r||(t="checked"),!!e[t]}function c(e,t){var r=e.getAttributeNode(t);return r&&r.specified?r.value:null}var i=["allowfullscreen","allowpaymentrequest","allowusermedia","async","autofocus","autoplay","checked","compact","complete","controls","declare","default","defaultchecked","defaultselected","defer","disabled","ended","formnovalidate","hidden","indeterminate","iscontenteditable","ismap","itemscope","loop","multiple","muted","nohref","nomodule","noresize","noshade","novalidate","nowrap","open","paused","playsinline","pubdate","readonly","required","reversed","scoped","seamless","seeking","selected","truespeed","typemustmatch","willvalidate"],d={"class":"className",readonly:"readOnly"};return function f(e,t){var r=null,a=t.toLowerCase();if("style"==a)return(r=e.style)&&"string"!=typeof r&&(r=r.cssText),r;if(("selected"==a||"checked"==a)&&u(e))return s(e)?"true":null;if(tagName=e.tagName.toUpperCase(),"IMG"==tagName&&"src"==a||"A"==tagName&&"href"==a)return(r=c(e,a))&&(r=e[a]),r;if("spellcheck"==a){if(null===!(r=c(e,a))){if("false"==r.toLowerCase())return"false";if("true"==r.toLowerCase())return"true"}return e[a]+""}var l,n=d[t]||t;if(i.some(function(e){e==a}))return(r=!(null===(r=c(e,a)))||e[n])?"true":null;try{l=e[n]}catch(o){}return null!=(r=null==l||"object"==typeof l||"function"==typeof l?c(e,t):l)?r.toString():null}})()
1
+ (function(){function u(e){var t=e.tagName.toUpperCase();if("OPTION"==t)return!0;if("INPUT"!=t)return!1;var r=e.type.toLowerCase();return"checkbox"==r||"radio"==r}function s(e){var t="selected",r=e.type&&e.type.toLowerCase();return"checkbox"!=r&&"radio"!=r||(t="checked"),!!e[t]}function c(e,t){var r=e.getAttributeNode(t);return r&&r.specified?r.value:null}var i=["allowfullscreen","allowpaymentrequest","allowusermedia","async","autofocus","autoplay","checked","compact","complete","controls","declare","default","defaultchecked","defaultselected","defer","disabled","ended","formnovalidate","hidden","indeterminate","iscontenteditable","ismap","itemscope","loop","multiple","muted","nohref","nomodule","noresize","noshade","novalidate","nowrap","open","paused","playsinline","pubdate","readonly","required","reversed","scoped","seamless","seeking","selected","truespeed","typemustmatch","willvalidate"],d={"class":"className",readonly:"readOnly"};return function f(e,t){var r=null,a=t.toLowerCase();if("style"==a)return(r=e.style)&&"string"!=typeof r&&(r=r.cssText),r;if(("selected"==a||"checked"==a)&&u(e))return s(e)?"true":null;if(tagName=e.tagName.toUpperCase(),"IMG"==tagName&&"src"==a||"A"==tagName&&"href"==a)return(r=c(e,a))&&(r=e[a]),r;if("spellcheck"==a){if(null!==(r=c(e,a))){if("false"==r.toLowerCase())return"false";if("true"==r.toLowerCase())return"true"}return e[a]+""}var l,n=d[t]||t;if(i.some(function(e){e==a}))return(r=!(null===(r=c(e,a)))||e[n])?"true":null;try{l=e[n]}catch(o){}return null!=(r=null==l||"object"==typeof l||"function"==typeof l?c(e,t):l)?r.toString():null}})()
@@ -117,7 +117,7 @@
117
117
 
118
118
  if ("spellcheck" == name) {
119
119
  value = getAttributeValue(element, name);
120
- if (!value === null) {
120
+ if (!(value === null)) {
121
121
  if (value.toLowerCase() == "false") {
122
122
  return "false";
123
123
  } else if (value.toLowerCase() == "true") {
@@ -20,7 +20,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
20
20
  require 'capybara/selenium/logger_suppressor'
21
21
  require 'capybara/selenium/patches/atoms'
22
22
  require 'capybara/selenium/patches/is_displayed'
23
- warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade." if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
23
+ require 'capybara/selenium/patches/action_pauser'
24
+ if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
25
+ warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade."
26
+ end
24
27
  rescue LoadError => e
25
28
  raise e unless e.message.match?(/selenium-webdriver/)
26
29
 
@@ -82,6 +85,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
82
85
 
83
86
  def html
84
87
  browser.page_source
88
+ rescue Selenium::WebDriver::Error::JavascriptError => e
89
+ raise unless e.message.match?(/documentElement is null/)
85
90
  end
86
91
 
87
92
  def title
@@ -143,7 +148,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
143
148
 
144
149
  switch_to_frame(:parent)
145
150
  begin
146
- return frame.base.obscured?(x: x, y: y)
151
+ frame.base.obscured?(x: x, y: y)
147
152
  ensure
148
153
  switch_to_frame(frame)
149
154
  end
@@ -219,7 +224,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
219
224
 
220
225
  def accept_modal(_type, **options)
221
226
  yield if block_given?
222
- modal = find_modal(options)
227
+ modal = find_modal(**options)
223
228
 
224
229
  modal.send_keys options[:with] if options[:with]
225
230
 
@@ -230,7 +235,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
230
235
 
231
236
  def dismiss_modal(_type, **options)
232
237
  yield if block_given?
233
- modal = find_modal(options)
238
+ modal = find_modal(**options)
234
239
  message = modal.text
235
240
  modal.dismiss
236
241
  message
@@ -238,7 +243,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
238
243
 
239
244
  def quit
240
245
  @browser&.quit
241
- rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED # rubocop:disable Lint/HandleExceptions
246
+ rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
242
247
  # Browser must have already gone
243
248
  rescue Selenium::WebDriver::Error::UnknownError => e
244
249
  unless silenced_unknown_error_message?(e.message) # Most likely already gone
@@ -290,7 +295,7 @@ private
290
295
  def clear_browser_state
291
296
  delete_all_cookies
292
297
  clear_storage
293
- rescue *clear_browser_state_errors # rubocop:disable Lint/HandleExceptions
298
+ rescue *clear_browser_state_errors
294
299
  # delete_all_cookies fails when we've previously gone
295
300
  # to about:blank, so we rescue this error and do nothing
296
301
  # instead.
@@ -314,7 +319,7 @@ private
314
319
  def clear_storage
315
320
  clear_session_storage unless options[:clear_session_storage] == false
316
321
  clear_local_storage unless options[:clear_local_storage] == false
317
- rescue Selenium::WebDriver::Error::JavascriptError # rubocop:disable Lint/HandleExceptions
322
+ rescue Selenium::WebDriver::Error::JavascriptError
318
323
  # session/local storage may not be available if on non-http pages (e.g. about:blank)
319
324
  end
320
325
 
@@ -325,7 +330,9 @@ private
325
330
  begin
326
331
  @browser&.execute_script('window.sessionStorage.clear()')
327
332
  rescue # rubocop:disable Style/RescueStandardError
328
- warn 'sessionStorage clear requested but is not supported by this driver' unless options[:clear_session_storage].nil?
333
+ unless options[:clear_session_storage].nil?
334
+ warn 'sessionStorage clear requested but is not supported by this driver'
335
+ end
329
336
  end
330
337
  end
331
338
  end
@@ -337,7 +344,9 @@ private
337
344
  begin
338
345
  @browser&.execute_script('window.localStorage.clear()')
339
346
  rescue # rubocop:disable Style/RescueStandardError
340
- warn 'localStorage clear requested but is not supported by this driver' unless options[:clear_local_storage].nil?
347
+ unless options[:clear_local_storage].nil?
348
+ warn 'localStorage clear requested but is not supported by this driver'
349
+ end
341
350
  end
342
351
  end
343
352
  end
@@ -346,7 +355,7 @@ private
346
355
  @browser.navigate.to(url)
347
356
  sleep 0.1 # slight wait for alert
348
357
  @browser.switch_to.alert.accept
349
- rescue modal_error # rubocop:disable Lint/HandleExceptions
358
+ rescue modal_error
350
359
  # alert now gone, should mean navigation happened
351
360
  end
352
361
 
@@ -378,7 +387,9 @@ private
378
387
  alert = @browser.switch_to.alert
379
388
  regexp = text.is_a?(Regexp) ? text : Regexp.new(Regexp.escape(text.to_s))
380
389
  matched = alert.text.match?(regexp)
381
- raise Capybara::ModalNotFound, "Unable to find modal dialog with #{text} - found '#{alert.text}' instead." unless matched
390
+ unless matched
391
+ raise Capybara::ModalNotFound, "Unable to find modal dialog with #{text} - found '#{alert.text}' instead."
392
+ end
382
393
 
383
394
  alert
384
395
  end
@@ -13,14 +13,12 @@ module Capybara::Selenium::Driver::ChromeDriver
13
13
 
14
14
  def fullscreen_window(handle)
15
15
  within_given_window(handle) do
16
- begin
17
- super
18
- rescue NoMethodError => e
19
- raise unless e.message.match?(/full_screen_window/)
20
-
21
- result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
22
- result['value']
23
- end
16
+ super
17
+ rescue NoMethodError => e
18
+ raise unless e.message.match?(/full_screen_window/)
19
+
20
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
21
+ result['value']
24
22
  end
25
23
  end
26
24
 
@@ -65,7 +63,7 @@ private
65
63
  end
66
64
 
67
65
  def clear_all_storage?
68
- storage_clears.none? { |s| s == false }
66
+ storage_clears.none? false
69
67
  end
70
68
 
71
69
  def uniform_storage_clear?
@@ -96,7 +94,7 @@ private
96
94
 
97
95
  def execute_cdp(cmd, params = {})
98
96
  if browser.respond_to? :execute_cdp
99
- browser.execute_cdp(cmd, params)
97
+ browser.execute_cdp(cmd, **params)
100
98
  else
101
99
  args = { cmd: cmd, params: params }
102
100
  result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
@@ -13,14 +13,12 @@ module Capybara::Selenium::Driver::EdgeDriver
13
13
  return super if edgedriver_version < 75
14
14
 
15
15
  within_given_window(handle) do
16
- begin
17
- super
18
- rescue NoMethodError => e
19
- raise unless e.message.match?(/full_screen_window/)
20
-
21
- result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
22
- result['value']
23
- end
16
+ super
17
+ rescue NoMethodError => e
18
+ raise unless e.message.match?(/full_screen_window/)
19
+
20
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
21
+ result['value']
24
22
  end
25
23
  end
26
24
 
@@ -74,7 +72,7 @@ private
74
72
  end
75
73
 
76
74
  def clear_all_storage?
77
- storage_clears.none? { |s| s == false }
75
+ storage_clears.none? false
78
76
  end
79
77
 
80
78
  def uniform_storage_clear?
@@ -46,7 +46,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
46
46
  begin
47
47
  # Firefox 68 hangs if we try to switch windows while a modal is visible
48
48
  browser.switch_to.alert&.dismiss
49
- rescue Selenium::WebDriver::Error::NoSuchAlertError # rubocop:disable Lint/HandleExceptions
49
+ rescue Selenium::WebDriver::Error::NoSuchAlertError
50
50
  # Swallow
51
51
  end
52
52
  end
@@ -61,7 +61,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
61
61
  accept_modal :confirm, wait: 0.1 do
62
62
  super
63
63
  end
64
- rescue Capybara::ModalNotFound # rubocop:disable Lint/HandleExceptions
64
+ rescue Capybara::ModalNotFound
65
65
  # No modal was opened - page has refreshed - ignore
66
66
  end
67
67
 
@@ -4,25 +4,32 @@ class Capybara::Selenium::Node
4
4
  module Html5Drag
5
5
  # Implement methods to emulate HTML5 drag and drop
6
6
 
7
- def drag_to(element, html5: nil, delay: 0.05)
7
+ def drag_to(element, html5: nil, delay: 0.05, drop_modifiers: [])
8
+ drop_modifiers = Array(drop_modifiers)
9
+
8
10
  driver.execute_script MOUSEDOWN_TRACKER
9
11
  scroll_if_needed { browser_action.click_and_hold(native).perform }
10
12
  html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
11
13
  if html5
12
- perform_html5_drag(element, delay)
14
+ perform_html5_drag(element, delay, drop_modifiers)
13
15
  else
14
- perform_legacy_drag(element)
16
+ perform_legacy_drag(element, drop_modifiers)
15
17
  end
16
18
  end
17
19
 
18
20
  private
19
21
 
20
- def perform_legacy_drag(element)
21
- element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
22
+ def perform_legacy_drag(element, drop_modifiers)
23
+ element.scroll_if_needed do
24
+ # browser_action.move_to(element.native).release.perform
25
+ keys_down = modifiers_down(browser_action, drop_modifiers)
26
+ keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
27
+ keys_up.perform
28
+ end
22
29
  end
23
30
 
24
- def perform_html5_drag(element, delay)
25
- driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000
31
+ def perform_html5_drag(element, delay, drop_modifiers)
32
+ driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000, normalize_keys(drop_modifiers)
26
33
  browser_action.release.perform
27
34
  end
28
35
 
@@ -153,6 +160,14 @@ class Capybara::Selenium::Node
153
160
  var targetRect = target.getBoundingClientRect();
154
161
  var sourceCenter = rectCenter(source.getBoundingClientRect());
155
162
 
163
+ for (var i = 0; i < drop_modifier_keys.length; i++) {
164
+ key = drop_modifier_keys[i];
165
+ if (key == "control"){
166
+ key = "ctrl"
167
+ }
168
+ opts[key + 'Key'] = true;
169
+ }
170
+
156
171
  // fire 2 dragover events to simulate dragging with a direction
157
172
  var entryPoint = pointOnRect(sourceCenter, targetRect)
158
173
  var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
@@ -166,17 +181,18 @@ class Capybara::Selenium::Node
166
181
  var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
167
182
  var dragOverEvent = new DragEvent('dragover', dragOverOpts);
168
183
  target.dispatchEvent(dragOverEvent);
169
- window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented);
184
+ window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
170
185
  }
171
186
 
172
- function dragLeave(drop) {
173
- var dragLeaveEvent = new DragEvent('dragleave', opts);
187
+ function dragLeave(drop, dragOverOpts) {
188
+ var dragLeaveOptions = Object.assign({}, opts, dragOverOpts);
189
+ var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
174
190
  target.dispatchEvent(dragLeaveEvent);
175
191
  if (drop) {
176
- var dropEvent = new DragEvent('drop', opts);
192
+ var dropEvent = new DragEvent('drop', dragLeaveOptions);
177
193
  target.dispatchEvent(dropEvent);
178
194
  }
179
- var dragEndEvent = new DragEvent('dragend', opts);
195
+ var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
180
196
  source.dispatchEvent(dragEndEvent);
181
197
  callback.call(true);
182
198
  }
@@ -184,7 +200,8 @@ class Capybara::Selenium::Node
184
200
  var source = arguments[0],
185
201
  target = arguments[1],
186
202
  step_delay = arguments[2],
187
- callback = arguments[3];
203
+ drop_modifier_keys = arguments[3],
204
+ callback = arguments[4];
188
205
 
189
206
  var dt = new DataTransfer();
190
207
  var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
@@ -54,7 +54,9 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
54
54
  # :backspace => send backspace keystrokes to clear the field <br/>
55
55
  # Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
56
56
  def set(value, **options)
57
- raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}" if value.is_a?(Array) && !multiple?
57
+ if value.is_a?(Array) && !multiple?
58
+ raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
59
+ end
58
60
 
59
61
  tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
60
62
  @tag_name ||= tag_name
@@ -76,11 +78,13 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
76
78
  set_datetime_local(value)
77
79
  when 'color'
78
80
  set_color(value)
81
+ when 'range'
82
+ set_range(value)
79
83
  else
80
- set_text(value, options)
84
+ set_text(value, **options)
81
85
  end
82
86
  when 'textarea'
83
- set_text(value, options)
87
+ set_text(value, **options)
84
88
  else
85
89
  set_content_editable(value)
86
90
  end
@@ -100,7 +104,20 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
100
104
  click_options = ClickOptions.new(keys, options)
101
105
  return native.click if click_options.empty?
102
106
 
103
- click_with_options(click_options)
107
+ perform_with_options(click_options) do |action|
108
+ target = click_options.coords? ? nil : native
109
+ if click_options.delay.zero?
110
+ action.click(target)
111
+ else
112
+ action.click_and_hold(target)
113
+ if w3c?
114
+ action.pause(action.pointer_inputs.first, click_options.delay)
115
+ else
116
+ action.pause(click_options.delay)
117
+ end
118
+ action.release
119
+ end
120
+ end
104
121
  rescue StandardError => e
105
122
  if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
106
123
  e.message.match?(/Other element would receive the click/)
@@ -112,14 +129,26 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
112
129
 
113
130
  def right_click(keys = [], **options)
114
131
  click_options = ClickOptions.new(keys, options)
115
- click_with_options(click_options) do |action|
116
- click_options.coords? ? action.context_click : action.context_click(native)
132
+ perform_with_options(click_options) do |action|
133
+ target = click_options.coords? ? nil : native
134
+ if click_options.delay.zero?
135
+ action.context_click(target)
136
+ elsif w3c?
137
+ action.move_to(target) if target
138
+ action.pointer_down(:right)
139
+ .pause(action.pointer_inputs.first, click_options.delay)
140
+ .pointer_up(:right)
141
+ else
142
+ raise ArgumentError, 'Delay is not supported when right clicking with legacy (non-w3c) selenium driver'
143
+ end
117
144
  end
118
145
  end
119
146
 
120
147
  def double_click(keys = [], **options)
121
148
  click_options = ClickOptions.new(keys, options)
122
- click_with_options(click_options) do |action|
149
+ raise ArgumentError, "double_click doesn't support a delay option" unless click_options.delay.zero?
150
+
151
+ perform_with_options(click_options) do |action|
123
152
  click_options.coords? ? action.double_click : action.double_click(native)
124
153
  end
125
154
  end
@@ -132,11 +161,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
132
161
  scroll_if_needed { browser_action.move_to(native).perform }
133
162
  end
134
163
 
135
- def drag_to(element, **)
164
+ def drag_to(element, drop_modifiers: [], **)
165
+ drop_modifiers = Array(drop_modifiers)
136
166
  # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
137
167
  # which means Seleniums `drag_and_drop` is now broken - do it manually
138
168
  scroll_if_needed { browser_action.click_and_hold(native).perform }
139
- element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
169
+ # element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
170
+ element.scroll_if_needed do
171
+ keys_down = modifiers_down(browser_action, drop_modifiers)
172
+ keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
173
+ keys_up.perform
174
+ end
140
175
  end
141
176
 
142
177
  def drop(*_)
@@ -202,7 +237,7 @@ protected
202
237
  JS
203
238
  begin
204
239
  driver.execute_script(script, self)
205
- rescue StandardError # rubocop:disable Lint/HandleExceptions
240
+ rescue StandardError
206
241
  # Swallow error if scrollIntoView with options isn't supported
207
242
  end
208
243
  end
@@ -232,7 +267,7 @@ private
232
267
  find_xpath(XPath.ancestor(:select)[1]).first
233
268
  end
234
269
 
235
- def set_text(value, clear: nil, **_unused)
270
+ def set_text(value, clear: nil, rapid: nil, **_unused)
236
271
  value = value.to_s
237
272
  if value.empty? && clear.nil?
238
273
  native.clear
@@ -244,11 +279,23 @@ private
244
279
  send_keys(*clear, value)
245
280
  else
246
281
  driver.execute_script 'arguments[0].select()', self unless clear == :none
247
- send_keys(value)
282
+ if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
283
+ send_keys(value[0..3])
284
+ driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
285
+ send_keys(value[-3..-1])
286
+ else
287
+ send_keys(value)
288
+ end
248
289
  end
249
290
  end
250
291
 
251
- def click_with_options(click_options)
292
+ def auto_rapid_set_length
293
+ 30
294
+ end
295
+
296
+ def perform_with_options(click_options, &block)
297
+ raise ArgumentError, 'A block must be provided' unless block
298
+
252
299
  scroll_if_needed do
253
300
  action_with_modifiers(click_options) do |action|
254
301
  if block_given?
@@ -288,6 +335,10 @@ private
288
335
  update_value_js(value)
289
336
  end
290
337
 
338
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
339
+ update_value_js(value)
340
+ end
341
+
291
342
  def update_value_js(value)
292
343
  driver.execute_script(<<-JS, self, value)
293
344
  if (arguments[0].readOnly) { return };
@@ -373,10 +424,12 @@ private
373
424
 
374
425
  def modifiers_down(actions, keys)
375
426
  each_key(keys) { |key| actions.key_down(key) }
427
+ actions
376
428
  end
377
429
 
378
430
  def modifiers_up(actions, keys)
379
431
  each_key(keys) { |key| actions.key_up(key) }
432
+ actions
380
433
  end
381
434
 
382
435
  def browser
@@ -391,18 +444,30 @@ private
391
444
  browser.action
392
445
  end
393
446
 
394
- def each_key(keys)
395
- keys.each do |key|
396
- key = case key
447
+ def capabilities
448
+ browser.capabilities
449
+ end
450
+
451
+ def w3c?
452
+ (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
453
+ capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
454
+ end
455
+
456
+ def normalize_keys(keys)
457
+ keys.map do |key|
458
+ case key
397
459
  when :ctrl then :control
398
460
  when :command, :cmd then :meta
399
461
  else
400
462
  key
401
463
  end
402
- yield key
403
464
  end
404
465
  end
405
466
 
467
+ def each_key(keys)
468
+ normalize_keys(keys).each { |key| yield(key) }
469
+ end
470
+
406
471
  def find_context
407
472
  native
408
473
  end
@@ -469,6 +534,16 @@ private
469
534
  })(arguments[0], arguments[1], arguments[2])
470
535
  JS
471
536
 
537
+ RAPID_APPEND_TEXT = <<~'JS'
538
+ (function(el, value) {
539
+ value = el.value + value;
540
+ if (el.maxLength && el.maxLength != -1){
541
+ value = value.slice(0, el.maxLength);
542
+ }
543
+ el.value = value;
544
+ })(arguments[0], arguments[1])
545
+ JS
546
+
472
547
  # SettableValue encapsulates time/date field formatting
473
548
  class SettableValue
474
549
  attr_reader :value
@@ -525,7 +600,11 @@ private
525
600
  end
526
601
 
527
602
  def empty?
528
- keys.empty? && !coords?
603
+ keys.empty? && !coords? && delay.zero?
604
+ end
605
+
606
+ def delay
607
+ options[:delay] || 0
529
608
  end
530
609
  end
531
610
  private_constant :ClickOptions