capybara 3.6.0 → 3.7.0

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +16 -0
  3. data/README.md +5 -1
  4. data/lib/capybara.rb +2 -0
  5. data/lib/capybara/minitest/spec.rb +1 -1
  6. data/lib/capybara/node/actions.rb +34 -25
  7. data/lib/capybara/node/base.rb +15 -17
  8. data/lib/capybara/node/document_matchers.rb +1 -3
  9. data/lib/capybara/node/element.rb +11 -12
  10. data/lib/capybara/node/finders.rb +2 -1
  11. data/lib/capybara/node/simple.rb +13 -6
  12. data/lib/capybara/queries/base_query.rb +4 -4
  13. data/lib/capybara/queries/selector_query.rb +119 -94
  14. data/lib/capybara/queries/text_query.rb +2 -1
  15. data/lib/capybara/rack_test/form.rb +4 -4
  16. data/lib/capybara/rack_test/node.rb +5 -5
  17. data/lib/capybara/result.rb +23 -32
  18. data/lib/capybara/rspec/compound.rb +1 -1
  19. data/lib/capybara/rspec/matchers.rb +63 -61
  20. data/lib/capybara/selector.rb +28 -10
  21. data/lib/capybara/selector/css.rb +17 -17
  22. data/lib/capybara/selector/filter_set.rb +9 -9
  23. data/lib/capybara/selector/selector.rb +3 -4
  24. data/lib/capybara/selenium/driver.rb +73 -95
  25. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +4 -4
  26. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +9 -0
  27. data/lib/capybara/selenium/node.rb +127 -67
  28. data/lib/capybara/selenium/nodes/chrome_node.rb +3 -3
  29. data/lib/capybara/selenium/nodes/marionette_node.rb +14 -8
  30. data/lib/capybara/server.rb +2 -2
  31. data/lib/capybara/server/animation_disabler.rb +17 -3
  32. data/lib/capybara/server/middleware.rb +8 -4
  33. data/lib/capybara/session.rb +43 -37
  34. data/lib/capybara/session/config.rb +8 -6
  35. data/lib/capybara/spec/session/assert_text_spec.rb +14 -0
  36. data/lib/capybara/spec/session/attach_file_spec.rb +7 -0
  37. data/lib/capybara/spec/session/check_spec.rb +21 -0
  38. data/lib/capybara/spec/session/choose_spec.rb +15 -1
  39. data/lib/capybara/spec/session/fill_in_spec.rb +7 -0
  40. data/lib/capybara/spec/session/find_spec.rb +2 -1
  41. data/lib/capybara/spec/session/has_selector_spec.rb +18 -0
  42. data/lib/capybara/spec/session/has_text_spec.rb +14 -0
  43. data/lib/capybara/spec/session/node_spec.rb +2 -1
  44. data/lib/capybara/spec/session/reset_session_spec.rb +4 -4
  45. data/lib/capybara/spec/session/text_spec.rb +2 -1
  46. data/lib/capybara/spec/session/title_spec.rb +2 -1
  47. data/lib/capybara/spec/session/uncheck_spec.rb +8 -0
  48. data/lib/capybara/spec/session/within_spec.rb +2 -1
  49. data/lib/capybara/spec/spec_helper.rb +1 -32
  50. data/lib/capybara/spec/views/with_js.erb +3 -4
  51. data/lib/capybara/version.rb +1 -1
  52. data/spec/minitest_spec.rb +4 -0
  53. data/spec/minitest_spec_spec.rb +4 -0
  54. data/spec/rack_test_spec.rb +4 -4
  55. data/spec/rspec/shared_spec_matchers.rb +4 -2
  56. data/spec/selector_spec.rb +15 -1
  57. data/spec/selenium_spec_chrome.rb +1 -6
  58. data/spec/selenium_spec_chrome_remote.rb +1 -1
  59. data/spec/selenium_spec_firefox_remote.rb +2 -5
  60. data/spec/selenium_spec_ie.rb +41 -4
  61. data/spec/selenium_spec_marionette.rb +1 -25
  62. data/spec/shared_selenium_session.rb +74 -16
  63. data/spec/spec_helper.rb +41 -0
  64. metadata +2 -2
@@ -7,8 +7,8 @@ module Capybara::Selenium::Driver::ChromeDriver
7
7
  within_given_window(handle) do
8
8
  begin
9
9
  super
