capybara 3.29.0 → 3.36.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (200) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +203 -15
  3. data/README.md +13 -4
  4. data/lib/capybara/config.rb +24 -10
  5. data/lib/capybara/cucumber.rb +1 -1
  6. data/lib/capybara/driver/base.rb +8 -0
  7. data/lib/capybara/driver/node.rb +1 -1
  8. data/lib/capybara/dsl.rb +10 -2
  9. data/lib/capybara/helpers.rb +19 -2
  10. data/lib/capybara/minitest/spec.rb +156 -97
  11. data/lib/capybara/minitest.rb +232 -144
  12. data/lib/capybara/node/actions.rb +41 -37
  13. data/lib/capybara/node/base.rb +6 -6
  14. data/lib/capybara/node/document.rb +2 -2
  15. data/lib/capybara/node/document_matchers.rb +3 -3
  16. data/lib/capybara/node/element.rb +24 -21
  17. data/lib/capybara/node/finders.rb +25 -18
  18. data/lib/capybara/node/matchers.rb +72 -57
  19. data/lib/capybara/node/simple.rb +13 -3
  20. data/lib/capybara/queries/active_element_query.rb +18 -0
  21. data/lib/capybara/queries/ancestor_query.rb +4 -3
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +14 -4
  24. data/lib/capybara/queries/selector_query.rb +71 -23
  25. data/lib/capybara/queries/sibling_query.rb +4 -3
  26. data/lib/capybara/queries/style_query.rb +1 -1
  27. data/lib/capybara/queries/text_query.rb +7 -1
  28. data/lib/capybara/rack_test/browser.rb +13 -4
  29. data/lib/capybara/rack_test/driver.rb +2 -1
  30. data/lib/capybara/rack_test/form.rb +2 -2
  31. data/lib/capybara/rack_test/node.rb +43 -15
  32. data/lib/capybara/registration_container.rb +44 -0
  33. data/lib/capybara/registrations/drivers.rb +18 -12
  34. data/lib/capybara/registrations/patches/puma_ssl.rb +3 -1
  35. data/lib/capybara/registrations/servers.rb +3 -2
  36. data/lib/capybara/result.rb +35 -15
  37. data/lib/capybara/rspec/matcher_proxies.rb +8 -8
  38. data/lib/capybara/rspec/matchers/base.rb +12 -6
  39. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  40. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  41. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  42. data/lib/capybara/rspec/matchers/have_selector.rb +16 -8
  43. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  44. data/lib/capybara/rspec/matchers/have_text.rb +3 -3
  45. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  46. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  47. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  48. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  49. data/lib/capybara/rspec/matchers.rb +33 -32
  50. data/lib/capybara/rspec.rb +2 -0
  51. data/lib/capybara/selector/builders/css_builder.rb +2 -2
  52. data/lib/capybara/selector/builders/xpath_builder.rb +4 -2
  53. data/lib/capybara/selector/css.rb +2 -2
  54. data/lib/capybara/selector/definition/button.rb +35 -13
  55. data/lib/capybara/selector/definition/checkbox.rb +3 -3
  56. data/lib/capybara/selector/definition/css.rb +3 -1
  57. data/lib/capybara/selector/definition/datalist_input.rb +2 -2
  58. data/lib/capybara/selector/definition/datalist_option.rb +1 -1
  59. data/lib/capybara/selector/definition/element.rb +3 -2
  60. data/lib/capybara/selector/definition/field.rb +1 -1
  61. data/lib/capybara/selector/definition/file_field.rb +2 -2
  62. data/lib/capybara/selector/definition/fillable_field.rb +3 -3
  63. data/lib/capybara/selector/definition/label.rb +5 -3
  64. data/lib/capybara/selector/definition/link.rb +8 -0
  65. data/lib/capybara/selector/definition/radio_button.rb +3 -3
  66. data/lib/capybara/selector/definition/select.rb +33 -14
  67. data/lib/capybara/selector/definition/table.rb +6 -3
  68. data/lib/capybara/selector/definition/table_row.rb +2 -2
  69. data/lib/capybara/selector/definition.rb +14 -11
  70. data/lib/capybara/selector/filter_set.rb +17 -17
  71. data/lib/capybara/selector/filters/base.rb +6 -1
  72. data/lib/capybara/selector/filters/locator_filter.rb +1 -1
  73. data/lib/capybara/selector/selector.rb +13 -3
  74. data/lib/capybara/selector.rb +37 -19
  75. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  76. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  77. data/lib/capybara/selenium/atoms/src/isDisplayed.js +1 -1
  78. data/lib/capybara/selenium/driver.rb +79 -16
  79. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +11 -13
  80. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +10 -12
  81. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +4 -4
  82. data/lib/capybara/selenium/extensions/find.rb +4 -4
  83. data/lib/capybara/selenium/extensions/html5_drag.rb +30 -13
  84. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  85. data/lib/capybara/selenium/logger_suppressor.rb +8 -2
  86. data/lib/capybara/selenium/node.rb +110 -26
  87. data/lib/capybara/selenium/nodes/chrome_node.rb +34 -19
  88. data/lib/capybara/selenium/nodes/edge_node.rb +5 -3
  89. data/lib/capybara/selenium/nodes/firefox_node.rb +11 -6
  90. data/lib/capybara/selenium/nodes/safari_node.rb +3 -3
  91. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  92. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  93. data/lib/capybara/selenium/patches/logs.rb +7 -9
  94. data/lib/capybara/server/animation_disabler.rb +17 -6
  95. data/lib/capybara/server/checker.rb +1 -1
  96. data/lib/capybara/server/middleware.rb +22 -10
  97. data/lib/capybara/server.rb +15 -3
  98. data/lib/capybara/session/config.rb +9 -3
  99. data/lib/capybara/session/matchers.rb +11 -11
  100. data/lib/capybara/session.rb +73 -37
  101. data/lib/capybara/spec/public/test.js +75 -7
  102. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  103. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  104. data/lib/capybara/spec/session/all_spec.rb +62 -9
  105. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  106. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  107. data/lib/capybara/spec/session/check_spec.rb +15 -0
  108. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  109. data/lib/capybara/spec/session/click_button_spec.rb +16 -0
  110. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  111. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  112. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  113. data/lib/capybara/spec/session/find_spec.rb +31 -8
  114. data/lib/capybara/spec/session/has_any_selectors_spec.rb +4 -0
  115. data/lib/capybara/spec/session/has_button_spec.rb +75 -0
  116. data/lib/capybara/spec/session/has_css_spec.rb +14 -10
  117. data/lib/capybara/spec/session/has_current_path_spec.rb +15 -2
  118. data/lib/capybara/spec/session/has_field_spec.rb +40 -0
  119. data/lib/capybara/spec/session/has_link_spec.rb +24 -0
  120. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  121. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  122. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  123. data/lib/capybara/spec/session/has_text_spec.rb +25 -1
  124. data/lib/capybara/spec/session/html_spec.rb +1 -1
  125. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  126. data/lib/capybara/spec/session/node_spec.rb +184 -33
  127. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  128. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  129. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  130. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  131. data/lib/capybara/spec/session/scroll_spec.rb +4 -4
  132. data/lib/capybara/spec/session/selectors_spec.rb +15 -2
  133. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  134. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  135. data/lib/capybara/spec/session/window/window_spec.rb +8 -8
  136. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  137. data/lib/capybara/spec/spec_helper.rb +17 -17
  138. data/lib/capybara/spec/test_app.rb +40 -29
  139. data/lib/capybara/spec/views/animated.erb +1 -1
  140. data/lib/capybara/spec/views/form.erb +52 -6
  141. data/lib/capybara/spec/views/frame_child.erb +1 -1
  142. data/lib/capybara/spec/views/frame_one.erb +1 -1
  143. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  144. data/lib/capybara/spec/views/frame_two.erb +1 -1
  145. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  146. data/lib/capybara/spec/views/layout.erb +10 -0
  147. data/lib/capybara/spec/views/obscured.erb +1 -1
  148. data/lib/capybara/spec/views/offset.erb +2 -1
  149. data/lib/capybara/spec/views/path.erb +2 -2
  150. data/lib/capybara/spec/views/popup_one.erb +1 -1
  151. data/lib/capybara/spec/views/popup_two.erb +1 -1
  152. data/lib/capybara/spec/views/react.erb +2 -2
  153. data/lib/capybara/spec/views/scroll.erb +2 -1
  154. data/lib/capybara/spec/views/spatial.erb +1 -1
  155. data/lib/capybara/spec/views/with_animation.erb +10 -3
  156. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  157. data/lib/capybara/spec/views/with_dragula.erb +5 -3
  158. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  159. data/lib/capybara/spec/views/with_hover.erb +2 -2
  160. data/lib/capybara/spec/views/with_html.erb +3 -3
  161. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  162. data/lib/capybara/spec/views/with_js.erb +5 -3
  163. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  164. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  165. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  166. data/lib/capybara/spec/views/with_sortable_js.erb +3 -3
  167. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  168. data/lib/capybara/spec/views/with_windows.erb +1 -1
  169. data/lib/capybara/spec/views/within_frames.erb +1 -1
  170. data/lib/capybara/version.rb +1 -1
  171. data/lib/capybara/window.rb +3 -7
  172. data/lib/capybara.rb +35 -29
  173. data/spec/basic_node_spec.rb +25 -11
  174. data/spec/capybara_spec.rb +1 -1
  175. data/spec/dsl_spec.rb +16 -3
  176. data/spec/fixtures/selenium_driver_rspec_success.rb +2 -2
  177. data/spec/minitest_spec.rb +3 -2
  178. data/spec/minitest_spec_spec.rb +46 -46
  179. data/spec/rack_test_spec.rb +37 -11
  180. data/spec/regexp_dissassembler_spec.rb +40 -36
  181. data/spec/result_spec.rb +42 -32
  182. data/spec/rspec/features_spec.rb +5 -2
  183. data/spec/rspec/scenarios_spec.rb +4 -0
  184. data/spec/rspec/shared_spec_matchers.rb +68 -56
  185. data/spec/rspec_spec.rb +8 -4
  186. data/spec/selector_spec.rb +17 -2
  187. data/spec/selenium_spec_chrome.rb +48 -25
  188. data/spec/selenium_spec_chrome_remote.rb +13 -6
  189. data/spec/selenium_spec_firefox.rb +25 -17
  190. data/spec/selenium_spec_firefox_remote.rb +2 -2
  191. data/spec/selenium_spec_ie.rb +3 -6
  192. data/spec/selenium_spec_safari.rb +27 -19
  193. data/spec/server_spec.rb +84 -31
  194. data/spec/session_spec.rb +1 -1
  195. data/spec/shared_selenium_node.rb +21 -7
  196. data/spec/shared_selenium_session.rb +114 -19
  197. data/spec/spec_helper.rb +1 -1
  198. metadata +78 -21
  199. data/lib/capybara/spec/session/source_spec.rb +0 -0
  200. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -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 };
