capybara 3.30.0 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +72 -0
  3. data/README.md +10 -3
  4. data/lib/capybara.rb +17 -7
  5. data/lib/capybara/cucumber.rb +1 -1
  6. data/lib/capybara/dsl.rb +10 -2
  7. data/lib/capybara/minitest.rb +232 -144
  8. data/lib/capybara/minitest/spec.rb +153 -97
  9. data/lib/capybara/node/actions.rb +16 -20
  10. data/lib/capybara/node/element.rb +13 -8
  11. data/lib/capybara/node/finders.rb +5 -1
  12. data/lib/capybara/node/matchers.rb +28 -21
  13. data/lib/capybara/node/simple.rb +1 -1
  14. data/lib/capybara/queries/base_query.rb +2 -1
  15. data/lib/capybara/queries/selector_query.rb +8 -1
  16. data/lib/capybara/queries/style_query.rb +1 -1
  17. data/lib/capybara/queries/text_query.rb +6 -0
  18. data/lib/capybara/rack_test/browser.rb +3 -1
  19. data/lib/capybara/rack_test/node.rb +34 -9
  20. data/lib/capybara/registration_container.rb +44 -0
  21. data/lib/capybara/registrations/servers.rb +1 -1
  22. data/lib/capybara/result.rb +29 -5
  23. data/lib/capybara/rspec/matcher_proxies.rb +4 -4
  24. data/lib/capybara/rspec/matchers.rb +27 -27
  25. data/lib/capybara/rspec/matchers/base.rb +12 -6
  26. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  27. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  28. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  29. data/lib/capybara/rspec/matchers/have_selector.rb +15 -7
  30. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  31. data/lib/capybara/rspec/matchers/have_text.rb +3 -3
  32. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  33. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  34. data/lib/capybara/rspec/matchers/match_style.rb +2 -2
  35. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  36. data/lib/capybara/selector.rb +12 -1
  37. data/lib/capybara/selector/definition.rb +5 -4
  38. data/lib/capybara/selector/definition/button.rb +1 -0
  39. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  40. data/lib/capybara/selector/definition/label.rb +1 -1
  41. data/lib/capybara/selector/definition/link.rb +8 -0
  42. data/lib/capybara/selector/definition/select.rb +31 -12
  43. data/lib/capybara/selector/definition/table.rb +1 -1
  44. data/lib/capybara/selector/selector.rb +4 -0
  45. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  46. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  47. data/lib/capybara/selenium/driver.rb +7 -4
  48. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +8 -10
  49. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +7 -9
  50. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -2
  51. data/lib/capybara/selenium/extensions/html5_drag.rb +24 -8
  52. data/lib/capybara/selenium/node.rb +92 -15
  53. data/lib/capybara/selenium/nodes/chrome_node.rb +4 -11
  54. data/lib/capybara/selenium/nodes/edge_node.rb +1 -1
  55. data/lib/capybara/selenium/nodes/firefox_node.rb +3 -3
  56. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  57. data/lib/capybara/selenium/patches/logs.rb +3 -5
  58. data/lib/capybara/session.rb +33 -18
  59. data/lib/capybara/session/config.rb +3 -1
  60. data/lib/capybara/spec/public/test.js +58 -6
  61. data/lib/capybara/spec/session/all_spec.rb +45 -5
  62. data/lib/capybara/spec/session/assert_text_spec.rb +5 -5
  63. data/lib/capybara/spec/session/click_button_spec.rb +11 -0
  64. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  65. data/lib/capybara/spec/session/find_spec.rb +11 -8
  66. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  67. data/lib/capybara/spec/session/has_css_spec.rb +12 -9
  68. data/lib/capybara/spec/session/has_current_path_spec.rb +2 -2
  69. data/lib/capybara/spec/session/has_field_spec.rb +16 -0
  70. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  71. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  72. data/lib/capybara/spec/session/has_text_spec.rb +5 -1
  73. data/lib/capybara/spec/session/node_spec.rb +146 -30
  74. data/lib/capybara/spec/session/window/window_spec.rb +7 -7
  75. data/lib/capybara/spec/spec_helper.rb +2 -2
  76. data/lib/capybara/spec/test_app.rb +14 -18
  77. data/lib/capybara/spec/views/form.erb +13 -2
  78. data/lib/capybara/spec/views/with_dragula.erb +3 -1
  79. data/lib/capybara/spec/views/with_html.erb +2 -2
  80. data/lib/capybara/spec/views/with_js.erb +1 -0
  81. data/lib/capybara/version.rb +1 -1
  82. data/spec/capybara_spec.rb +1 -1
  83. data/spec/dsl_spec.rb +14 -1
  84. data/spec/minitest_spec.rb +1 -1
  85. data/spec/rack_test_spec.rb +13 -1
  86. data/spec/regexp_dissassembler_spec.rb +0 -4
  87. data/spec/result_spec.rb +40 -29
  88. data/spec/rspec/shared_spec_matchers.rb +65 -53
  89. data/spec/selector_spec.rb +1 -1
  90. data/spec/selenium_spec_chrome.rb +6 -3
  91. data/spec/selenium_spec_chrome_remote.rb +2 -0
  92. data/spec/server_spec.rb +41 -49
  93. data/spec/shared_selenium_node.rb +18 -0
  94. data/spec/shared_selenium_session.rb +25 -7
  95. data/spec/spec_helper.rb +1 -1
  96. metadata +5 -3
