capybara 3.35.3 → 3.36.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +31 -1
  3. data/README.md +5 -1
  4. data/lib/capybara/config.rb +16 -4
  5. data/lib/capybara/driver/base.rb +4 -0
  6. data/lib/capybara/driver/node.rb +1 -1
  7. data/lib/capybara/helpers.rb +2 -11
  8. data/lib/capybara/node/actions.rb +10 -5
  9. data/lib/capybara/node/document.rb +2 -2
  10. data/lib/capybara/node/element.rb +1 -1
  11. data/lib/capybara/node/finders.rb +1 -1
  12. data/lib/capybara/node/simple.rb +5 -1
  13. data/lib/capybara/queries/active_element_query.rb +18 -0
  14. data/lib/capybara/queries/ancestor_query.rb +2 -1
  15. data/lib/capybara/queries/current_path_query.rb +1 -1
  16. data/lib/capybara/queries/selector_query.rb +14 -1
  17. data/lib/capybara/queries/sibling_query.rb +2 -1
  18. data/lib/capybara/rack_test/node.rb +9 -6
  19. data/lib/capybara/registrations/drivers.rb +3 -3
  20. data/lib/capybara/rspec/matcher_proxies.rb +3 -3
  21. data/lib/capybara/rspec/matchers/have_selector.rb +1 -1
  22. data/lib/capybara/selector/builders/css_builder.rb +1 -1
  23. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  24. data/lib/capybara/selector/css.rb +1 -1
  25. data/lib/capybara/selector/definition/button.rb +9 -4
  26. data/lib/capybara/selector/definition/checkbox.rb +1 -1
  27. data/lib/capybara/selector/definition/file_field.rb +1 -1
  28. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  29. data/lib/capybara/selector/definition/radio_button.rb +1 -1
  30. data/lib/capybara/selector/definition.rb +2 -1
  31. data/lib/capybara/selector/filter_set.rb +4 -6
  32. data/lib/capybara/selector.rb +1 -0
  33. data/lib/capybara/selenium/driver.rb +19 -9
  34. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +1 -1
  35. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +1 -1
  36. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +1 -1
  37. data/lib/capybara/selenium/node.rb +10 -8
  38. data/lib/capybara/selenium/nodes/chrome_node.rb +1 -1
  39. data/lib/capybara/selenium/nodes/edge_node.rb +1 -1
  40. data/lib/capybara/selenium/nodes/firefox_node.rb +1 -1
  41. data/lib/capybara/selenium/nodes/safari_node.rb +2 -2
  42. data/lib/capybara/server/animation_disabler.rb +15 -9
  43. data/lib/capybara/session.rb +12 -2
  44. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  45. data/lib/capybara/spec/session/all_spec.rb +4 -6
  46. data/lib/capybara/spec/session/check_spec.rb +9 -0
  47. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  48. data/lib/capybara/spec/session/has_any_selectors_spec.rb +4 -0
  49. data/lib/capybara/spec/session/has_button_spec.rb +24 -0
  50. data/lib/capybara/spec/session/has_field_spec.rb +24 -0
  51. data/lib/capybara/spec/session/has_link_spec.rb +24 -0
  52. data/lib/capybara/spec/session/has_text_spec.rb +1 -1
  53. data/lib/capybara/spec/session/node_spec.rb +1 -1
  54. data/lib/capybara/spec/session/scroll_spec.rb +4 -4
  55. data/lib/capybara/spec/spec_helper.rb +4 -3
  56. data/lib/capybara/spec/test_app.rb +17 -8
  57. data/lib/capybara/spec/views/animated.erb +1 -1
  58. data/lib/capybara/spec/views/form.erb +11 -3
  59. data/lib/capybara/spec/views/frame_child.erb +1 -1
  60. data/lib/capybara/spec/views/frame_one.erb +1 -1
  61. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  62. data/lib/capybara/spec/views/frame_two.erb +1 -1
  63. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  64. data/lib/capybara/spec/views/layout.erb +10 -0
  65. data/lib/capybara/spec/views/obscured.erb +1 -1
  66. data/lib/capybara/spec/views/offset.erb +2 -1
  67. data/lib/capybara/spec/views/path.erb +2 -2
  68. data/lib/capybara/spec/views/popup_one.erb +1 -1
  69. data/lib/capybara/spec/views/popup_two.erb +1 -1
  70. data/lib/capybara/spec/views/react.erb +2 -2
  71. data/lib/capybara/spec/views/scroll.erb +2 -1
  72. data/lib/capybara/spec/views/spatial.erb +1 -1
  73. data/lib/capybara/spec/views/with_animation.erb +2 -3
  74. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  75. data/lib/capybara/spec/views/with_dragula.erb +2 -2
  76. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  77. data/lib/capybara/spec/views/with_hover.erb +2 -2
  78. data/lib/capybara/spec/views/with_html.erb +1 -1
  79. data/lib/capybara/spec/views/with_jquery_animation.erb +1 -1
  80. data/lib/capybara/spec/views/with_js.erb +2 -3
  81. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  82. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  83. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  84. data/lib/capybara/spec/views/with_sortable_js.erb +2 -2
  85. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  86. data/lib/capybara/spec/views/with_windows.erb +1 -1
  87. data/lib/capybara/spec/views/within_frames.erb +1 -1
  88. data/lib/capybara/version.rb +1 -1
  89. data/lib/capybara.rb +18 -22
  90. data/spec/basic_node_spec.rb +16 -3
  91. data/spec/dsl_spec.rb +1 -1
  92. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  93. data/spec/rack_test_spec.rb +14 -10
  94. data/spec/result_spec.rb +5 -6
  95. data/spec/rspec/features_spec.rb +1 -1
  96. data/spec/rspec/shared_spec_matchers.rb +2 -2
  97. data/spec/selenium_spec_chrome.rb +8 -9
  98. data/spec/selenium_spec_chrome_remote.rb +9 -8
  99. data/spec/selenium_spec_firefox.rb +4 -3
  100. data/spec/selenium_spec_firefox_remote.rb +2 -2
  101. data/spec/selenium_spec_ie.rb +3 -6
  102. data/spec/selenium_spec_safari.rb +27 -19
  103. data/spec/shared_selenium_node.rb +0 -4
  104. data/spec/shared_selenium_session.rb +11 -8
  105. metadata +35 -14
  106. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -43,7 +43,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