10
- rescue NoMethodError => e
11
- raise unless e.message =~ /full_screen_window/
10
+ rescue NoMethodError => err
11
+ raise unless err.message =~ /full_screen_window/
12
12
  bridge = browser.send(:bridge)
13
13
  result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
14
14
  result['value']
@@ -18,8 +18,8 @@ module Capybara::Selenium::Driver::ChromeDriver
18
18
 
19
19
  def resize_window_to(handle, width, height)
20
20
  super
21
- rescue Selenium::WebDriver::Error::UnknownError => e
22
- raise unless e.message =~ /failed to change window state/
21
+ rescue Selenium::WebDriver::Error::UnknownError => err
22
+ raise unless err.message =~ /failed to change window state/
23
23
  # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
24
24
  # and raises unnecessary error. Wait a bit and try again.
25
25
  sleep 0.5
@@ -23,6 +23,15 @@ module Capybara::Selenium::Driver::MarionetteDriver
23
23
  super
24
24
  end
25
25
 
26
+ def refresh
27
+ # Accept any "will repost content" confirmation that occurs
28
+ accept_modal :confirm, wait: 0.1 do
29
+ super
30
+ end
31
+ rescue Capybara::ModalNotFound # rubocop:disable Lint/HandleExceptions
32
+ # No modal was opened - page has refreshed - ignore
33
+ end
34
+
26
35
  private
27
36
 
28
37
  def build_node(native_node)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Selenium specific implementation of the Capybara::Driver::Node API
3
4
  class Capybara::Selenium::Node < Capybara::Driver::Node
4
5
  def visible_text
5
6
  native.text
@@ -22,7 +23,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
22
23
 
23
24
  def value
24
25
  if tag_name == 'select' && multiple?
25
- native.find_elements(:css, 'option:checked').map { |n| n[:value] || n.text }
26
+ native.find_elements(:css, 'option:checked').map { |el| el[:value] || el.text }
26
27
  else
27
28
  native[:value]
28
29
  end
@@ -73,46 +74,38 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
73
74
  end
74
75
 
75
76
  def select_option
76
- native.click unless selected? || disabled?
77
+ click unless selected? || disabled?
77
78
  end
78
79
 
79
80
  def unselect_option
80
81
  raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?
81
- native.click if selected?
82
+ click if selected?
82
83
  end
83
84
 
84
85
  def click(keys = [], **options)
85
- if keys.empty? && !coords?(options)
86
- native.click
87
- else
88
- scroll_if_needed do
89
- action_with_modifiers(keys, options) do |a|
90
- coords?(options) ? a.click : a.click(native)
91
- end
92
- end
93
- end
94
- rescue StandardError => e
95
- if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
96
- e.message =~ /Other element would receive the click/
86
+ click_options = ClickOptions.new(keys, options)
87
+ return native.click if click_options.empty?
88
+ click_with_options(click_options)
89
+ rescue StandardError => err
90
+ if err.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
91
+ err.message =~ /Other element would receive the click/
97
92
  scroll_to_center
98
93
  end
99
94
 
100
- raise e
95
+ raise err
101
96
  end
102
97
 
103
98
  def right_click(keys = [], **options)
104
- scroll_if_needed do
105
- action_with_modifiers(keys, options) do |a|
106
- coords?(options) ? a.context_click : a.context_click(native)
107
- end
99
+ click_options = ClickOptions.new(keys, options)
100
+ click_with_options(click_options) do |action|
101
+ click_options.coords? ? action.context_click : action.context_click(native)
108
102
  end
109
103
  end
110
104
 
111
105
  def double_click(keys = [], **options)
112
- scroll_if_needed do
113
- action_with_modifiers(keys, options) do |a|
114
- coords?(options) ? a.double_click : a.double_click(native)
115
- end
106
+ click_options = ClickOptions.new(keys, options)
107
+ click_with_options(click_options) do |action|
108
+ click_options.coords? ? action.double_click : action.double_click(native)
116
109
  end
117
110
  end
118
111
 
@@ -121,11 +114,14 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
121
114
  end
122
115
 
123
116
  def hover
124
- scroll_if_needed { driver.browser.action.move_to(native).perform }
117
+ scroll_if_needed { browser_action.move_to(native).perform }
125
118
  end
126
119
 
127
120
  def drag_to(element)
128
- scroll_if_needed { driver.browser.action.drag_and_drop(native, element.native).perform }
121
+ # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
122
+ # which means Seleniums `drag_and_drop` is now broken - do it manually
123
+ scroll_if_needed { browser_action.click_and_hold(native).perform }
124
+ element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
129
125
  end