@@ -10,6 +10,7 @@ module Capybara
10
10
  class Selector
11
11
  class Definition
12
12
  attr_reader :name, :expressions
13
+
13
14
  extend Forwardable
14
15
 
15
16
  def initialize(name, locator_type: nil, raw_locator: false, supports_exact: nil, &block)
@@ -189,7 +190,7 @@ module Capybara
189
190
  def describe_all_expression_filters(**opts)
190
191
  expression_filters.map do |ef_name, ef|
191
192
  if ef.matcher?
192
- handled_custom_keys(ef, opts.keys).map { |key| " with #{ef_name}[#{key} => #{opts[key]}]" }.join
193
+ handled_custom_options(ef, opts).map { |option, value| " with #{ef_name}[#{option} => #{value}]" }.join
193
194
  elsif opts.key?(ef_name)
194
195
  " with #{ef_name} #{opts[ef_name]}"
195
196
  end
@@ -251,9 +252,9 @@ module Capybara
251
252
 
252
253
  private
253
254
 
254
- def handled_custom_keys(filter, keys)
255
- keys.select do |key|
256
- filter.handles_option?(key) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(key)
255
+ def handled_custom_options(filter, options)
256
+ options.select do |option, _|
257
+ filter.handles_option?(option) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(option)
257
258
  end
258
259
  end
259
260
 
@@ -4,6 +4,7 @@ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
4
4
  xpath(:value, :title, :type, :name) do |locator, **options|
5
5
  input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
6
6
  btn_xpath = XPath.descendant(:button)
7
+ btn_xpath += XPath.descendant[XPath.attr(:role).equals('button')] if enable_aria_role
7
8
  image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
8
9
 
9
10
  unless locator.nil?
@@ -18,7 +18,7 @@ Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
18
18
  end
19
19
  end
20
20
 
21
- filter_set(:_field, %i[disabled multiple name placeholder valid])
21
+ filter_set(:_field, %i[disabled multiple name placeholder valid validation_message])
22
22
 
23
23
  node_filter(:with) do |node, with|
24
24
  val = node.value
@@ -2,7 +2,7 @@
2
2
 
3
3
  Capybara.add_selector(:label, locator_type: [String, Symbol]) do
4
4
  label 'label'
5
- xpath(:for) do |locator, options|
5
+ xpath(:for) do |locator, **options|
6
6
  xpath = XPath.descendant(:label)
7
7
  unless locator.nil?
8
8
  locator_matchers = XPath.string.n.is(locator.to_s) | (XPath.attr(:id) == locator.to_s)
@@ -5,6 +5,13 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
5
5
  xpath = XPath.descendant(:a)
6
6
  xpath = builder(xpath).add_attribute_conditions(href: href) unless href == false
7
7
 