43
43
  Gem::Version.new(Selenium::WebDriver::VERSION)
44
44
  end
45
45
 
46
- unless Gem::Requirement.new('>= 3.5.0').satisfied_by? @selenium_webdriver_version
46
+ unless Gem::Requirement.new('>= 3.142.7').satisfied_by? @selenium_webdriver_version
47
47
  warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade."
48
48
  end
49
49
 
@@ -148,8 +148,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
148
148
  unwrap_script_result(result)
149
149
  end
150
150
 
151
+ def active_element
152
+ build_node(native_active_element)
153
+ end
154
+
151
155
  def send_keys(*args)
152
- active_element.send_keys(*args)
156
+ # Should this call the specialized nodes rather than native???
157
+ native_active_element.send_keys(*args)
153
158
  end
154
159
 
155
160
  def save_screenshot(path, **_options)
@@ -249,7 +254,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
249
254
  end
250
255
 
251
256
  def open_new_window(kind = :tab)
252
- browser.manage.new_window(kind)
257
+ if browser.switch_to.respond_to?(:new_window)
258
+ handle = current_window_handle
259
+ browser.switch_to.new_window(kind)
260
+ switch_to_window(handle)
261
+ else
262
+ browser.manage.new_window(kind)
263
+ end
253
264
  rescue NoMethodError, Selenium::WebDriver::Error::WebDriverError
254
265
  # If not supported by the driver or browser default to using JS
255
266
  browser.execute_script('window.open();')
@@ -293,7 +304,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
293
304
  end
294
305
 
295
306
  def invalid_element_errors
