capybara 3.16.1 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +321 -0
  4. data/README.md +51 -60
  5. data/lib/capybara.rb +71 -114
  6. data/lib/capybara/config.rb +8 -5
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/node.rb +15 -3
  9. data/lib/capybara/dsl.rb +10 -2
  10. data/lib/capybara/helpers.rb +5 -3
  11. data/lib/capybara/minitest.rb +242 -141
  12. data/lib/capybara/minitest/spec.rb +159 -90
  13. data/lib/capybara/node/actions.rb +85 -74
  14. data/lib/capybara/node/base.rb +4 -4
  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 +216 -117
  18. data/lib/capybara/node/finders.rb +65 -65
  19. data/lib/capybara/node/matchers.rb +228 -126
  20. data/lib/capybara/node/simple.rb +9 -4
  21. data/lib/capybara/queries/ancestor_query.rb +5 -7
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +1 -1
  24. data/lib/capybara/queries/selector_query.rb +296 -30
  25. data/lib/capybara/queries/sibling_query.rb +5 -4
  26. data/lib/capybara/queries/style_query.rb +2 -2
  27. data/lib/capybara/queries/text_query.rb +13 -1
  28. data/lib/capybara/queries/title_query.rb +1 -1
  29. data/lib/capybara/rack_test/browser.rb +7 -2
  30. data/lib/capybara/rack_test/driver.rb +1 -1
  31. data/lib/capybara/rack_test/form.rb +1 -1
  32. data/lib/capybara/rack_test/node.rb +43 -7
  33. data/lib/capybara/registration_container.rb +44 -0
  34. data/lib/capybara/registrations/drivers.rb +36 -0
  35. data/lib/capybara/registrations/patches/puma_ssl.rb +27 -0
  36. data/lib/capybara/registrations/servers.rb +44 -0
  37. data/lib/capybara/result.rb +36 -8
  38. data/lib/capybara/rspec/matcher_proxies.rb +6 -4
  39. data/lib/capybara/rspec/matchers.rb +100 -63
  40. data/lib/capybara/rspec/matchers/base.rb +23 -10
  41. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  42. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  43. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  44. data/lib/capybara/rspec/matchers/have_selector.rb +16 -8
  45. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  46. data/lib/capybara/rspec/matchers/have_text.rb +4 -4
  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 +2 -2
  50. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  51. data/lib/capybara/selector.rb +219 -588
  52. data/lib/capybara/selector/builders/css_builder.rb +10 -6
  53. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  54. data/lib/capybara/selector/css.rb +4 -2
  55. data/lib/capybara/selector/definition.rb +277 -0
  56. data/lib/capybara/selector/definition/button.rb +52 -0
  57. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  58. data/lib/capybara/selector/definition/css.rb +10 -0
  59. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  60. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  61. data/lib/capybara/selector/definition/element.rb +27 -0
  62. data/lib/capybara/selector/definition/field.rb +40 -0
  63. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  64. data/lib/capybara/selector/definition/file_field.rb +13 -0
  65. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  66. data/lib/capybara/selector/definition/frame.rb +17 -0
  67. data/lib/capybara/selector/definition/id.rb +6 -0
  68. data/lib/capybara/selector/definition/label.rb +62 -0
  69. data/lib/capybara/selector/definition/link.rb +54 -0
  70. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  71. data/lib/capybara/selector/definition/option.rb +27 -0
  72. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  73. data/lib/capybara/selector/definition/select.rb +81 -0
  74. data/lib/capybara/selector/definition/table.rb +109 -0
  75. data/lib/capybara/selector/definition/table_row.rb +21 -0
  76. data/lib/capybara/selector/definition/xpath.rb +5 -0
  77. data/lib/capybara/selector/filter_set.rb +13 -9
  78. data/lib/capybara/selector/filters/base.rb +11 -2
  79. data/lib/capybara/selector/filters/locator_filter.rb +13 -3
  80. data/lib/capybara/selector/regexp_disassembler.rb +9 -2
  81. data/lib/capybara/selector/selector.rb +43 -448
  82. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  83. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  84. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  85. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  86. data/lib/capybara/selenium/driver.rb +125 -56
  87. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +73 -17
  88. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  89. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +41 -2
  90. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +14 -1
  91. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +14 -5
  92. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  93. data/lib/capybara/selenium/extensions/find.rb +67 -45
  94. data/lib/capybara/selenium/extensions/html5_drag.rb +152 -36
  95. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  96. data/lib/capybara/selenium/logger_suppressor.rb +34 -0
  97. data/lib/capybara/selenium/node.rb +227 -56
  98. data/lib/capybara/selenium/nodes/chrome_node.rb +93 -8
  99. data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
  100. data/lib/capybara/selenium/nodes/firefox_node.rb +37 -59
  101. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  102. data/lib/capybara/selenium/nodes/safari_node.rb +27 -54
  103. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  104. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  105. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  106. data/lib/capybara/selenium/patches/logs.rb +45 -0
  107. data/lib/capybara/server.rb +19 -3
  108. data/lib/capybara/server/animation_disabler.rb +2 -2
  109. data/lib/capybara/server/checker.rb +6 -2
  110. data/lib/capybara/server/middleware.rb +23 -13
  111. data/lib/capybara/session.rb +124 -106
  112. data/lib/capybara/session/config.rb +12 -10
  113. data/lib/capybara/session/matchers.rb +6 -6
  114. data/lib/capybara/spec/public/offset.js +6 -0
  115. data/lib/capybara/spec/public/test.js +94 -5
  116. data/lib/capybara/spec/session/all_spec.rb +84 -6
  117. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  118. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  119. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  120. data/lib/capybara/spec/session/attach_file_spec.rb +14 -6
  121. data/lib/capybara/spec/session/check_spec.rb +10 -4
  122. data/lib/capybara/spec/session/choose_spec.rb +8 -2
  123. data/lib/capybara/spec/session/click_button_spec.rb +44 -1
  124. data/lib/capybara/spec/session/click_link_spec.rb +11 -0
  125. data/lib/capybara/spec/session/evaluate_script_spec.rb +12 -0
  126. data/lib/capybara/spec/session/fill_in_spec.rb +37 -2
  127. data/lib/capybara/spec/session/find_spec.rb +60 -6
  128. data/lib/capybara/spec/session/first_spec.rb +1 -1
  129. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  130. data/lib/capybara/spec/session/frame/within_frame_spec.rb +12 -1
  131. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  132. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  133. data/lib/capybara/spec/session/has_css_spec.rb +35 -6
  134. data/lib/capybara/spec/session/has_current_path_spec.rb +6 -4
  135. data/lib/capybara/spec/session/has_field_spec.rb +34 -0
  136. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  137. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  138. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  139. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  140. data/lib/capybara/spec/session/has_text_spec.rb +47 -0
  141. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  142. data/lib/capybara/spec/session/node_spec.rb +574 -16
  143. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  144. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  145. data/lib/capybara/spec/session/scroll_spec.rb +1 -1
  146. data/lib/capybara/spec/session/select_spec.rb +5 -10
  147. data/lib/capybara/spec/session/selectors_spec.rb +24 -3
  148. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  149. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  150. data/lib/capybara/spec/session/window/window_spec.rb +10 -9
  151. data/lib/capybara/spec/spec_helper.rb +7 -2
  152. data/lib/capybara/spec/test_app.rb +26 -21
  153. data/lib/capybara/spec/views/animated.erb +49 -0
  154. data/lib/capybara/spec/views/form.erb +25 -4
  155. data/lib/capybara/spec/views/frame_child.erb +2 -1
  156. data/lib/capybara/spec/views/frame_one.erb +1 -0
  157. data/lib/capybara/spec/views/obscured.erb +9 -9
  158. data/lib/capybara/spec/views/offset.erb +32 -0
  159. data/lib/capybara/spec/views/react.erb +45 -0
  160. data/lib/capybara/spec/views/spatial.erb +31 -0
  161. data/lib/capybara/spec/views/with_animation.erb +29 -1
  162. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  163. data/lib/capybara/spec/views/with_html.erb +28 -2
  164. data/lib/capybara/spec/views/with_js.erb +2 -1
  165. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  166. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  167. data/lib/capybara/version.rb +1 -1
  168. data/lib/capybara/window.rb +10 -10
  169. data/spec/basic_node_spec.rb +6 -6
  170. data/spec/capybara_spec.rb +28 -28
  171. data/spec/dsl_spec.rb +16 -3
  172. data/spec/filter_set_spec.rb +5 -5
  173. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  174. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  175. data/spec/minitest_spec.rb +12 -2
  176. data/spec/minitest_spec_spec.rb +56 -45
  177. data/spec/rack_test_spec.rb +25 -12
  178. data/spec/regexp_dissassembler_spec.rb +53 -39
  179. data/spec/result_spec.rb +50 -54
  180. data/spec/rspec/features_spec.rb +1 -0
  181. data/spec/rspec/shared_spec_matchers.rb +78 -62
  182. data/spec/rspec_spec.rb +5 -5
  183. data/spec/sauce_spec_chrome.rb +1 -0
  184. data/spec/selector_spec.rb +26 -16
  185. data/spec/selenium_spec_chrome.rb +84 -5
  186. data/spec/selenium_spec_chrome_remote.rb +23 -8
  187. data/spec/selenium_spec_edge.rb +23 -8
  188. data/spec/selenium_spec_firefox.rb +16 -21
  189. data/spec/selenium_spec_firefox_remote.rb +4 -13
  190. data/spec/selenium_spec_ie.rb +23 -15
  191. data/spec/selenium_spec_safari.rb +17 -17
  192. data/spec/server_spec.rb +87 -42
  193. data/spec/session_spec.rb +11 -4
  194. data/spec/shared_selenium_node.rb +83 -0
  195. data/spec/shared_selenium_session.rb +62 -72
  196. data/spec/spec_helper.rb +43 -5
  197. metadata +114 -16
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Capybara::Selenium::Node
4
+ #
5
+ # @api private
6
+ #
7
+ class ModifierKeysStack
8
+ def initialize
9
+ @stack = []
10
+ end
11
+
12
+ def include?(key)
13
+ @stack.flatten.include?(key)
14
+ end
15
+
16
+ def press(key)
17
+ @stack.last.push(key)
18
+ end
19
+
20
+ def push
21
+ @stack.push []
22
+ end
23
+
24
+ def pop
25
+ @stack.pop
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Selenium
5
+ module DeprecationSuppressor
6
+ def initialize(*)
7
+ @suppress_for_capybara = false
8
+ super
9
+ end
10
+
11
+ def deprecate(*)
12
+ super unless @suppress_for_capybara
13
+ end
14
+
15
+ def suppress_deprecations
16
+ prev_suppress_for_capybara, @suppress_for_capybara = @suppress_for_capybara, true
17
+ yield
18
+ ensure
19
+ @suppress_for_capybara = prev_suppress_for_capybara
20
+ end
21
+ end
22
+
23
+ module ErrorSuppressor
24
+ def for_code(*)
25
+ ::Selenium::WebDriver.logger.suppress_deprecations do
26
+ super
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ Selenium::WebDriver::Logger.prepend Capybara::Selenium::DeprecationSuppressor
34
+ Selenium::WebDriver::Error.singleton_class.prepend Capybara::Selenium::ErrorSuppressor
@@ -54,11 +54,16 @@ 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
60
+
61
+ tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
62
+ @tag_name ||= tag_name
58
63
 