130
126
 
131
127
  def tag_name
@@ -149,11 +145,11 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
149
145
  end
150
146
 
151
147
  def find_xpath(locator)
152
- native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
148
+ native.find_elements(:xpath, locator).map { |el| self.class.new(driver, el) }
153
149
  end
154
150
 
155
151
  def find_css(locator)
156
- native.find_elements(:css, locator).map { |n| self.class.new(driver, n) }
152
+ native.find_elements(:css, locator).map { |el| self.class.new(driver, el) }
157
153
  end
158
154
 
159
155
  def ==(other)
@@ -190,12 +186,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
190
186
  '/' + result.reverse.join('/')
191
187
  end
192
188
 
193
- private
189
+ protected
194
190
 
195
- def coords?(options)
196
- options[:x] && options[:y]
191
+ def scroll_if_needed
192
+ yield
193
+ rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
194
+ scroll_to_center
195
+ yield
197
196
  end
198
197
 
198
+ private
199
+
199
200
  def boolean_attr(val)
200
201
  val && (val != 'false')
201
202
  end
@@ -206,30 +207,34 @@ private
206
207
  end
207
208
 
208
209
  def set_text(value, clear: nil, **_unused)
209
- if value.to_s.empty? && clear.nil?
210
+ value = value.to_s
211
+ if value.empty? && clear.nil?
210
212
  native.clear
211
213
  elsif clear == :backspace
212
214
  # Clear field by sending the correct number of backspace keys.
213
215
  backspaces = [:backspace] * self.value.to_s.length
214
- send_keys(*([:end] + backspaces + [value.to_s]))
215
- elsif clear == :none
216
- send_keys(value.to_s)
216
+ send_keys(*([:end] + backspaces + [value]))
217
217
  elsif clear.is_a? Array
218
- send_keys(*clear, value.to_s)
218
+ send_keys(*clear, value)
219
219
  else
220
220
  # Clear field by JavaScript assignment of the value property.
221
221
  # Script can change a readonly element which user input cannot, so
222
222
  # don't execute if readonly.
223
- driver.execute_script "arguments[0].value = ''", self
224
- send_keys(value.to_s)
223
+ driver.execute_script "arguments[0].value = ''", self unless clear == :none
224
+ send_keys(value)
225
225
  end
226
226
  end
227
227
 
228
- def scroll_if_needed
229
- yield
230
- rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
231
- scroll_to_center
232
- yield
228
+ def click_with_options(click_options)
229
+ scroll_if_needed do
230
+ action_with_modifiers(click_options) do |action|
231
+ if block_given?
232
+ yield action
233
+ else
234
+ click_options.coords? ? action.click : action.click(native)
235
+ end
236
+ end
237
+ end
233
238
  end
234
239
 
235
240
  def scroll_to_center
@@ -248,21 +253,24 @@ private
248
253
  end
249
254
 
250
255
  def set_date(value) # rubocop:disable Naming/AccessorMethodName
251
- return set_text(value) if value.is_a?(String) || !value.respond_to?(:to_date)
256
+ value = SettableValue.new(value)
257
+ return set_text(value) unless value.dateable?
252
258
  # TODO: this would be better if locale can be detected and correct keystrokes sent
253
- update_value_js(value.to_date.strftime('%Y-%m-%d'))
259
+ update_value_js(value.to_date_str)
254
260
  end
255
261
 
256
262
  def set_time(value) # rubocop:disable Naming/AccessorMethodName
257
- return set_text(value) if value.is_a?(String) || !value.respond_to?(:to_time)
263
+ value = SettableValue.new(value)
264
+ return set_text(value) unless value.timeable?
258
265
  # TODO: this would be better if locale can be detected and correct keystrokes sent
259
- update_value_js(value.to_time.strftime('%H:%M'))
266
+ update_value_js(value.to_time_str)
260
267
  end
261
268
 
262
269
  def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
263
- return set_text(value) if value.is_a?(String) || !value.respond_to?(:to_time)
270
+ value = SettableValue.new(value)
271
+ return set_text(value) unless value.timeable?
264
272
  # TODO: this would be better if locale can be detected and correct keystrokes sent
265
- update_value_js(value.to_time.strftime('%Y-%m-%dT%H:%M'))
273
+ update_value_js(value.to_datetime_str)
266
274
  end
267
275
 