296
- @invalid_element_errors ||= begin
307
+ @invalid_element_errors ||=
297
308
  [
298
309
  ::Selenium::WebDriver::Error::StaleElementReferenceError,
299
310
  ::Selenium::WebDriver::Error::ElementNotInteractableError,
@@ -313,7 +324,6 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
313
324
  end
314
325
  end
315
326
  end
316
- end
317
327
  end
318
328
 
319
329
  def no_such_window_error
@@ -330,6 +340,10 @@ private
330
340
  args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
331
341
  end
332
342
 
343
+ def native_active_element
344
+ browser.switch_to.active_element
345
+ end
346
+
333
347
  def clear_browser_state
334
348
  delete_all_cookies
335
349
  clear_storage
@@ -475,10 +489,6 @@ private
475
489
  browser
476
490
  end
477
491
 
478
- def active_element
479
- browser.switch_to.active_element
480
- end
481
-
482
492
  def build_node(native_node, initial_cache = {})
483
493
  ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
484
494
  end
@@ -38,7 +38,7 @@ module Capybara::Selenium::Driver::ChromeDriver
38
38
  return unless @browser
39
39
 
40
40
  switch_to_window(window_handles.first)
41
- window_handles.slice(1..-1).each { |win| close_window(win) }
41
+ window_handles.slice(1..).each { |win| close_window(win) }
42
42
  return super if chromedriver_version < 73
43
43
 
44
44
  timer = Capybara::Helpers.timer(expire_in: 10)
@@ -39,7 +39,7 @@ module Capybara::Selenium::Driver::EdgeDriver
39
39
  return unless @browser
40
40
 
41
41
  switch_to_window(window_handles.first)
42
- window_handles.slice(1..-1).each { |win| close_window(win) }
42
+ window_handles.slice(1..).each { |win| close_window(win) }
43
43
 
44
44
  timer = Capybara::Helpers.timer(expire_in: 10)
45
45
  begin
@@ -52,7 +52,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
52
52
  end
53
53
 
54
54
  switch_to_window(window_handles.first)
55
- window_handles.slice(1..-1).each { |win| close_window(win) }
55
+ window_handles.slice(1..).each { |win| close_window(win) }
56
56
  super
57
57
  end
58
58
 
@@ -14,7 +14,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
14
14
  end
15
15
 
16
16
  def all_text
17
- text = driver.evaluate_script('arguments[0].textContent', self)
17
+ text = driver.evaluate_script('arguments[0].textContent', self) || ''
18
18
  text.gsub(/[\u200b\u200e\u200f]/, '')
19
19
  .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
20
20
  .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
@@ -199,10 +199,6 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
199
199
  native.attribute('isContentEditable') == 'true'
200
200
  end
201
201
 
202
- def ==(other)
203
- native == other.native
204
- end
205
-
206
202
  def path
207
203
  driver.evaluate_script GET_XPATH_SCRIPT, self
208
204
  end
@@ -274,7 +270,7 @@ private
274
270
  elsif clear == :backspace
275
271
  # Clear field by sending the correct number of backspace keys.
276
272
  backspaces = [:backspace] * self.value.to_s.length
277
- send_keys(*([:end] + backspaces + [value]))
273
+ send_keys(:end, *backspaces, value)
278
274
  elsif clear.is_a? Array
279
275
  send_keys(*clear, value)
280
276
  else
@@ -282,7 +278,7 @@ private
282
278
  if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
283
279
  send_keys(value[0..3])
284
280
  driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
285
- send_keys(value[-3..-1])
281
+ send_keys(value[-3..])
286
282
  else
287
283
  send_keys(value)
288
284
  end
@@ -298,7 +294,7 @@ private
298
294
 
299
295
  scroll_if_needed do
300
296
  action_with_modifiers(click_options) do |action|
301
- if block_given?
297
+ if block
302
298
  yield action
303
299
  else
304
300
  click_options.coords? ? action.click : action.click(native)
@@ -488,6 +484,12 @@ private
488
484
  JS
489
485
  end
490
486
 
487
+ def native_id
488
+ # Selenium 3 -> 4 changed the return of ref
489
+ type_or_id, id = native.ref
490
+ id || type_or_id
491
+ end
492
+
491
493
  GET_XPATH_SCRIPT = <<~'JS'
492
494
  (function(el, xml){
493
495
  var xpath = '';
@@ -65,7 +65,7 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
65
65
  return super unless native_displayed?
66
66
 
67
67
  begin
68
- bridge.send(:execute, :is_element_displayed, id: native.ref)
68
+ bridge.send(:execute, :is_element_displayed, id: native_id)
69
69
  rescue Selenium::WebDriver::Error::UnknownCommandError
70
70
  # If the is_element_displayed command is unknown, no point in trying again
71
71
  driver.options[:native_displayed] = false
@@ -69,7 +69,7 @@ class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node
69
69
  return super unless chrome_edge? && native_displayed?
70
70
 
71
71
  begin
72
- bridge.send(:execute, :is_element_displayed, id: native.ref)
72
+ bridge.send(:execute, :is_element_displayed, id: native_id)
73
73
  rescue Selenium::WebDriver::Error::UnknownCommandError
74
74
  # If the is_element_displayed command is unknown, no point in trying again
75
75
  driver.options[:native_displayed] = false
@@ -76,7 +76,7 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
76
76
  return super unless native_displayed?
77
77
 
78
78
  begin
79
- bridge.send(:execute, :is_element_displayed, id: native.ref)
79
+ bridge.send(:execute, :is_element_displayed, id: native_id)
80
80
  rescue Selenium::WebDriver::Error::UnknownCommandError
81
81
  # If the is_element_displayed command is unknown, no point in trying again
82
82
  driver.options[:native_displayed] = false
@@ -14,7 +14,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
14
14
  warn 'You are attempting to click a table row which has issues in safaridriver - '\
15
15
  'Your test should probably be clicking on a table cell like a user would. '\
16
16
  'Clicking the first cell in the row instead.'
17
- return find_css('th:first-child,td:first-child')[0].click(keys, options)
17
+ return find_css('th:first-child,td:first-child')[0].click(keys, **options)
18
18
  end
19
19
  raise
20
20
  rescue ::Selenium::WebDriver::Error::WebDriverError => e
@@ -74,7 +74,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
74
74
  if clear == :backspace
75
75
  # Clear field by sending the correct number of backspace keys.
76
76
  backspaces = [:backspace] * self.value.to_s.length
77
- send_keys(*([[:control, 'e']] + backspaces + [value]))
77
+ send_keys([:control, 'e'], *backspaces, value)
78
78
  else
79
79
  super.tap do
80
80
  # React doesn't see the safaridriver element clear
@@ -16,7 +16,10 @@ module Capybara
16
16
 
17
17
  def initialize(app)
18
18
  @app = app
19
- @disable_markup = format(DISABLE_MARKUP_TEMPLATE, selector: self.class.selector_for(Capybara.disable_animation))
19
+ @disable_css_markup = format(DISABLE_CSS_MARKUP_TEMPLATE,
20
+ selector: self.class.selector_for(Capybara.disable_animation))
21
+ @disable_js_markup = format(DISABLE_JS_MARKUP_TEMPLATE,
22
+ selector: self.class.selector_for(Capybara.disable_animation))
20
23
  end
21
24
 
22
25
  def call(env)
@@ -33,22 +36,17 @@ module Capybara
33
36
 
34
37
  private
35
38
 
36
- attr_reader :disable_markup
39
+ attr_reader :disable_css_markup, :disable_js_markup
37
40
 
38
41
  def html_content?
39
42
  /html/.match?(@headers['Content-Type'])
40
43
  end
41
44
 
42
45
  def insert_disable(html)
43
- html.sub(%r{(</body>)}, "#{disable_markup}\\1")
46
+ html.sub(%r{(</head>)}, "#{disable_css_markup}\\1").sub(%r{(</body>)}, "#{disable_js_markup}\\1")
44
47
  end
45
48
 
46
- DISABLE_MARKUP_TEMPLATE = <<~HTML
47
- <script>
48
- //<![CDATA[
49
- (typeof jQuery !== 'undefined') && (jQuery.fx.off = true);
50
- //]]>
51
- </script>
49
+ DISABLE_CSS_MARKUP_TEMPLATE = <<~HTML
52
50
  <style>
53
51
  %<selector>s, %<selector>s::before, %<selector>s::after {
54
52
  transition: none !important;
@@ -58,6 +56,14 @@ module Capybara
58
56
  }
59
57
  </style>
60
58
  HTML
59
+
60
+ DISABLE_JS_MARKUP_TEMPLATE = <<~HTML
61
+ <script>
62
+ //<![CDATA[
63
+ (typeof jQuery !== 'undefined') && (jQuery.fx.off = true);
64
+ //]]>
65
+ </script>
66
+ HTML
61
67
  end
62
68
  end
63
69
  end
@@ -311,6 +311,16 @@ module Capybara
311
311
  driver.send_keys(*args, **kw_args)
312
312
  end
313
313
 
314
+ ##
315
+ #
316
+ # Returns the element with focus.
317
+ #
318
+ # Not supported by Rack Test
319
+ #
320
+ def active_element
321
+ Capybara::Queries::ActiveElementQuery.new.resolve_for(self)[0].tap(&:allow_reload!)
322
+ end
323
+
314
324
  ##
315
325
  #
316
326
  # Executes the given block within the context of a node. {#within} takes the
@@ -408,11 +418,11 @@ module Capybara
408
418
  idx = scopes.index(:frame)
409
419
  top_level_scopes = [:frame, nil]
410
420
  if idx
411
- if scopes.slice(idx..-1).any? { |scope| !top_level_scopes.include?(scope) }
421
+ if scopes.slice(idx..).any? { |scope| !top_level_scopes.include?(scope) }
412
422
  raise Capybara::ScopeError, "`switch_to_frame(:top)` cannot be called from inside a descendant frame's "\
413
423
  '`within` block.'
414
424
  end
415
- scopes.slice!(idx..-1)
425
+ scopes.slice!(idx..)
416
426
  driver.switch_to_frame(:top)
417
427
  end
418
428
  else
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara::SpecHelper.spec '#active_element', requires: [:active_element] do
4
+ it 'should return the active element' do
5
+ @session.visit('/form')
6
+ @session.send_keys(:tab)
7
+
8
+ expect(@session.active_element).to match_selector(:css, '[tabindex="1"]')
9
+
10
+ @session.send_keys(:tab)
11
+
12
+ expect(@session.active_element).to match_selector(:css, '[tabindex="2"]')
13
+ end
14
+
15
+ it 'should support reloading' do
16
+ @session.visit('/form')
17
+ expect(@session.active_element).to match_selector(:css, 'body')
18
+ @session.execute_script <<-JS
19
+ window.setTimeout(() => {
20
+ document.querySelector('#form_title').focus();
21
+ }, 1000)
22
+ JS
23
+ expect(@session.active_element).to match_selector(:css, 'body', wait: false)
24
+ expect(@session.active_element).to match_selector(:css, '#form_title', wait: 2)
25
+ end
26
+
27
+ it 'should return a Capybara::Element' do
28
+ @session.visit('/form')
29
+ expect(@session.active_element).to be_a Capybara::Node::Element
30
+ end
31
+ end
@@ -203,12 +203,10 @@ Capybara::SpecHelper.spec '#all' do
203
203
  expect { @session.all(:css, 'h1, p', between: 0..3) }.to raise_error(Capybara::ExpectationNotMet)
204
204
  end
205
205
 
206
- eval <<~TEST, binding, __FILE__, __LINE__ + 1 if RUBY_VERSION.to_f > 2.5
207
- it'treats an endless range as minimum' do
208
- expect { @session.all(:css, 'h1, p', between: 2..) }.not_to raise_error
209
- expect { @session.all(:css, 'h1, p', between: 5..) }.to raise_error(Capybara::ExpectationNotMet)
210
- end
211
- TEST
206
+ it 'treats an endless range as minimum' do
207
+ expect { @session.all(:css, 'h1, p', between: 2..) }.not_to raise_error
208
+ expect { @session.all(:css, 'h1, p', between: 5..) }.to raise_error(Capybara::ExpectationNotMet)
209
+ end
212
210
 
213
211
  eval <<~TEST, binding, __FILE__, __LINE__ + 1 if RUBY_VERSION.to_f > 2.6
214
212
  it'treats a beginless range as maximum' do
@@ -236,6 +236,15 @@ Capybara::SpecHelper.spec '#check' do
236
236
  expect(@session).to have_field('multi_label_checkbox', checked: true, visible: :hidden)
237
237
  end
238
238
  end
239
+
240
+ context 'with allow_label_click options', requires: [:js] do
241
+ it 'should allow offsets to click location on label' do
242
+ expect(@session.find(:checkbox, 'form_cars_lotus', unchecked: true, visible: :hidden)).to be_truthy
243
+ @session.check('form_cars_lotus', allow_label_click: { x: 90, y: 10 })
244
+ @session.click_button('awesome')
245
+ expect(extract_results(@session)['cars']).to include('lotus')
246
+ end
247
+ end
239
248
  end
240
249
  end
241
250
  end
@@ -11,6 +11,12 @@ Capybara::SpecHelper.spec '#choose' do
11
11
  expect(extract_results(@session)['gender']).to eq('male')
12
12
  end
13
13
 
14
+ it 'ignores readonly attribute on radio buttons' do
15
+ @session.choose('gender_both')
16
+ @session.click_button('awesome')
17
+ expect(extract_results(@session)['gender']).to eq('both')
18
+ end
19
+
14
20
  it 'should choose a radio button by label' do
15
21
  @session.choose('Both')
16
22
  @session.click_button('awesome')
@@ -22,4 +22,8 @@ Capybara::SpecHelper.spec '#have_any_of_selectors' do
22
22
  expect(@session).to have_any_of_selectors('p a#blah', 'h2#h2three')
23
23
  end.to raise_error ::RSpec::Expectations::ExpectationNotMetError
24
24
  end
25
+
26
+ it 'should be negateable' do
27
+ expect(@session).not_to have_any_of_selectors(:css, 'span a#foo', 'h2#h2nope', 'h2#h2one_no')
28
+ end
25
29
  end
@@ -65,6 +65,18 @@ Capybara::SpecHelper.spec '#has_button?' do
65
65
  it 'should not affect other selectors when enable_aria_role: false' do
66
66
  expect(@session).to have_button('Click me!', enable_aria_role: false)
67
67
  end
68
+
69
+ context 'with focused:', requires: [:active_element] do
70
+ it 'should be true if a field has focus when focused: true' do
71
+ @session.send_keys(:tab)
72
+
73
+ expect(@session).to have_button('A Button', focused: true)
74
+ end
75
+
76
+ it 'should be true if a field does not have focus when focused: false' do
77
+ expect(@session).to have_button('A Button', focused: false)
78
+ end
79
+ end
68
80
  end
69
81
 
70
82
  Capybara::SpecHelper.spec '#has_no_button?' do
@@ -117,4 +129,16 @@ Capybara::SpecHelper.spec '#has_no_button?' do
117
129
  it 'should not affect other selectors when enable_aria_role: false' do
118
130
  expect(@session).to have_no_button('Junk button that does not exist', enable_aria_role: false)
119
131
  end
132
+
133
+ context 'with focused:', requires: [:active_element] do
134
+ it 'should be true if a button does not have focus when focused: true' do
135
+ expect(@session).to have_no_button('A Button', focused: true)
136
+ end
137
+
138
+ it 'should be false if a button has focus when focused: false' do
139
+ @session.send_keys(:tab)
140
+
141
+ expect(@session).to have_no_button('A Button', focused: false)
142
+ end
143
+ end
120
144
  end
@@ -110,6 +110,18 @@ Capybara::SpecHelper.spec '#has_field' do
110
110
  end
111
111
  end
112
112
 
113
+ context 'with focused:', requires: [:active_element] do
114
+ it 'should be true if a field has focus' do
115
+ 2.times { @session.send_keys(:tab) }
116
+
117
+ expect(@session).to have_field('An Input', focused: true)
118
+ end
119
+
120
+ it 'should be false if a field does not have focus' do
121
+ expect(@session).to have_field('An Input', focused: false)
122
+ end
123
+ end
124
+
113
125
  context 'with valid', requires: [:js] do
114
126
  it 'should be true if field is valid' do
115
127
  @session.fill_in 'required', with: 'something'
@@ -184,6 +196,18 @@ Capybara::SpecHelper.spec '#has_no_field' do
184
196
  expect(@session).to have_no_field('Languages', type: 'textarea')
185
197
  end
186
198
  end
199
+
200
+ context 'with focused:', requires: [:active_element] do
201
+ it 'should be true if a field does not have focus when focused: true' do
202
+ expect(@session).to have_no_field('An Input', focused: true)
203
+ end
204
+
205
+ it 'should be false if a field has focus when focused: true' do
206
+ 2.times { @session.send_keys(:tab) }
207
+
208
+ expect(@session).not_to have_no_field('An Input', focused: true)
209
+ end
210
+ end
187
211
  end
188
212
 
189
213
  Capybara::SpecHelper.spec '#has_checked_field?' do
@@ -18,6 +18,18 @@ Capybara::SpecHelper.spec '#has_link?' do
18
18
  expect(@session).not_to have_link('A link', href: '/nonexistent-href')
19
19
  expect(@session).not_to have_link('A link', href: /nonexistent/)
20
20
  end
21
+
22
+ context 'with focused:', requires: [:active_element] do
23
+ it 'should be true if the given link is on the page and has focus' do
24
+ @session.send_keys(:tab)
25
+
26
+ expect(@session).to have_link('labore', focused: true)
27
+ end
28
+
29
+ it 'should be false if the given link is on the page and does not have focus' do
30
+ expect(@session).to have_link('labore', focused: false)
31
+ end
32
+ end
21
33
  end
22
34
 
23
35
  Capybara::SpecHelper.spec '#has_no_link?' do
@@ -36,4 +48,16 @@ Capybara::SpecHelper.spec '#has_no_link?' do
36
48
  expect(@session).to have_no_link('A link', href: '/nonexistent-href')
37
49
  expect(@session).to have_no_link('A link', href: %r{/nonexistent-href})
38
50
  end
51
+
52
+ context 'with focused:', requires: [:active_element] do
53
+ it 'should be true if the given link is on the page and has focus' do
54
+ expect(@session).to have_no_link('labore', focused: true)
55
+ end
56
+
57
+ it 'should be false if the given link is on the page and does not have focus' do
58
+ @session.send_keys(:tab)
59
+
60
+ expect(@session).to have_no_link('labore', focused: false)
61
+ end
62
+ end
39
63
  end
@@ -356,7 +356,7 @@ Capybara::SpecHelper.spec '#has_no_text?' do
356
356
  expect(@session).to have_no_text(/xxxxyzzz/)
357
357
  end
358
358
 
359
- it 'should be false if the text in the page matches given regexp' do
359
+ it 'should be false if the text in the page matches given regexp' do
360
360
  @session.visit('/with_html')
361
361
  expect(@session).not_to have_no_text(/Lorem/)
362
362
  end
@@ -1115,7 +1115,7 @@ Capybara::SpecHelper.spec 'node' do
1115
1115
  end
1116
1116
 
1117
1117
  describe '#evaluate_script', requires: %i[js es_args] do
1118
- it 'should evaluate the given script in the context of the element and return whatever it produces' do
1118
+ it 'should evaluate the given script in the context of the element and return whatever it produces' do
1119
1119
  @session.visit('/with_js')
1120
1120
  el = @session.find(:css, '#with_change_event')
1121
1121
  expect(el.evaluate_script('this.value')).to eq('default value')
@@ -15,7 +15,7 @@ Capybara::SpecHelper.spec '#scroll_to', requires: [:scroll] do
15
15
  el = @session.find(:css, '#scroll')
16
16
  @session.scroll_to(el, align: :bottom)
17
17
  el_bottom = el.evaluate_script('this.getBoundingClientRect().bottom')
18
- viewport_bottom = el.evaluate_script('document.body.clientHeight')
18
+ viewport_bottom = el.evaluate_script('document.documentElement.clientHeight')
19
19
  expect(el_bottom).to be_within(1).of(viewport_bottom)
20
20
  end
21
21
 
@@ -23,7 +23,7 @@ Capybara::SpecHelper.spec '#scroll_to', requires: [:scroll] do
23
23
  el = @session.find(:css, '#scroll')
24
24
  @session.scroll_to(el, align: :center)
25
25
  el_center = el.evaluate_script('(function(rect){return (rect.top + rect.bottom)/2})(this.getBoundingClientRect())')
26
- viewport_bottom = el.evaluate_script('document.body.clientHeight')
26
+ viewport_bottom = el.evaluate_script('document.documentElement.clientHeight')
27
27
  expect(el_center).to be_within(2).of(viewport_bottom / 2)
28
28
  end
29
29
 
@@ -35,13 +35,13 @@ Capybara::SpecHelper.spec '#scroll_to', requires: [:scroll] do
35
35
 
36
36
  it 'can scroll the window to the vertical bottom' do
37
37
  @session.scroll_to :bottom
38
- max_scroll = @session.evaluate_script('document.body.scrollHeight - document.body.clientHeight')
38
+ max_scroll = @session.evaluate_script('document.documentElement.scrollHeight - document.documentElement.clientHeight')
39
39
  expect(@session.evaluate_script('[window.scrollX || window.pageXOffset, window.scrollY || window.pageYOffset]')).to eq [0, max_scroll]
40
40
  end
41
41
 
42
42
  it 'can scroll the window to the vertical center' do
43
43
  @session.scroll_to :center
44
- max_scroll = @session.evaluate_script('document.documentElement.scrollHeight - document.body.clientHeight')
44
+ max_scroll = @session.evaluate_script('document.documentElement.scrollHeight - document.documentElement.clientHeight')
45
45
  expect(@session.evaluate_script('[window.scrollX || window.pageXOffset, window.scrollY || window.pageYOffset]')).to eq [0, max_scroll / 2]
46
46
  end
47
47
 
@@ -36,7 +36,7 @@ module Capybara
36
36
  Capybara.test_id = nil
37
37
  Capybara.predicates_wait = true
38
38
  Capybara.default_normalize_ws = false
39
- Capybara.allow_gumbo = true
39
+ Capybara.use_html5_parsing = !ENV['HTML5_PARSING'].nil?
40
40
  Capybara.w3c_click_offset = false
41
41
  reset_threadsafe
42
42
  end
@@ -117,8 +117,9 @@ module Capybara
117
117
 
118
118
  def extract_results(session)
119
119
  expect(session).to have_xpath("//pre[@id='results']")
120
- # YAML.load Nokogiri::HTML(session.body).xpath("//pre[@id='results']").first.inner_html.lstrip
121
- YAML.load Capybara::HTML(session.body).xpath("//pre[@id='results']").first.inner_html.lstrip
120
+ perms = [(::Sinatra::IndifferentHash if defined? ::Sinatra::IndifferentHash)].compact
121
+ results = Capybara::HTML(session.body).xpath("//pre[@id='results']").first.inner_html.lstrip
122
+ YAML.safe_load results, permitted_classes: perms
122
123
  end
123
124
 
124
125
  def be_an_invalid_element_error(session)
@@ -53,8 +53,8 @@ class TestApp < Sinatra::Base
53
53
 
54
54
  get '/referer_base' do
55
55
  '<a href="/get_referer">direct link</a>' \
56
- '<a href="/redirect_to_get_referer">link via redirect</a>' \
57
- '<form action="/get_referer" method="get"><input type="submit"></form>'
56
+ '<a href="/redirect_to_get_referer">link via redirect</a>' \
57
+ '<form action="/get_referer" method="get"><input type="submit"></form>'
58
58
  end
59
59
 
60
60
  get '/redirect_to_get_referer' do
@@ -163,21 +163,30 @@ class TestApp < Sinatra::Base
163
163
 
164
164
  get '/with_title' do
165
165
  <<-HTML
166
- <title>#{params[:title] || 'Test Title'}</title>
167
- <body>
168
- <svg><title>abcdefg</title></svg>
169
- </body>
166
+ <!DOCTYPE html>
167
+ <html lang="en">
168
+ <head>
169
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
170
+ <title>#{params[:title] || 'Test Title'}</title>
171
+ </head>
172
+
173
+ <body>
174
+ <svg><title>abcdefg</title></svg>
175
+ </body>
176
+ </html>
170
177
  HTML
171
178
  end
172
179
 
173
180
  get '/download.csv' do
174
181
  content_type 'text/csv'
175
182
  'This, is, comma, separated' \
176
- 'Thomas, Walpole, was , here'
183
+ 'Thomas, Walpole, was , here'
177
184
  end
178
185
 
179
186
  get '/:view' do |view|
180
- erb view.to_sym, locals: { referrer: request.referrer }
187
+ view_template = "#{__dir__}/views/#{view}.erb"
188
+ has_layout = File.exist?(view_template) && File.open(view_template) { |f| f.first.downcase.include?('doctype') }
189
+ erb view.to_sym, locals: { referrer: request.referrer }, layout: !has_layout
181
190
  end
182
191
 
183
192
  post '/form' do
@@ -1,3 +1,4 @@
1
+ <!DOCTYPE html>
1
2
  <html>
2
3
  <head>
3
4
  <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
@@ -46,4 +47,3 @@
46
47
 
47
48
  <iframe id="frameOne" src="/frame_one"></iframe>
48
49
  </html>
49
-