59
64
  case tag_name
60
65
  when 'input'
61
- case self[:type]
66
+ case type
62
67
  when 'radio'
63
68
  click
64
69
  when 'checkbox'
@@ -71,13 +76,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
71
76
  set_time(value)
72
77
  when 'datetime-local'
73
78
  set_datetime_local(value)
79
+ when 'color'
80
+ set_color(value)
81
+ when 'range'
82
+ set_range(value)
74
83
  else
75
- set_text(value, options)
84
+ set_text(value, **options)
76
85
  end
77
86
  when 'textarea'
78
- set_text(value, options)
87
+ set_text(value, **options)
79
88
  else
80
- set_content_editable(value) if content_editable?
89
+ set_content_editable(value)
81
90
  end
82
91
  end
83
92
 
@@ -95,26 +104,51 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
95
104
  click_options = ClickOptions.new(keys, options)
96
105
  return native.click if click_options.empty?
97
106
 
98
- click_with_options(click_options)
99
- rescue StandardError => err
100
- if err.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
101
- err.message =~ /Other element would receive the click/
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
121
+ rescue StandardError => e
122
+ if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
123
+ e.message.match?(/Other element would receive the click/)
102
124
  scroll_to_center
103
125
  end
104
126
 
105
- raise err
127
+ raise e
106
128
  end
