capybara 3.23.0 → 3.35.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +264 -11
  3. data/README.md +10 -6
  4. data/lib/capybara.rb +20 -8
  5. data/lib/capybara/config.rb +10 -8
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/driver/base.rb +4 -0
  8. data/lib/capybara/driver/node.rb +4 -0
  9. data/lib/capybara/dsl.rb +10 -2
  10. data/lib/capybara/helpers.rb +28 -2
  11. data/lib/capybara/minitest.rb +232 -144
  12. data/lib/capybara/minitest/spec.rb +156 -97
  13. data/lib/capybara/node/actions.rb +36 -36
  14. data/lib/capybara/node/base.rb +6 -6
  15. data/lib/capybara/node/document.rb +2 -2
  16. data/lib/capybara/node/document_matchers.rb +3 -3
  17. data/lib/capybara/node/element.rb +77 -33
  18. data/lib/capybara/node/finders.rb +24 -17
  19. data/lib/capybara/node/matchers.rb +79 -64
  20. data/lib/capybara/node/simple.rb +11 -4
  21. data/lib/capybara/queries/ancestor_query.rb +6 -10
  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 +259 -23
  25. data/lib/capybara/queries/sibling_query.rb +5 -11
  26. data/lib/capybara/queries/style_query.rb +1 -1
  27. data/lib/capybara/queries/text_query.rb +13 -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 +42 -6
  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 +29 -0
  35. data/lib/capybara/registrations/servers.rb +9 -2
  36. data/lib/capybara/result.rb +39 -19
  37. data/lib/capybara/rspec.rb +2 -0
  38. data/lib/capybara/rspec/matcher_proxies.rb +5 -5
  39. data/lib/capybara/rspec/matchers.rb +97 -74
  40. data/lib/capybara/rspec/matchers/base.rb +19 -6
  41. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  42. data/lib/capybara/rspec/matchers/have_ancestor.rb +5 -7
  43. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  44. data/lib/capybara/rspec/matchers/have_selector.rb +15 -10
  45. data/lib/capybara/rspec/matchers/have_sibling.rb +4 -7
  46. data/lib/capybara/rspec/matchers/have_text.rb +4 -7
  47. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  48. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  49. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  50. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  51. data/lib/capybara/selector.rb +46 -19
  52. data/lib/capybara/selector/builders/css_builder.rb +10 -6
  53. data/lib/capybara/selector/builders/xpath_builder.rb +4 -2
  54. data/lib/capybara/selector/css.rb +1 -1
  55. data/lib/capybara/selector/definition.rb +13 -11
  56. data/lib/capybara/selector/definition/button.rb +32 -15
  57. data/lib/capybara/selector/definition/checkbox.rb +2 -2
  58. data/lib/capybara/selector/definition/css.rb +3 -1
  59. data/lib/capybara/selector/definition/datalist_input.rb +2 -2
  60. data/lib/capybara/selector/definition/datalist_option.rb +1 -1
  61. data/lib/capybara/selector/definition/element.rb +3 -2
  62. data/lib/capybara/selector/definition/field.rb +1 -1
  63. data/lib/capybara/selector/definition/file_field.rb +1 -1
  64. data/lib/capybara/selector/definition/fillable_field.rb +2 -2
  65. data/lib/capybara/selector/definition/label.rb +5 -3
  66. data/lib/capybara/selector/definition/link.rb +8 -0
  67. data/lib/capybara/selector/definition/option.rb +1 -1
  68. data/lib/capybara/selector/definition/radio_button.rb +2 -2
  69. data/lib/capybara/selector/definition/select.rb +33 -14
  70. data/lib/capybara/selector/definition/table.rb +6 -3
  71. data/lib/capybara/selector/definition/table_row.rb +2 -2
  72. data/lib/capybara/selector/filter_set.rb +13 -11
  73. data/lib/capybara/selector/filters/base.rb +6 -1
  74. data/lib/capybara/selector/filters/locator_filter.rb +1 -1
  75. data/lib/capybara/selector/regexp_disassembler.rb +7 -0
  76. data/lib/capybara/selector/selector.rb +13 -3
  77. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  78. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -1
  79. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  80. data/lib/capybara/selenium/atoms/src/isDisplayed.js +10 -10
  81. data/lib/capybara/selenium/driver.rb +86 -24
  82. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +24 -21
  83. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +21 -19
  84. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +17 -1
  85. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +0 -4
  86. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  87. data/lib/capybara/selenium/extensions/find.rb +37 -26
  88. data/lib/capybara/selenium/extensions/html5_drag.rb +55 -11
  89. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  90. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  91. data/lib/capybara/selenium/logger_suppressor.rb +8 -2
  92. data/lib/capybara/selenium/node.rb +160 -40
  93. data/lib/capybara/selenium/nodes/chrome_node.rb +72 -12
  94. data/lib/capybara/selenium/nodes/edge_node.rb +32 -14
  95. data/lib/capybara/selenium/nodes/firefox_node.rb +28 -32
  96. data/lib/capybara/selenium/nodes/safari_node.rb +5 -29
  97. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  98. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  99. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  100. data/lib/capybara/selenium/patches/logs.rb +32 -7
  101. data/lib/capybara/server.rb +19 -3
  102. data/lib/capybara/server/animation_disabler.rb +8 -3
  103. data/lib/capybara/server/checker.rb +1 -1
  104. data/lib/capybara/server/middleware.rb +22 -10
  105. data/lib/capybara/session.rb +66 -40
  106. data/lib/capybara/session/config.rb +11 -3
  107. data/lib/capybara/session/matchers.rb +11 -11
  108. data/lib/capybara/spec/public/offset.js +6 -0
  109. data/lib/capybara/spec/public/test.js +75 -7
  110. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  111. data/lib/capybara/spec/session/all_spec.rb +60 -5
  112. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  113. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  114. data/lib/capybara/spec/session/check_spec.rb +6 -0
  115. data/lib/capybara/spec/session/click_button_spec.rb +16 -0
  116. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  117. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  118. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  119. data/lib/capybara/spec/session/find_spec.rb +55 -0
  120. data/lib/capybara/spec/session/has_ancestor_spec.rb +2 -0
  121. data/lib/capybara/spec/session/has_button_spec.rb +51 -0
  122. data/lib/capybara/spec/session/has_css_spec.rb +26 -4
  123. data/lib/capybara/spec/session/has_current_path_spec.rb +15 -2
  124. data/lib/capybara/spec/session/has_field_spec.rb +34 -0
  125. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  126. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  127. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  128. data/lib/capybara/spec/session/has_text_spec.rb +30 -0
  129. data/lib/capybara/spec/session/html_spec.rb +1 -1
  130. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  131. data/lib/capybara/spec/session/node_spec.rb +394 -9
  132. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  133. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  134. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  135. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -15
  136. data/lib/capybara/spec/session/selectors_spec.rb +16 -3
  137. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  138. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  139. data/lib/capybara/spec/session/window/window_spec.rb +8 -8
  140. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  141. data/lib/capybara/spec/spec_helper.rb +14 -14
  142. data/lib/capybara/spec/test_app.rb +27 -21
  143. data/lib/capybara/spec/views/form.erb +47 -4
  144. data/lib/capybara/spec/views/offset.erb +32 -0
  145. data/lib/capybara/spec/views/spatial.erb +31 -0
  146. data/lib/capybara/spec/views/with_animation.erb +37 -1
  147. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  148. data/lib/capybara/spec/views/with_html.erb +24 -2
  149. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  150. data/lib/capybara/spec/views/with_js.erb +4 -1
  151. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  152. data/lib/capybara/spec/views/with_sortable_js.erb +1 -1
  153. data/lib/capybara/version.rb +1 -1
  154. data/lib/capybara/window.rb +3 -7
  155. data/spec/basic_node_spec.rb +15 -14
  156. data/spec/capybara_spec.rb +28 -28
  157. data/spec/dsl_spec.rb +16 -3
  158. data/spec/filter_set_spec.rb +5 -5
  159. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  160. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  161. data/spec/minitest_spec.rb +3 -2
  162. data/spec/minitest_spec_spec.rb +46 -46
  163. data/spec/rack_test_spec.rb +38 -15
  164. data/spec/regexp_dissassembler_spec.rb +52 -38
  165. data/spec/result_spec.rb +43 -32
  166. data/spec/rspec/features_spec.rb +4 -1
  167. data/spec/rspec/scenarios_spec.rb +4 -0
  168. data/spec/rspec/shared_spec_matchers.rb +68 -56
  169. data/spec/rspec_spec.rb +9 -5
  170. data/spec/selector_spec.rb +32 -17
  171. data/spec/selenium_spec_chrome.rb +78 -11
  172. data/spec/selenium_spec_chrome_remote.rb +23 -6
  173. data/spec/selenium_spec_edge.rb +15 -12
  174. data/spec/selenium_spec_firefox.rb +24 -19
  175. data/spec/selenium_spec_firefox_remote.rb +0 -8
  176. data/spec/selenium_spec_ie.rb +1 -6
  177. data/spec/server_spec.rb +106 -44
  178. data/spec/session_spec.rb +5 -5
  179. data/spec/shared_selenium_node.rb +56 -2
  180. data/spec/shared_selenium_session.rb +122 -15
  181. data/spec/spec_helper.rb +2 -2
  182. metadata +63 -17
  183. data/lib/capybara/spec/session/source_spec.rb +0 -0
