capybara 3.23.0 → 3.35.3

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