107
129
 
108
130
  def right_click(keys = [], **options)
109
131
  click_options = ClickOptions.new(keys, options)
110
- click_with_options(click_options) do |action|
111
- 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
112
144
  end
113
145
  end
114
146
 
115
147
  def double_click(keys = [], **options)
116
148
  click_options = ClickOptions.new(keys, options)
117
- 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|
118
152
  click_options.coords? ? action.double_click : action.double_click(native)
119
153
  end
120
154
  end
@@ -127,15 +161,25 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
127
161
  scroll_if_needed { browser_action.move_to(native).perform }
128
162
  end
129
163
 
130
- def drag_to(element)
164
+ def drag_to(element, drop_modifiers: [], **)
165
+ drop_modifiers = Array(drop_modifiers)
131
166
  # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
132
167
  # which means Seleniums `drag_and_drop` is now broken - do it manually
133
168
  scroll_if_needed { browser_action.click_and_hold(native).perform }
134
- 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
175
+ end
176
+
177
+ def drop(*_)
178
+ raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
135
179
  end
136
180
 
137
181
  def tag_name
138
- native.tag_name.downcase
182
+ @tag_name ||= native.tag_name.downcase
139
183
  end
140
184
 
141
185
  def visible?; boolean_attr(native.displayed?); end