8
+ if enable_aria_role
9
+ role_path = XPath.descendant[XPath.attr(:role).equals('link')]
10
+ role_path = builder(role_path).add_attribute_conditions(href: href) unless [true, false].include? href
11
+
12
+ xpath += role_path
13
+ end
14
+
8
15
  unless locator.nil?
9
16
  locator = locator.to_s
10
17
  matchers = [XPath.attr(:id) == locator,
@@ -18,6 +25,7 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
18
25
 
19
26
  xpath = xpath[find_by_attr(:title, title)]
20
27
  xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt) == alt]] if alt
28
+
21
29
  xpath
22
30
  end
23
31
 
@@ -11,16 +11,26 @@ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
11
11
  filter_set(:_field, %i[disabled multiple name placeholder])
12
12
 
13
13
  node_filter(:options) do |node, options|
14
- actual = if node.visible?
15
- node.all(:xpath, './/option', wait: false).map(&:text)
16
- else
17
- node.all(:xpath, './/option', visible: false, wait: false).map { |option| option.text(:all) }
18
- end
14
+ actual = options_text(node)
19
15
  (options.sort == actual.sort).tap do |res|
20
16
  add_error("Expected options #{options.inspect} found #{actual.inspect}") unless res
21
17
  end
22
18
  end
23
19
 
20
+ node_filter(:enabled_options) do |node, options|
21
+ actual = options_text(node) { |o| !o.disabled? }
22
+ (options.sort == actual.sort).tap do |res|
23
+ add_error("Expected enabled options #{options.inspect} found #{actual.inspect}") unless res
24
+ end
25
+ end
26
+
27
+ node_filter(:disabled_options) do |node, options|
28
+ actual = options_text(node, &:disabled?)
29
+ (options.sort == actual.sort).tap do |res|
30
+ add_error("Expected disabled options #{options.inspect} found #{actual.inspect}") unless res
31
+ end
32
+ end
33
+
24
34
  expression_filter(:with_options) do |expr, options|
25
35
  options.inject(expr) do |xpath, option|
26
36
  xpath[expression_for(:option, option)]
@@ -28,18 +38,14 @@ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
28
38
  end
29
39
 
30
40
  node_filter(:selected) do |node, selected|
31
- actual = node.all(:xpath, './/option', visible: false, wait: false)
32
- .select(&:selected?)
33
- .map { |option| option.text(:all) }
41
+ actual = options_text(node, visible: false, &:selected?)
34
42
  (Array(selected).sort == actual.sort).tap do |res|
35
43
  add_error("Expected #{selected.inspect} to be selected found #{actual.inspect}") unless res
36
44
  end
37
45
  end
38
46
 
39
47
  node_filter(:with_selected) do |node, selected|
40
- actual = node.all(:xpath, './/option', visible: false, wait: false)
41
- .select(&:selected?)
42
- .map { |option| option.text(:all) }
48
+ actual = options_text(node, visible: false, &:selected?)
43
49
  (Array(selected) - actual).empty?.tap do |res|
44
50
  add_error("Expected at least #{selected.inspect} to be selected found #{actual.inspect}") unless res
45
51
  end
@@ -51,12 +57,25 @@ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
51
57
  desc
52
58
  end
53
59
 
54
- describe_node_filters do |options: nil, selected: nil, with_selected: nil, disabled: nil, **|
60
+ describe_node_filters do |
61
+ options: nil, disabled_options: nil, enabled_options: nil,
62
+ selected: nil, with_selected: nil,
63
+ disabled: nil, **|
55
64
  desc = +''
56
65
  desc << " with options #{options.inspect}" if options
66
+ desc << " with disabled options #{disabled_options.inspect}}" if disabled_options
67
+ desc << " with enabled options #{enabled_options.inspect}" if enabled_options
57
68
  desc << " with #{selected.inspect} selected" if selected
58
69
  desc << " with at least #{with_selected.inspect} selected" if with_selected
59
70
  desc << ' which is disabled' if disabled
60
71
  desc
61
72
  end