268
276
  def update_value_js(value)
@@ -301,34 +309,33 @@ private
301
309
  # if we use the faster direct send_keys. For now just send_keys to the element
302
310
  # we've already focused.
303
311
  # native.send_keys(value.to_s)
304
- driver.browser.action.send_keys(value.to_s).perform
312
+ browser_action.send_keys(value.to_s).perform
305
313
  end
306
314
 
307
- def action_with_modifiers(keys, x: nil, y: nil)
308
- actions = driver.browser.action
309
- actions.move_to(native, x, y)
310
- modifiers_down(actions, keys)
315
+ def action_with_modifiers(click_options)
316
+ actions = browser_action.move_to(native, *click_options.coords)
317
+ modifiers_down(actions, click_options.keys)
311
318
  yield actions
312
- modifiers_up(actions, keys)
319
+ modifiers_up(actions, click_options.keys)
313
320
  actions.perform
314
321
  ensure
315
- a = driver.browser.action
316
- a.release_actions if a.respond_to?(:release_actions)
322
+ act = browser_action
323
+ act.release_actions if act.respond_to?(:release_actions)
317
324
  end
318
325
 
319
326
  def modifiers_down(actions, keys)
320
- keys.each do |key|
321
- key = case key
322
- when :ctrl then :control
323
- when :command, :cmd then :meta
324
- else
325
- key
326
- end
327
- actions.key_down(key)
328
- end
327
+ each_key(keys) { |key| actions.key_down(key) }
329
328
  end
330
329
 
331
330
  def modifiers_up(actions, keys)
331
+ each_key(keys) { |key| actions.key_up(key) }
332
+ end
333
+
334
+ def browser_action
335
+ driver.browser.action
336
+ end
337
+
338
+ def each_key(keys)
332
339
  keys.each do |key|
333
340
  key = case key
334
341
  when :ctrl then :control
@@ -336,7 +343,60 @@ private
336
343
  else
337
344
  key
338
345
  end
339
- actions.key_up(key)
346
+ yield key
347
+ end
348
+ end
349
+
350
+ # SettableValue encapsulates time/date field formatting
351
+ class SettableValue
352
+ attr_reader :value
353
+
354
+ def initialize(value)
355
+ @value = value
356
+ end
357
+
358
+ def dateable?
359
+ !value.is_a?(String) && value.respond_to?(:to_date)
360
+ end
361
+
362
+ def to_date_str
363
+ value.to_date.strftime('%Y-%m-%d')
364
+ end
365
+
366
+ def timeable?
367
+ !value.is_a?(String) && value.respond_to?(:to_time)
368
+ end
369
+
370
+ def to_time_str
371
+ value.to_time.strftime('%H:%M')
372
+ end
373
+
374
+ def to_datetime_str
375
+ value.to_time.strftime('%Y-%m-%dT%H:%M')
376
+ end
377
+ end
378
+ private_constant :SettableValue
379
+
380
+ # ClickOptions encapsulates click option logic
381
+ class ClickOptions
382
+ attr_reader :keys, :options
383
+
384
+ def initialize(keys, options)
385
+ @keys = keys
386
+ @options = options
387
+ end
388
+
389
+ def coords?
390
+ options[:x] && options[:y]
391
+ end
392
+
393
+ def coords
394
+ [options[:x], options[:y]]
395
+ end
396
+
397
+ def empty?
398
+ keys.empty? && !coords?
340
399
  end
341
400
  end
401
+ private_constant :ClickOptions
342
402
  end
@@ -3,8 +3,8 @@
3
3
  class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
4
4
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
5
5
  super(value)
6
- rescue ::Selenium::WebDriver::Error::ExpectedError => e
7
- if e.message =~ /File not found : .+\n.+/m
6
+ rescue ::Selenium::WebDriver::Error::ExpectedError => err
7
+ if err.message =~ /File not found : .+\n.+/m
8
8
  raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload"
9
9
  end
10
10
  raise
@@ -13,7 +13,7 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
13
13
  def drag_to(element)
14
14
  return super unless self[:draggable] == 'true'
15
15
 
16
- scroll_if_needed { driver.browser.action.click_and_hold(native).perform }
16
+ scroll_if_needed { browser_action.click_and_hold(native).perform }
17
17
  driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element
18
18
  end
19
19
 
@@ -7,7 +7,7 @@ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node
7
7
  if tag_name == 'tr'