@@ -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
@@ -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(*_)
@@ -179,6 +214,10 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
179
214
  driver.frame_obscured_at?(x: res['x'], y: res['y'])
180
215
  end
181
216
 
217
+ def rect
218
+ native.rect
219
+ end
220
+
182
221
  protected
183
222
 
184
223
  def scroll_if_needed
@@ -188,6 +227,21 @@ protected
188
227
  yield
189
228
  end
190
229
 
230
+ def scroll_to_center
231
+ script = <<-'JS'
232
+ try {
233
+ arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
234
+ } catch(e) {
235
+ arguments[0].scrollIntoView(true);
236
+ }
237
+ JS
238
+ begin
239
+ driver.execute_script(script, self)
240
+ rescue StandardError
241
+ # Swallow error if scrollIntoView with options isn't supported
242
+ end
243
+ end
244
+
191
245
  private
192
246
 
193
247
  def sibling_index(parent, node, selector)
@@ -213,7 +267,7 @@ private
213
267
  find_xpath(XPath.ancestor(:select)[1]).first
214
268
  end
215
269
 
216
- def set_text(value, clear: nil, **_unused)
270
+ def set_text(value, clear: nil, rapid: nil, **_unused)
217
271
  value = value.to_s
218
272
  if value.empty? && clear.nil?