73
+
74
+ def options_text(node, **opts, &filter_block)
75
+ opts[:wait] = false
76
+ opts[:visible] = false unless node.visible?
77
+ node.all(:xpath, './/option', **opts, &filter_block).map do |o|
78
+ o.text((:all if opts[:visible] == false))
79
+ end
80
+ end
62
81
  end
@@ -43,7 +43,7 @@ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
43
43
  end
44
44
 
45
45
  expression_filter(:cols, valid_values: [Array]) do |xpath, cols|
46
- raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all? { |col| col.is_a? Array }
46
+ raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all?(Array)
47
47
 
48
48
  rows = cols.transpose
49
49
  col_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
@@ -48,6 +48,10 @@ module Capybara
48
48
  @config[:enable_aria_label]
49
49
  end
50
50
 
51
+ def enable_aria_role
52
+ @config[:enable_aria_role]
53
+ end
54
+
51
55
  def test_id
52
56
  @config[:test_id]
53
57
  end
@@ -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,6 +20,7 @@ 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
+ require 'capybara/selenium/patches/action_pauser'
23
24
  if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
24
25
  warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade."
25
26
  end
@@ -84,6 +85,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
84
85
 
85
86
  def html
86
87
  browser.page_source
88
+ rescue Selenium::WebDriver::Error::JavascriptError => e
89
+ raise unless e.message.match?(/documentElement is null/)
87
90
  end
88
91
 
89
92
  def title
@@ -240,7 +243,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
240
243
 
241
244
  def quit
242
245
  @browser&.quit
243
- rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED # rubocop:disable Lint/SuppressedException
246
+ rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
244
247
  # Browser must have already gone
245
248
  rescue Selenium::WebDriver::Error::UnknownError => e
246
249
  unless silenced_unknown_error_message?(e.message) # Most likely already gone
@@ -292,7 +295,7 @@ private
292
295
  def clear_browser_state
293
296
  delete_all_cookies
294
297
  clear_storage
295
- rescue *clear_browser_state_errors # rubocop:disable Lint/SuppressedException
298
+ rescue *clear_browser_state_errors
296
299
  # delete_all_cookies fails when we've previously gone
297
300
  # to about:blank, so we rescue this error and do nothing
298
301
  # instead.
@@ -316,7 +319,7 @@ private
316
319
  def clear_storage
317
320
  clear_session_storage unless options[:clear_session_storage] == false
318
321
  clear_local_storage unless options[:clear_local_storage] == false
319
- rescue Selenium::WebDriver::Error::JavascriptError # rubocop:disable Lint/SuppressedException
322
+ rescue Selenium::WebDriver::Error::JavascriptError
320
323
  # session/local storage may not be available if on non-http pages (e.g. about:blank)
321
324
  end
322
325
 
@@ -352,7 +355,7 @@ private
352
355
  @browser.navigate.to(url)
353
356
  sleep 0.1 # slight wait for alert
354
357
  @browser.switch_to.alert.accept
355
- rescue modal_error # rubocop:disable Lint/SuppressedException
358
+ rescue modal_error
356
359
  # alert now gone, should mean navigation happened
357
360
  end
358
361
 
@@ -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/SuppressedException
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/SuppressedException
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);
@@ -185,7 +200,8 @@ class Capybara::Selenium::Node
185
200
  var source = arguments[0],
186
201
  target = arguments[1],
187
202
  step_delay = arguments[2],
188
- callback = arguments[3];
203
+ drop_modifier_keys = arguments[3],
204
+ callback = arguments[4];
189
205
 
190
206
  var dt = new DataTransfer();
191
207
  var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
@@ -78,6 +78,8 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
78
78
  set_datetime_local(value)
79
79
  when 'color'
80
80
  set_color(value)
81
+ when 'range'
82
+ set_range(value)
81
83
  else
82
84
  set_text(value, **options)
83
85
  end
@@ -102,7 +104,20 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
102
104
  click_options = ClickOptions.new(keys, options)
103
105
  return native.click if click_options.empty?
104
106
 
105
- 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
106
121
  rescue StandardError => e
107
122
  if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
108
123
  e.message.match?(/Other element would receive the click/)
@@ -114,14 +129,26 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
114
129
 