8
8
  warn 'You are attempting to click a table row which has issues in geckodriver/marionette - see https://github.com/mozilla/geckodriver/issues/1228. ' \
9
9
  'Your test should probably be clicking on a table cell like a user would. Clicking the first cell in the row instead.'
10
- return find_css('th:first-child,td:first-child')[0].click
10
+ return find_css('th:first-child,td:first-child')[0].click(keys, options)
11
11
  end
12
12
  raise
13
13
  end
@@ -26,7 +26,8 @@ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node
26
26
  end
27
27
 
28
28
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
29
- native.clear # By default files are appended so we have to clear here
29
+ # By default files are appended so we have to clear here if its multiple and already set
30
+ native.clear if multiple? && driver.evaluate_script('arguments[0].files', self).any?
30
31
  return super if browser_version >= 62.0
31
32
 
32
33
  # Workaround lack of support for multiple upload by uploading one at a time
@@ -44,25 +45,30 @@ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node
44
45
 
45
46
  def send_keys(*args)
46
47
  # https://github.com/mozilla/geckodriver/issues/846
47
- return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none? { |s| s.is_a? Array }
48
+ return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none? { |arg| arg.is_a? Array }
48
49
 
49
50
  native.click
50
- actions = driver.browser.action
51
- args.each do |keys|
51
+ args.each_with_object(browser_action) do |keys, actions|
52
52
  _send_keys(keys, actions)
53
- end
54
- actions.perform
53
+ end.perform
55
54
  end
56
55
 
57
56
  def drag_to(element)
58
57
  return super unless (browser_version >= 62.0) && (self[:draggable] == 'true')
59
58
 
60
- scroll_if_needed { driver.browser.action.click_and_hold(native).perform }
59
+ scroll_if_needed { browser_action.click_and_hold(native).perform }
61
60
  driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element
62
61
  end
63
62
 
64
63
  private
65
64
 
65
+ def click_with_options(click_options)
66
+ # Firefox/marionette has an issue clicking with offset near viewport edge
67
+ # scroll element to middle just in case
68
+ scroll_to_center if click_options.coords?
69
+ super
70
+ end
71
+
66
72
  def _send_keys(keys, actions, down_keys = nil)
67
73
  case keys
68
74
  when String
@@ -31,7 +31,7 @@ module Capybara
31
31
  end
32
32
 
33
33
  def reset_error!
34
- middleware.error = nil
34
+ middleware.clear_error
35
35
  end
36
36
 
37
37
  def error
@@ -49,7 +49,7 @@ module Capybara
49
49
  if res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPRedirection)
50
50
  return res.body == app.object_id.to_s
51
51
  end
52
- rescue SystemCallError
52
+ rescue SystemCallError, Net::ReadTimeout, OpenSSL::SSL::SSLError
53
53
  false
54
54
  end
55
55
 
@@ -3,8 +3,20 @@
3
3
  module Capybara
4
4
  class Server
5
5
  class AnimationDisabler
6
+ def self.selector_for(css_or_bool)
7
+ case css_or_bool
8
+ when String
9
+ css_or_bool
10
+ when true
11
+ '*'
12
+ else
13
+ raise CapybaraError, 'Capybara.disable_animation supports either a String (the css selector to disable) or a boolean'
14
+ end
15
+ end
16
+
6
17
  def initialize(app)
7
18
  @app = app
19
+ @disable_markup = DISABLE_MARKUP_TEMPLATE % AnimationDisabler.selector_for(Capybara.disable_animation)
8
20
  end
9
21
 
10
22
  def call(env)
@@ -20,18 +32,20 @@ module Capybara
20
32
 
21
33
  private
22
34
 
35
+ attr_reader :disable_markup
36
+
23
37
  def html_content?
24
38
  !!(@headers['Content-Type'] =~ /html/)
25
39
  end
26
40
 
27
41
  def insert_disable(html)
28
- html.sub(%r{(</head>)}, DISABLE_MARKUP + '\\1')
42
+ html.sub(%r{(</head>)}, disable_markup + '\\1')
29
43
  end
30
44
 
31
- DISABLE_MARKUP = <<~HTML
45
+ DISABLE_MARKUP_TEMPLATE = <<~HTML
32
46
  <script defer>(typeof jQuery !== 'undefined') && (jQuery.fx.off = true);</script>
33
47
  <style>
34
- * {
48
+ %s {
35
49
  transition: none !important;
36
50
  animation-duration: 0s !important;
37
51
  animation-delay: 0s !important;