219
273
  native.clear
@@ -224,15 +278,24 @@ private
224
278
  elsif clear.is_a? Array
225
279
  send_keys(*clear, value)
226
280
  else
227
- # Clear field by JavaScript assignment of the value property.
228
- # Script can change a readonly element which user input cannot, so
229
- # don't execute if readonly.
230
- driver.execute_script "if (!arguments[0].readOnly){ arguments[0].value = '' }", self unless clear == :none
231
- send_keys(value)
281
+ driver.execute_script 'arguments[0].select()', self unless clear == :none
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
232
289
  end
233
290
  end
234
291
 
235
- 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
+
236
299
  scroll_if_needed do
237
300
  action_with_modifiers(click_options) do |action|
238
301
  if block_given?
@@ -244,21 +307,6 @@ private
244
307
  end
245
308
  end
246
309
 
247
- def scroll_to_center
248
- script = <<-'JS'
249
- try {
250
- arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
251
- } catch(e) {
252
- arguments[0].scrollIntoView(true);
253
- }
254
- JS
255
- begin
256
- driver.execute_script(script, self)
257
- rescue StandardError # rubocop:disable Lint/HandleExceptions
258
- # Swallow error if scrollIntoView with options isn't supported
259
- end
260
- end
261
-
262
310
  def set_date(value) # rubocop:disable Naming/AccessorMethodName
263
311
  value = SettableValue.new(value)
264
312
  return set_text(value) unless value.dateable?
@@ -287,6 +335,10 @@ private
287
335
  update_value_js(value)
288
336
  end
289
337
 
338
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
339
+ update_value_js(value)
340
+ end
341
+
290
342
  def update_value_js(value)
291
343
  driver.execute_script(<<-JS, self, value)
292
344
  if (arguments[0].readOnly) { return };