@@ -148,11 +192,11 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
148
192
  return true unless native.enabled?
149
193
 
150
194
  # WebDriver only defines `disabled?` for form controls but fieldset makes sense too
151
- tag_name == 'fieldset' && find_xpath('ancestor-or-self::fieldset[@disabled]').any?
195
+ find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
152
196
  end
153
197
 
154
198
  def content_editable?
155
- native.attribute('isContentEditable')
199
+ native.attribute('isContentEditable') == 'true'
156
200
  end
157
201
 
158
202
  def ==(other)
@@ -163,6 +207,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
163
207
  driver.evaluate_script GET_XPATH_SCRIPT, self
164
208
  end
165
209
 
210
+ def obscured?(x: nil, y: nil)
211
+ res = driver.evaluate_script(OBSCURED_OR_OFFSET_SCRIPT, self, x, y)
212
+ return true if res == true
213
+
214
+ driver.frame_obscured_at?(x: res['x'], y: res['y'])
215
+ end
216
+
217
+ def rect
218
+ native.rect
219
+ end
220
+
166
221
  protected
167
222
 
168
223
  def scroll_if_needed
@@ -172,6 +227,21 @@ protected
172
227
  yield
173
228
  end
174
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
+
175
245
  private
176
246
 
177
247
  def sibling_index(parent, node, selector)
@@ -197,7 +267,7 @@ private
197
267
  find_xpath(XPath.ancestor(:select)[1]).first
198
268
  end
199
269
 
200
- def set_text(value, clear: nil, **_unused)
270
+ def set_text(value, clear: nil, rapid: nil, **_unused)
201
271
  value = value.to_s
202
272
  if value.empty? && clear.nil?
203
273
  native.clear
@@ -208,15 +278,24 @@ private
208
278
  elsif clear.is_a? Array
209
279
  send_keys(*clear, value)
210
280
  else
211
- # Clear field by JavaScript assignment of the value property.
212
- # Script can change a readonly element which user input cannot, so
213
- # don't execute if readonly.
214
- driver.execute_script "arguments[0].value = ''", self unless clear == :none
215
- 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
216
289
  end
217
290
  end
218
291
 
219
- 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
+
220
299
  scroll_if_needed do
221
300
  action_with_modifiers(click_options) do |action|
222
301
  if block_given?
@@ -228,21 +307,6 @@ private
228
307
  end
229
308
  end
230
309
 
231
- def scroll_to_center
232
- script = <<-'JS'
233
- try {
234
- arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
235
- } catch(e) {
236
- arguments[0].scrollIntoView(true);
237
- }
238
- JS
239
- begin
240
- driver.execute_script(script, self)
241
- rescue StandardError # rubocop:disable Lint/HandleExceptions
242
- # Swallow error if scrollIntoView with options isn't supported
243
- end
244
- end
245
-
246
310
  def set_date(value) # rubocop:disable Naming/AccessorMethodName
247
311
  value = SettableValue.new(value)
248
312
  return set_text(value) unless value.dateable?
@@ -267,8 +331,17 @@ private
267
331
  update_value_js(value.to_datetime_str)
268
332
  end
269
333
 
334
+ def set_color(value) # rubocop:disable Naming/AccessorMethodName
335
+ update_value_js(value)
336
+ end
337
+
338
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
339
+ update_value_js(value)
340
+ end
341
+
270
342
  def update_value_js(value)
271
343
  driver.execute_script(<<-JS, self, value)
344
+ if (arguments[0].readOnly) { return };
272
345
  if (document.activeElement !== arguments[0]){
273
346
  arguments[0].focus();
274
347
  }
@@ -281,32 +354,65 @@ private
281
354
  end
282
355
 
283
356
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
284
- path_names = value.to_s.empty? ? [] : value
285
- 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
286
382
  end
287
383
 
288
384
  def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
289
385
  # Ensure we are focused on the element
290
386
  click
291
387
 
