capybara 3.6.0 → 3.7.0

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