@@ -302,8 +354,31 @@ private
302
354
  end
303
355
 
304
356
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
305
- path_names = value.to_s.empty? ? [] : value
306
- native.send_keys(Array(path_names).join("\n"))
357
+ with_file_detector do
358
+ path_names = value.to_s.empty? ? [] : value
359
+ file_names = Array(path_names).map do |pn|
360
+ Pathname.new(pn).absolute? ? pn : File.expand_path(pn)
361
+ end.join("\n")
362
+ native.send_keys(file_names)
363
+ end
364
+ end
365
+
366
+ def with_file_detector
367
+ if driver.options[:browser] == :remote &&
368
+ bridge.respond_to?(:file_detector) &&
369
+ bridge.file_detector.nil?
370
+ begin
371
+ bridge.file_detector = lambda do |(fn, *)|
372
+ str = fn.to_s
373
+ str if File.exist?(str)
374
+ end
375
+ yield
376
+ ensure
377
+ bridge.file_detector = nil
378
+ end
379
+ else
380
+ yield
381
+ end
307
382
  end
308
383
 
309
384
  def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
@@ -331,7 +406,13 @@ private
331
406
  end
332
407
 
333
408
  def action_with_modifiers(click_options)
334
- actions = browser_action.move_to(native, *click_options.coords)
409
+ actions = browser_action.tap do |acts|
410
+ if click_options.center_offset? && click_options.coords?
411
+ acts.move_to(native).move_by(*click_options.coords)
412
+ else
413
+ acts.move_to(native, *click_options.coords)
414
+ end
415
+ end
335
416
  modifiers_down(actions, click_options.keys)
336
417
  yield actions
337
418
  modifiers_up(actions, click_options.keys)
@@ -343,32 +424,50 @@ private
343
424
 
344
425
  def modifiers_down(actions, keys)
345
426
  each_key(keys) { |key| actions.key_down(key) }
427
+ actions
346
428
  end
347
429
 
348
430
  def modifiers_up(actions, keys)
349
431
  each_key(keys) { |key| actions.key_up(key) }
432
+ actions
350
433
  end
351
434
 
352
435
  def browser
353
436
  driver.browser
354
437
  end
355
438
 
439
+ def bridge
440
+ browser.send(:bridge)
441
+ end
442
+
356
443
  def browser_action
357
444
  browser.action
358
445
  end
359
446
 
360
- def each_key(keys)
361
- keys.each do |key|
362
- 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
363
459
  when :ctrl then :control
364
460
  when :command, :cmd then :meta
365
461
  else
366
462
  key
367
463
  end
368
- yield key
369
464
  end
370
465
  end
371
466
 
467
+ def each_key(keys, &block)
468
+ normalize_keys(keys).each(&block)
469
+ end
470
+
372
471
  def find_context
373
472
  native
374
473
  end
@@ -394,6 +493,9 @@ private
394
493
  var xpath = '';
395
494
  var pos, tempitem2;
396
495
 
496
+ if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
497
+ return "(: Shadow DOM element - no XPath :)";
498
+ };
397
499
  while(el !== xml.documentElement) {
398
500
  pos = 0;
399
501
  tempitem2 = el;
@@ -435,6 +537,16 @@ private
435
537
  })(arguments[0], arguments[1], arguments[2])
436
538
  JS
437
539
 
540
+ RAPID_APPEND_TEXT = <<~'JS'
541
+ (function(el, value) {
542
+ value = el.value + value;
543
+ if (el.maxLength && el.maxLength != -1){
544
+ value = value.slice(0, el.maxLength);
545
+ }
546
+ el.value = value;
547
+ })(arguments[0], arguments[1])
548
+ JS
549
+
438
550
  # SettableValue encapsulates time/date field formatting
439
551
  class SettableValue
440
552
  attr_reader :value
@@ -486,8 +598,16 @@ private
486
598
  [options[:x], options[:y]]
487
599
  end
488
600
 
601
+ def center_offset?
602
+ options[:offset] == :center
603
+ end
604
+
489
605
  def empty?
490
- keys.empty? && !coords?
606
+ keys.empty? && !coords? && delay.zero?
607
+ end
608
+
609
+ def delay
610
+ options[:delay] || 0
491
611
  end