@@ -45,20 +45,18 @@ module Capybara
45
45
  JS
46
46
  end
47
47
 
48
+ SCROLL_POSITIONS = {
49
+ top: '0',
50
+ bottom: 'arguments[0].scrollHeight',
51
+ center: '(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
52
+ }.freeze
53
+
48
54
  def scroll_to_location(location)
49
- scroll_y = case location
50
- when :top
51
- '0'
52
- when :bottom
53
- 'arguments[0].scrollHeight'
54
- when :center
55
- '(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
56
- end
57
55
  driver.execute_script <<~JS, self
58
56
  if (arguments[0].scrollTo){
59
- arguments[0].scrollTo(0, #{scroll_y});
57
+ arguments[0].scrollTo(0, #{SCROLL_POSITIONS[location]});
60
58
  } else {
61
- arguments[0].scrollTop = #{scroll_y};
59
+ arguments[0].scrollTop = #{SCROLL_POSITIONS[location]};
62
60
  }
63
61
  JS
64
62
  end
@@ -8,8 +8,14 @@ module Capybara
8
8
  super
9
9
  end
10
10
 
11
- def deprecate(*)
12
- super unless @suppress_for_capybara
11
+ def deprecate(*args, **opts, &block)
12
+ return if @suppress_for_capybara
13
+
14
+ if opts.empty?
15
+ super(*args, &block) # support Selenium 3
16
+ else
17
+ super
18
+ end
13
19
  end
14
20
 
15
21
  def suppress_deprecations
@@ -14,7 +14,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
14
14
  end
15
15
 
16
16
  def all_text
17
- text = driver.evaluate_script('arguments[0].textContent', self)
17
+ text = driver.evaluate_script('arguments[0].textContent', self) || ''
18
18
  text.gsub(/[\u200b\u200e\u200f]/, '')
19
19
  .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
20
20
  .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
@@ -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,10 +104,23 @@ 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
- e.message.match?(/Other element would receive the click/)
123
+ e.message.include?('Other element would receive the click')
107
124
  scroll_to_center
108
125
  end
109
126
 
@@ -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(*_)
@@ -164,10 +199,6 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
164
199
  native.attribute('isContentEditable') == 'true'
165
200
  end
166
201
 
167
- def ==(other)
168
- native == other.native
169
- end
170
-
171
202
  def path
172
203
  driver.evaluate_script GET_XPATH_SCRIPT, self
173
204
  end
@@ -202,7 +233,7 @@ protected
202
233
  JS
203
234
  begin
204
235
  driver.execute_script(script, self)
205
- rescue StandardError # rubocop:disable Lint/HandleExceptions
236
+ rescue StandardError
206
237
  # Swallow error if scrollIntoView with options isn't supported
207
238
  end
208
239
  end
@@ -232,26 +263,38 @@ private
232
263
  find_xpath(XPath.ancestor(:select)[1]).first
233
264
  end
234
265
 
235
- def set_text(value, clear: nil, **_unused)
266
+ def set_text(value, clear: nil, rapid: nil, **_unused)
236
267
  value = value.to_s
237
268
  if value.empty? && clear.nil?
238
269
  native.clear
239
270
  elsif clear == :backspace
240
271
  # Clear field by sending the correct number of backspace keys.
241
272
  backspaces = [:backspace] * self.value.to_s.length
242
- send_keys(*([:end] + backspaces + [value]))
273
+ send_keys(:end, *backspaces, value)
243
274
  elsif clear.is_a? Array
244
275
  send_keys(*clear, value)
245
276
  else
246
277
  driver.execute_script 'arguments[0].select()', self unless clear == :none
247
- send_keys(value)
278
+ if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
279
+ send_keys(value[0..3])
280
+ driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
281
+ send_keys(value[-3..])
282
+ else
283
+ send_keys(value)
284
+ end
248
285
  end
249
286
  end
250
287
 
251
- def click_with_options(click_options)
288
+ def auto_rapid_set_length
289
+ 30
290
+ end
291
+
292
+ def perform_with_options(click_options, &block)
293
+ raise ArgumentError, 'A block must be provided' unless block
294
+
252
295
  scroll_if_needed do
253
296
  action_with_modifiers(click_options) do |action|
254
- if block_given?
297
+ if block
255
298
  yield action
256
299
  else
257
300
  click_options.coords? ? action.click : action.click(native)
@@ -288,6 +331,10 @@ private
288
331
  update_value_js(value)
289
332
  end
290
333
 
334
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
335
+ update_value_js(value)
336
+ end
337
+
291
338
  def update_value_js(value)
292
339
  driver.execute_script(<<-JS, self, value)
293
340
  if (arguments[0].readOnly) { return };
@@ -373,10 +420,12 @@ private
373
420
 
374
421
  def modifiers_down(actions, keys)
375
422
  each_key(keys) { |key| actions.key_down(key) }
423
+ actions
376
424
  end
377
425
 
378
426
  def modifiers_up(actions, keys)
379
427
  each_key(keys) { |key| actions.key_up(key) }
428
+ actions
380
429
  end
381
430
 
382
431
  def browser
@@ -391,18 +440,30 @@ private
391
440
  browser.action
392
441
  end
393
442
 
394
- def each_key(keys)
395
- keys.each do |key|
396
- key = case key
443
+ def capabilities
444
+ browser.capabilities
445
+ end
446
+
447
+ def w3c?
448
+ (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
449
+ capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
450
+ end
451
+
452
+ def normalize_keys(keys)
453
+ keys.map do |key|
454
+ case key
397
455
  when :ctrl then :control
398
456
  when :command, :cmd then :meta
399
457
  else
400
458
  key
401
459
  end
402
- yield key
403
460
  end
404
461
  end
405
462
 
463
+ def each_key(keys, &block)
464
+ normalize_keys(keys).each(&block)
465
+ end
466
+
406
467
  def find_context
407
468
  native
408
469
  end
@@ -423,11 +484,20 @@ private
423
484
  JS
424
485
  end
425
486
 
487
+ def native_id
488
+ # Selenium 3 -> 4 changed the return of ref
489
+ type_or_id, id = native.ref
490
+ id || type_or_id
491
+ end
492
+
426
493
  GET_XPATH_SCRIPT = <<~'JS'
427
494
  (function(el, xml){
428
495
  var xpath = '';
429
496
  var pos, tempitem2;
430
497
 
498
+ if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
499
+ return "(: Shadow DOM element - no XPath :)";
500
+ };
431
501
  while(el !== xml.documentElement) {
432
502
  pos = 0;
433
503
  tempitem2 = el;
@@ -469,6 +539,16 @@ private
469
539
  })(arguments[0], arguments[1], arguments[2])
470
540
  JS
471
541
 
542
+ RAPID_APPEND_TEXT = <<~'JS'
543
+ (function(el, value) {
544
+ value = el.value + value;
545
+ if (el.maxLength && el.maxLength != -1){
546
+ value = value.slice(0, el.maxLength);
547
+ }
548
+ el.value = value;
549
+ })(arguments[0], arguments[1])
550
+ JS
551
+
472
552
  # SettableValue encapsulates time/date field formatting
473
553
  class SettableValue
474
554
  attr_reader :value
@@ -525,7 +605,11 @@ private
525
605
  end
526
606
 
527
607
  def empty?
528
- keys.empty? && !coords?
608
+ keys.empty? && !coords? && delay.zero?
609
+ end
610
+
611
+ def delay
612
+ options[:delay] || 0
529
613
  end
530
614
  end
531
615
  private_constant :ClickOptions
@@ -18,14 +18,16 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
18
18
  # In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set
19
19
  if browser_version >= 75.0
20
20
  driver.execute_script(<<~JS, self)
21
- if (arguments[0].multiple && (arguments[0].files.length > 0)){
21
+ if (arguments[0].multiple && arguments[0].files.length){
22
22
  arguments[0].value = null;
23
23
  }
24
24
  JS
25
25
  end
26
26
  super
27
27
  rescue *file_errors => e
28
- raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload" if e.message.match?(/File not found : .+\n.+/m)
28
+ if e.message.match?(/File not found : .+\n.+/m)
29
+ raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload"
30
+ end
29
31
 
30
32
  raise
31
33
  end
@@ -34,13 +36,15 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
34
36
  html5_drop(*args)
35
37
  end
36
38
 
37
- def click(*)
39
+ def click(*, **)
38
40
  super
39
41
  rescue ::Selenium::WebDriver::Error::ElementClickInterceptedError
40
42
  raise
41
43
  rescue ::Selenium::WebDriver::Error::WebDriverError => e
42
44
  # chromedriver 74 (at least on mac) raises the wrong error for this
43
- raise ::Selenium::WebDriver::Error::ElementClickInterceptedError, e.message if e.message.match?(/element click intercepted/)
45
+ if e.message.include?('element click intercepted')
46
+ raise ::Selenium::WebDriver::Error::ElementClickInterceptedError, e.message
47
+ end
44
48
 
45
49
  raise
46
50
  end
@@ -61,7 +65,7 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
61
65
  return super unless native_displayed?
62
66
 
63
67
  begin
64
- bridge.send(:execute, :is_element_displayed, id: native.ref)
68
+ bridge.send(:execute, :is_element_displayed, id: native_id)
65
69
  rescue Selenium::WebDriver::Error::UnknownCommandError
66
70
  # If the is_element_displayed command is unknown, no point in trying again
67
71
  driver.options[:native_displayed] = false
@@ -69,11 +73,31 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
69
73
  end
70
74
  end
71
75
 
76
+ def send_keys(*args)
77
+ args.chunk { |inp| inp.is_a?(String) && inp.match?(/\p{Emoji Presentation}/) }
78
+ .each do |contains_emoji, inputs|
79
+ if contains_emoji
80
+ inputs.join.grapheme_clusters.chunk { |gc| gc.match?(/\p{Emoji Presentation}/) }
81
+ .each do |emoji, clusters|
82
+ if emoji
83
+ driver.send(:execute_cdp, 'Input.insertText', text: clusters.join)
84
+ else
85
+ super(clusters.join)
86
+ end
87
+ end
88
+ else
89
+ super(*inputs)
90
+ end
91
+ end
92
+ end
93
+
72
94
  private
73
95
 
74
- def perform_legacy_drag(element)
96
+ def perform_legacy_drag(element, drop_modifiers)
75
97
  return super if chromedriver_fixed_actions_key_state? || !w3c? || element.obscured?
76
98
 
99
+ raise ArgumentError, 'Modifier keys are not supported while dragging in this version of Chrome.' unless drop_modifiers.empty?
100
+
77
101
  # W3C Chrome/chromedriver < 77 doesn't maintain mouse button state across actions API performs
78
102
  # https://bugs.chromium.org/p/chromedriver/issues/detail?id=2981
79
103
  browser_action.release.perform
@@ -86,12 +110,7 @@ private
86
110
  end
87
111
  end
88
112
 
89
- def w3c?
90
- (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
91
- capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
92
- end
93
-
94
- def browser_version(to_float = true)
113
+ def browser_version(to_float: true)
95
114
  caps = capabilities
96
115
  ver = (caps[:browser_version] || caps[:version])
97
116
  ver = ver.to_f if to_float
@@ -99,19 +118,15 @@ private
99
118
  end
100
119
 
101
120
  def chromedriver_fixed_actions_key_state?
102
- Gem::Version.new(chromedriver_version) >= Gem::Version.new('76.0.3809.68')
121
+ Gem::Requirement.new('>= 76.0.3809.68').satisfied_by?(chromedriver_version)
103
122
  end
104
123
 
105
124
  def chromedriver_supports_displayed_endpoint?
106
- Gem::Version.new(chromedriver_version) >= Gem::Version.new('76.0.3809.25')
125
+ Gem::Requirement.new('>= 76.0.3809.25').satisfied_by?(chromedriver_version)
107
126
  end
108
127
 
109
128
  def chromedriver_version
110
- capabilities['chrome']['chromedriverVersion'].split(' ')[0]
111
- end
112
-
113
- def capabilities
114
- driver.browser.capabilities
129
+ Gem::Version.new(capabilities['chrome']['chromedriverVersion'].split(' ')[0]) # rubocop:disable Style/RedundantArgument
115
130
  end
116
131
 
117
132
  def native_displayed?
@@ -18,14 +18,16 @@ class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node
18
18
  # In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set
19
19
  if chrome_edge?
20
20
  driver.execute_script(<<~JS, self)
21
- if (arguments[0].multiple && (arguments[0].files.length > 0)){
21
+ if (arguments[0].multiple && arguments[0].files.length){
22
22
  arguments[0].value = null;
23
23
  }
24
24
  JS
25
25
  end
26
26
  super
27
27
  rescue *file_errors => e
28
- raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload" if e.message.match?(/File not found : .+\n.+/m)
28
+ if e.message.match?(/File not found : .+\n.+/m)
29
+ raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload"
30
+ end
29
31
 
30
32
  raise
31
33
  end
@@ -67,7 +69,7 @@ class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node
67
69
  return super unless chrome_edge? && native_displayed?
68
70
 
69
71
  begin
70
- bridge.send(:execute, :is_element_displayed, id: native.ref)
72
+ bridge.send(:execute, :is_element_displayed, id: native_id)
71
73
  rescue Selenium::WebDriver::Error::UnknownCommandError
72
74
  # If the is_element_displayed command is unknown, no point in trying again
73
75
  driver.options[:native_displayed] = false
@@ -14,7 +14,7 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
14
14
  warn 'You are attempting to click a table row which has issues in geckodriver/marionette - '\
15
15
  'see https://github.com/mozilla/geckodriver/issues/1228. Your test should probably be '\
16
16
  'clicking on a table cell like a user would. Clicking the first cell in the row instead.'
17
- return find_css('th:first-child,td:first-child')[0].click(keys, options)
17
+ return find_css('th:first-child,td:first-child')[0].click(keys, **options)
18
18
  end
19
19
  raise
20
20
  end
@@ -26,7 +26,7 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
26
26
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
27
27
  # By default files are appended so we have to clear here if its multiple and already set
28
28
  driver.execute_script(<<~JS, self)
29
- if (arguments[0].multiple && (arguments[0].files.length > 0)){
29
+ if (arguments[0].multiple && arguments[0].files.length){
30
30
  arguments[0].value = null;
31
31
  }
32
32
  JS
@@ -40,11 +40,16 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
40
40
  path_names.each { |path| native.send_keys(path) }
41
41
  end
42
42
 
43
+ def focused?
44
+ driver.evaluate_script('arguments[0] == document.activeElement', self)
45
+ end
46
+
43
47
  def send_keys(*args)
44
48
  # https://github.com/mozilla/geckodriver/issues/846
45
- return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none? { |arg| arg.is_a? Array }
49
+ return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none?(Array)
50
+
51
+ native.click unless focused?
46
52
 
47
- native.click
48
53
  _send_keys(args).perform
49
54
  end
50
55
 
@@ -71,7 +76,7 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
71
76
  return super unless native_displayed?
72
77
 
73
78
  begin
74
- bridge.send(:execute, :is_element_displayed, id: native.ref)
79
+ bridge.send(:execute, :is_element_displayed, id: native_id)
75
80
  rescue Selenium::WebDriver::Error::UnknownCommandError
76
81
  # If the is_element_displayed command is unknown, no point in trying again
77
82
  driver.options[:native_displayed] = false
@@ -85,7 +90,7 @@ private
85
90
  (driver.options[:native_displayed] != false) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
86
91
  end
87
92
 
88
- def click_with_options(click_options)
93
+ def perform_with_options(click_options)
89
94
  # Firefox/marionette has an issue clicking with offset near viewport edge
90
95
  # scroll element to middle just in case
91
96
  scroll_to_center if click_options.coords?
@@ -14,7 +14,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
14
14
  warn 'You are attempting to click a table row which has issues in safaridriver - '\
15
15
  'Your test should probably be clicking on a table cell like a user would. '\
16
16
  'Clicking the first cell in the row instead.'
17
- return find_css('th:first-child,td:first-child')[0].click(keys, options)
17
+ return find_css('th:first-child,td:first-child')[0].click(keys, **options)
18
18
  end
19
19
  raise
20
20
  rescue ::Selenium::WebDriver::Error::WebDriverError => e
@@ -43,7 +43,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
43
43
  return '' unless visible?
44
44
 
45
45
  vis_text = driver.execute_script('return arguments[0].innerText', self)
46
- vis_text.gsub(/\ +/, ' ')
46
+ vis_text.squeeze(' ')
47
47
  .gsub(/[\ \n]*\n[\ \n]*/, "\n")
48
48
  .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
49
49
  .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
@@ -74,7 +74,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
74
74
  if clear == :backspace
75
75
  # Clear field by sending the correct number of backspace keys.
76
76
  backspaces = [:backspace] * self.value.to_s.length
77
- send_keys(*([[:control, 'e']] + backspaces + [value]))
77
+ send_keys([:control, 'e'], *backspaces, value)
78
78
  else
79
79
  super.tap do
80
80
  # React doesn't see the safaridriver element clear
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPauser
4
+ def initialize(mouse, keyboard)
5
+ super
6
+ @devices[:pauser] = Pauser.new
7
+ end
8
+
9
+ def pause(duration)
10
+ @actions << [:pauser, :pause, [duration]]
11
+ self
12
+ end
13
+
14
+ class Pauser
15
+ def pause(duration)
16
+ sleep duration
17
+ end
18
+ end
19
+
20
+ private_constant :Pauser
21
+ end
22
+
23
+ if defined?(::Selenium::WebDriver::VERSION) && (::Selenium::WebDriver::VERSION.to_f < 4) &&
24
+ defined?(::Selenium::WebDriver::ActionBuilder)
25
+ ::Selenium::WebDriver::ActionBuilder.prepend(ActionPauser)
26
+ end
@@ -6,10 +6,10 @@ private
6
6
  def read_atom(function)
7
7
  @atoms ||= Hash.new do |hash, key|
8
8
  hash[key] = begin
9
- File.read(File.expand_path("../../atoms/#{key}.min.js", __FILE__))
10
- rescue Errno::ENOENT
11
- super
12
- end
9
+ File.read(File.expand_path("../../atoms/#{key}.min.js", __FILE__))
10
+ rescue Errno::ENOENT
11
+ super
12
+ end
13
13
  end
14
14
  @atoms[function]
15
15
  end