115
130
  def right_click(keys = [], **options)
116
131
  click_options = ClickOptions.new(keys, options)
117
- click_with_options(click_options) do |action|
118
- 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
119
144
  end
120
145
  end
121
146
 
122
147
  def double_click(keys = [], **options)
123
148
  click_options = ClickOptions.new(keys, options)
124
- 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|
125
152
  click_options.coords? ? action.double_click : action.double_click(native)
126
153
  end
127
154
  end
@@ -134,11 +161,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
134
161
  scroll_if_needed { browser_action.move_to(native).perform }
135
162
  end
136
163
 
137
- def drag_to(element, **)
164
+ def drag_to(element, drop_modifiers: [], **)
165
+ drop_modifiers = Array(drop_modifiers)
138
166
  # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
139
167
  # which means Seleniums `drag_and_drop` is now broken - do it manually
140
168
  scroll_if_needed { browser_action.click_and_hold(native).perform }
141
- 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
142
175
  end
143
176
 
144
177
  def drop(*_)
@@ -204,7 +237,7 @@ protected
204
237
  JS
205
238
  begin
206
239
  driver.execute_script(script, self)
207
- rescue StandardError # rubocop:disable Lint/SuppressedException
240
+ rescue StandardError
208
241
  # Swallow error if scrollIntoView with options isn't supported
209
242
  end
210
243
  end
@@ -234,7 +267,7 @@ private
234
267
  find_xpath(XPath.ancestor(:select)[1]).first
235
268
  end
236
269
 
237
- def set_text(value, clear: nil, **_unused)
270
+ def set_text(value, clear: nil, rapid: nil, **_unused)
238
271
  value = value.to_s
239
272
  if value.empty? && clear.nil?
240
273
  native.clear
@@ -246,11 +279,23 @@ private
246
279
  send_keys(*clear, value)
247
280
  else
248
281
  driver.execute_script 'arguments[0].select()', self unless clear == :none
249
- 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
250
289
  end
251
290
  end
252
291
 
253
- 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
+
254
299
  scroll_if_needed do
255
300
  action_with_modifiers(click_options) do |action|
256
301
  if block_given?
@@ -290,6 +335,10 @@ private
290
335
  update_value_js(value)
291
336
  end
292
337
 
338
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
339
+ update_value_js(value)
340
+ end
341
+
293
342
  def update_value_js(value)
294
343
  driver.execute_script(<<-JS, self, value)
295
344
  if (arguments[0].readOnly) { return };
@@ -375,10 +424,12 @@ private
375
424
 
376
425
  def modifiers_down(actions, keys)
377
426
  each_key(keys) { |key| actions.key_down(key) }
427
+ actions
378
428
  end
379
429
 
380
430
  def modifiers_up(actions, keys)
381
431
  each_key(keys) { |key| actions.key_up(key) }
432
+ actions
382
433
  end
383
434
 
384
435
  def browser
@@ -393,18 +444,30 @@ private
393
444
  browser.action
394
445
  end
395
446
 
396
- def each_key(keys)
397
- keys.each do |key|
398
- 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
399
459
  when :ctrl then :control
400
460
  when :command, :cmd then :meta
401
461
  else
402
462
  key
403
463
  end
404
- yield key
405
464
  end
406
465
  end
407
466
 
467
+ def each_key(keys)
468
+ normalize_keys(keys).each { |key| yield(key) }
469
+ end
470
+
408
471
  def find_context
409
472
  native
410
473
  end
@@ -471,6 +534,16 @@ private
471
534
  })(arguments[0], arguments[1], arguments[2])
472
535
  JS
473
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
+
474
547
  # SettableValue encapsulates time/date field formatting
475
548
  class SettableValue
476
549
  attr_reader :value
@@ -527,7 +600,11 @@ private
527
600
  end
528
601
 
529
602
  def empty?
530
- keys.empty? && !coords?
603
+ keys.empty? && !coords? && delay.zero?
604
+ end
605
+
606
+ def delay
607
+ options[:delay] || 0
531
608
  end
532
609
  end
533
610
  private_constant :ClickOptions