492
612
  end
493
613
  private_constant :ClickOptions
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/selenium/extensions/html5_drag'
4
+ require 'capybara/selenium/extensions/file_input_click_emulation'
4
5
 
5
6
  class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
6
7
  include Html5Drag
8
+ include FileInputClickEmulation
7
9
 
8
10
  def set_text(value, clear: nil, **_unused)
9
11
  super.tap do
@@ -16,14 +18,16 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
16
18
  # In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set
17
19
  if browser_version >= 75.0
18
20
  driver.execute_script(<<~JS, self)
19
- if (arguments[0].multiple && (arguments[0].files.length > 0)){
21
+ if (arguments[0].multiple && arguments[0].files.length){
20
22
  arguments[0].value = null;
21
23
  }
22
24
  JS
23
25
  end
24
26
  super
25
27
  rescue *file_errors => e
26
- 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
27
31
 
28
32
  raise
29
33
  end
@@ -32,11 +36,15 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
32
36
  html5_drop(*args)
33
37
  end
34
38
 
35
- def click(*)
39
+ def click(*, **)
36
40
  super
41
+ rescue ::Selenium::WebDriver::Error::ElementClickInterceptedError
42
+ raise
37
43
  rescue ::Selenium::WebDriver::Error::WebDriverError => e
38
44
  # chromedriver 74 (at least on mac) raises the wrong error for this
39
- 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
40
48
 
41
49
  raise
42
50
  end
@@ -53,25 +61,77 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
53
61
  click unless selected_or_disabled
54
62
  end
55
63
 
64
+ def visible?
65
+ return super unless native_displayed?
66
+
67
+ begin
68
+ bridge.send(:execute, :is_element_displayed, id: native.ref)
69
+ rescue Selenium::WebDriver::Error::UnknownCommandError
70
+ # If the is_element_displayed command is unknown, no point in trying again
71
+ driver.options[:native_displayed] = false
72
+ super
73
+ end
74
+ end
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
+
56
94
  private
57
95
 
96
+ def perform_legacy_drag(element, drop_modifiers)
97
+ return super if chromedriver_fixed_actions_key_state? || !w3c? || element.obscured?
98
+
99
+ raise ArgumentError, 'Modifier keys are not supported while dragging in this version of Chrome.' unless drop_modifiers.empty?
100
+
101
+ # W3C Chrome/chromedriver < 77 doesn't maintain mouse button state across actions API performs
102
+ # https://bugs.chromium.org/p/chromedriver/issues/detail?id=2981
103
+ browser_action.release.perform
104
+ browser_action.click_and_hold(native).move_to(element.native).release.perform
105
+ end
106
+
58
107
  def file_errors
59
108
  @file_errors = ::Selenium::WebDriver.logger.suppress_deprecations do
60
109
  [::Selenium::WebDriver::Error::ExpectedError]
61
110
  end
62
111
  end
63
112
 
64
- def bridge
65
- driver.browser.send(:bridge)
113
+ def browser_version(to_float: true)
114
+ caps = capabilities
115
+ ver = (caps[:browser_version] || caps[:version])
116
+ ver = ver.to_f if to_float
117
+ ver
118
+ end
119
+
120
+ def chromedriver_fixed_actions_key_state?
121
+ Gem::Requirement.new('>= 76.0.3809.68').satisfied_by?(chromedriver_version)
122
+ end
123
+
124
+ def chromedriver_supports_displayed_endpoint?
125
+ Gem::Requirement.new('>= 76.0.3809.25').satisfied_by?(chromedriver_version)
66
126
  end
67
127
 
68
- def w3c?
69
- (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
70
- driver.browser.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
128
+ def chromedriver_version
129
+ Gem::Version.new(capabilities['chrome']['chromedriverVersion'].split(' ')[0]) # rubocop:disable Style/RedundantArgument
71
130
  end
72
131
 
73
- def browser_version
74
- caps = driver.browser.capabilities
75
- (caps[:browser_version] || caps[:version]).to_f
132
+ def native_displayed?
133
+ (driver.options[:native_displayed] != false) &&
134
+ (w3c? && chromedriver_supports_displayed_endpoint?) &&
135
+ (!ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS'])
76
136
  end
77
137
  end