capybara 3.35.3 → 3.36.0

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