292
- driver.execute_script <<-JS, self
293
- var range = document.createRange();
294
- var sel = window.getSelection();
295
- arguments[0].focus();
296
- range.selectNodeContents(arguments[0]);
297
- sel.removeAllRanges();
298
- sel.addRange(range);
388
+ editable = driver.execute_script <<-JS, self
389
+ if (arguments[0].isContentEditable) {
390
+ var range = document.createRange();
391
+ var sel = window.getSelection();
392
+ arguments[0].focus();
393
+ range.selectNodeContents(arguments[0]);
394
+ sel.removeAllRanges();
395
+ sel.addRange(range);
396
+ return true;
397
+ }
398
+ return false;
299
399
  JS
300
400
 
301
401
  # The action api has a speed problem but both chrome and firefox 58 raise errors
302
402
  # if we use the faster direct send_keys. For now just send_keys to the element
303
403
  # we've already focused.
304
404
  # native.send_keys(value.to_s)
305
- browser_action.send_keys(value.to_s).perform
405
+ browser_action.send_keys(value.to_s).perform if editable
306
406
  end
307
407
 
308
408
  def action_with_modifiers(click_options)
309
- 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
310
416
  modifiers_down(actions, click_options.keys)
311
417
  yield actions
312
418
  modifiers_up(actions, click_options.keys)
@@ -318,32 +424,50 @@ private
318
424
 
319
425
  def modifiers_down(actions, keys)
320
426
  each_key(keys) { |key| actions.key_down(key) }
427
+ actions
321
428
  end
322
429
 
323
430
  def modifiers_up(actions, keys)
324
431
  each_key(keys) { |key| actions.key_up(key) }
432
+ actions
325
433
  end
326
434
 
327
435
  def browser
328
436
  driver.browser
329
437
  end
330
438
 
439
+ def bridge
440
+ browser.send(:bridge)
441
+ end
442
+
331
443
  def browser_action
332
444
  browser.action
333
445
  end
334
446
 
335
- def each_key(keys)
336
- keys.each do |key|
337
- 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
338
459
  when :ctrl then :control
339
460
  when :command, :cmd then :meta
340
461
  else
341
462
  key
342
463
  end
343
- yield key
344
464
  end
345
465
  end
346
466
 
467
+ def each_key(keys)
468
+ normalize_keys(keys).each { |key| yield(key) }
469
+ end
470
+
347
471
  def find_context
348
472
  native
349
473
  end
@@ -352,6 +476,18 @@ private
352
476
  self.class.new(driver, native_node, initial_cache)
353
477
  end
354
478
 
479
+ def attrs(*attr_names)
480
+ return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']
481
+
482
+ driver.evaluate_script <<~'JS', self, attr_names.map(&:to_s)
483
+ (function(el, names){
484
+ return names.map(function(name){
485
+ return el[name]
486
+ });
487
+ })(arguments[0], arguments[1]);
488
+ JS
489
+ end
490
+
355
491
  GET_XPATH_SCRIPT = <<~'JS'
356
492
  (function(el, xml){
357
493
  var xpath = '';
@@ -381,6 +517,33 @@ private
381
517
  })(arguments[0], document)
382
518
  JS
383
519
 
520
+ OBSCURED_OR_OFFSET_SCRIPT = <<~'JS'
521
+ (function(el, x, y) {
522
+ var box = el.getBoundingClientRect();
523
+ if (x == null) x = box.width/2;
524
+ if (y == null) y = box.height/2 ;
525
+
526
+ var px = box.left + x,
527
+ py = box.top + y,
528
+ e = document.elementFromPoint(px, py);
529
+
530
+ if (!el.contains(e))
531
+ return true;
532
+
533
+ return { x: px, y: py };
534
+ })(arguments[0], arguments[1], arguments[2])
535
+ JS
536
+
537
+ RAPID_APPEND_TEXT = <<~'JS'
538
+ (function(el, value) {
539
+ value = el.value + value;
540
+ if (el.maxLength && el.maxLength != -1){
541
+ value = value.slice(0, el.maxLength);
542
+ }
543
+ el.value = value;
544
+ })(arguments[0], arguments[1])
545
+ JS
546
+
384
547
  # SettableValue encapsulates time/date field formatting
385
548
  class SettableValue
386
549
  attr_reader :value
@@ -432,8 +595,16 @@ private
432
595
  [options[:x], options[:y]]
433
596
  end
434
597
 
598
+ def center_offset?
599
+ options[:offset] == :center
600
+ end
601
+
435
602
  def empty?
436
- keys.empty? && !coords?
603
+ keys.empty? && !coords? && delay.zero?
604
+ end
605
+
606
+ def delay
607
+ options[:delay] || 0
437
608
  end
438
609
  end
439
610
  private_constant :ClickOptions