capybara 3.26.0 → 3.27.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +18 -0
  3. data/README.md +1 -2
  4. data/lib/capybara/minitest.rb +29 -29
  5. data/lib/capybara/node/element.rb +2 -1
  6. data/lib/capybara/node/matchers.rb +6 -6
  7. data/lib/capybara/node/simple.rb +2 -1
  8. data/lib/capybara/queries/ancestor_query.rb +5 -9
  9. data/lib/capybara/queries/selector_query.rb +2 -2
  10. data/lib/capybara/queries/sibling_query.rb +4 -10
  11. data/lib/capybara/registrations/servers.rb +4 -1
  12. data/lib/capybara/selector/regexp_disassembler.rb +7 -0
  13. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -1
  14. data/lib/capybara/selenium/atoms/src/isDisplayed.js +9 -9
  15. data/lib/capybara/selenium/driver.rb +2 -1
  16. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +9 -4
  17. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +8 -6
  18. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +9 -0
  19. data/lib/capybara/selenium/nodes/chrome_node.rb +41 -5
  20. data/lib/capybara/selenium/nodes/firefox_node.rb +16 -0
  21. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  22. data/lib/capybara/spec/session/node_spec.rb +40 -3
  23. data/lib/capybara/spec/views/with_html.erb +10 -0
  24. data/lib/capybara/version.rb +1 -1
  25. data/spec/basic_node_spec.rb +6 -6
  26. data/spec/capybara_spec.rb +28 -28
  27. data/spec/filter_set_spec.rb +5 -5
  28. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  29. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  30. data/spec/rack_test_spec.rb +9 -9
  31. data/spec/regexp_dissassembler_spec.rb +12 -2
  32. data/spec/rspec/shared_spec_matchers.rb +2 -2
  33. data/spec/rspec_spec.rb +1 -1
  34. data/spec/selector_spec.rb +15 -15
  35. data/spec/selenium_spec_chrome.rb +38 -0
  36. data/spec/selenium_spec_firefox.rb +1 -1
  37. data/spec/server_spec.rb +18 -18
  38. data/spec/session_spec.rb +4 -4
  39. data/spec/shared_selenium_node.rb +36 -0
  40. metadata +3 -2
@@ -11,7 +11,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
11
11
  clear_local_storage: nil,
12
12
  clear_session_storage: nil
13
13
  }.freeze
14
- SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout].freeze
14
+ SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout native_displayed].freeze
15
15
  attr_reader :app, :options
16
16
 
17
17
  class << self
@@ -19,6 +19,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
19
19
  require 'selenium-webdriver'
20
20
  require 'capybara/selenium/logger_suppressor'
21
21
  require 'capybara/selenium/patches/atoms'
22
+ require 'capybara/selenium/patches/is_displayed'
22
23
  warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade." if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
23
24
  rescue LoadError => e
24
25
  raise e unless e.message.match?(/selenium-webdriver/)
@@ -7,6 +7,8 @@ module Capybara::Selenium::Driver::ChromeDriver
7
7
  def self.extended(base)
8
8
  bridge = base.send(:bridge)
9
9
  bridge.extend Capybara::Selenium::ChromeLogs unless bridge.respond_to?(:log)
10
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.commands(:is_element_displayed)
11
+ base.options[:native_displayed] = false if base.options[:native_displayed].nil?
10
12
  end
11
13
 
12
14
  def fullscreen_window(handle)
@@ -43,8 +45,8 @@ module Capybara::Selenium::Driver::ChromeDriver
43
45
 
44
46
  timer = Capybara::Helpers.timer(expire_in: 10)
45
47
  begin
46
- @browser.navigate.to('about:blank')
47
48
  clear_storage unless uniform_storage_clear?
49
+ @browser.navigate.to('about:blank')
48
50
  wait_for_empty_page(timer)
49
51
  rescue *unhandled_alert_errors
50
52
  accept_unhandled_reset_alert
@@ -63,12 +65,15 @@ private
63
65
  end
64
66
 
65
67
  def clear_all_storage?
66
- options.values_at(:clear_session_storage, :clear_local_storage).none? { |s| s == false }
68
+ storage_clears.none? { |s| s == false }
67
69
  end
68
70
 
69
71
  def uniform_storage_clear?
70
- clear = options.values_at(:clear_session_storage, :clear_local_storage)
71
- clear.all? { |s| s == false } || clear.none? { |s| s == false }
72
+ storage_clears.uniq { |s| s == false }.length <= 1
73
+ end
74
+
75
+ def storage_clears
76
+ options.values_at(:clear_session_storage, :clear_local_storage)
72
77
  end
73
78
 
74
79
  def clear_storage
@@ -39,8 +39,8 @@ module Capybara::Selenium::Driver::EdgeDriver
39
39
 
40
40
  timer = Capybara::Helpers.timer(expire_in: 10)
41
41
  begin
42
- @browser.navigate.to('about:blank')
43
42
  clear_storage unless uniform_storage_clear?
43
+ @browser.navigate.to('about:blank')
44
44
  wait_for_empty_page(timer)
45
45
  rescue *unhandled_alert_errors
46
46
  accept_unhandled_reset_alert
@@ -68,17 +68,19 @@ private
68
68
  end
69
69
 
70
70
  def clear_all_storage?
71
- options.values_at(:clear_session_storage, :clear_local_storage).none? { |s| s == false }
71
+ storage_clears.none? { |s| s == false }
72
72
  end
73
73
 
74
74
  def uniform_storage_clear?
75
- clear = options.values_at(:clear_session_storage, :clear_local_storage)
76
- clear.all? { |s| s == false } || clear.none? { |s| s == false }
75
+ storage_clears.uniq { |s| s == false }.length <= 1
76
+ end
77
+
78
+ def storage_clears
79
+ options.values_at(:clear_session_storage, :clear_local_storage)
77
80
  end
78
81
 
79
82
  def clear_storage
80
- # Chrome errors if attempt to clear storage on about:blank
81
- # In W3C mode it crashes chromedriver
83
+ # Edgedriver crashes if attempt to clear storage on about:blank
82
84
  url = current_url
83
85
  super unless url.nil? || url.start_with?('about:')
84
86
  end
@@ -5,18 +5,27 @@ require 'capybara/selenium/nodes/firefox_node'
5
5
  module Capybara::Selenium::Driver::FirefoxDriver
6
6
  def self.extended(driver)
7
7
  driver.extend Capybara::Selenium::Driver::W3CFirefoxDriver if w3c?(driver)
8
+ bridge = driver.send(:bridge)
9
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.commands(:is_element_displayed)
8
10
  end
9
11
 
10
12
  def self.w3c?(driver)
11
13
  (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
12
14
  driver.browser.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
13
15
  end
16
+
17
+ private
18
+
19
+ def bridge
20
+ browser.send(:bridge)
21
+ end
14
22
  end
15
23
 
16
24
  module Capybara::Selenium::Driver::W3CFirefoxDriver
17
25
  class << self
18
26
  def extended(driver)
19
27
  require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(driver.browser)
28
+ driver.options[:native_displayed] = false if driver.options[:native_displayed].nil?
20
29
  end
21
30
 
22
31
  def pause_broken?(sel_driver)
@@ -55,10 +55,22 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
55
55
  click unless selected_or_disabled
56
56
  end
57
57
 
58
+ def visible?
59
+ return super unless native_displayed?
60
+
61
+ begin
62
+ bridge.send(:execute, :is_element_displayed, id: native.ref)
63
+ rescue Selenium::WebDriver::Error::UnknownCommandError
64
+ # If the is_element_displayed command is unknown, no point in trying again
65
+ driver.options[:native_displayed] = false
66
+ super
67
+ end
68
+ end
69
+
58
70
  private
59
71
 
60
72
  def perform_legacy_drag(element)
61
- return super unless (browser_version < 77.0) && w3c? && !element.obscured?
73
+ return super if chromedriver_fixed_actions_key_state? || !w3c? || element.obscured?
62
74
 
63
75
  # W3C Chrome/chromedriver < 77 doesn't maintain mouse button state across actions API performs
64
76
  # https://bugs.chromium.org/p/chromedriver/issues/detail?id=2981
@@ -78,11 +90,35 @@ private
78
90
 
79
91
  def w3c?
80
92
  (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
81
- driver.browser.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
93
+ capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
94
+ end
95
+
96
+ def browser_version(to_float = true)
97
+ caps = capabilities
98
+ ver = (caps[:browser_version] || caps[:version])
99
+ ver = ver.to_f if to_float
100
+ ver
101
+ end
102
+
103
+ def chromedriver_fixed_actions_key_state?
104
+ Gem::Version.new(chromedriver_version) >= Gem::Version.new('76.0.3809.68')
105
+ end
106
+
107
+ def chromedriver_supports_displayed_endpoint?
108
+ Gem::Version.new(chromedriver_version) >= Gem::Version.new('76.0.3809.25')
109
+ end
110
+
111
+ def chromedriver_version
112
+ capabilities['chrome']['chromedriverVersion'].split(' ')[0]
113
+ end
114
+
115
+ def capabilities
116
+ driver.browser.capabilities
82
117
  end
83
118
 
84
- def browser_version
85
- caps = driver.browser.capabilities
86
- (caps[:browser_version] || caps[:version]).to_f
119
+ def native_displayed?
120
+ (driver.options[:native_displayed] != false) &&
121
+ (w3c? && chromedriver_supports_displayed_endpoint?) &&
122
+ (!ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS'])
87
123
  end
88
124
  end
@@ -65,8 +65,24 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
65
65
  click unless selected_or_disabled
66
66
  end
67
67
 
68
+ def visible?
69
+ return super unless native_displayed?
70
+
71
+ begin
72
+ bridge.send(:execute, :is_element_displayed, id: native.ref)
73
+ rescue Selenium::WebDriver::Error::UnknownCommandError
74
+ # If the is_element_displayed command is unknown, no point in trying again
75
+ driver.options[:native_displayed] = false
76
+ super
77
+ end
78
+ end
79
+
68
80
  private
69
81
 
82
+ def native_displayed?
83
+ (driver.options[:native_displayed] != false) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
84
+ end
85
+
70
86
  def click_with_options(click_options)
71
87
  # Firefox/marionette has an issue clicking with offset near viewport edge
72
88
  # scroll element to middle just in case
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Selenium
5
+ module IsDisplayed
6
+ def commands(command)
7
+ case command
8
+ when :is_element_displayed
9
+ [:get, 'session/:session_id/element/:id/displayed']
10
+ else
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -233,8 +233,9 @@ Capybara::SpecHelper.spec 'node' do
233
233
  end
234
234
 
235
235
  describe '#visible?' do
236
+ before { Capybara.ignore_hidden_elements = false }
237
+
236
238
  it 'should extract node visibility' do
237
- Capybara.ignore_hidden_elements = false
238
239
  expect(@session.first('//a')).to be_visible
239
240
 
240
241
  expect(@session.find('//div[@id="hidden"]')).not_to be_visible
@@ -245,15 +246,29 @@ Capybara::SpecHelper.spec 'node' do
245
246
  end
246
247
 
247
248
  it 'template elements should not be visible' do
248
- Capybara.ignore_hidden_elements = false
249
249
  expect(@session.find('//template')).not_to be_visible
250
250
  end
251
251
 
252
252
  it 'should be boolean' do
253
- Capybara.ignore_hidden_elements = false
254
253
  expect(@session.first('//a').visible?).to be true
255
254
  expect(@session.find('//div[@id="hidden"]').visible?).to be false
256
255
  end
256
+
257
+ it 'details > summary elements and descendants should be visible' do
258
+ expect(@session.find(:css, 'details summary')).to be_visible
259
+ expect(@session.find(:css, 'details summary h6')).to be_visible
260
+ end
261
+
262
+ it 'details non-summary descendants should be non-visible' do
263
+ @session.first(:css, 'details li').visible?
264
+ descendants = @session.all(:css, 'details > *:not(summary), details > *:not(summary) *', minimum: 2)
265
+ expect(descendants).not_to include(be_visible)
266
+ end
267
+
268
+ it 'sees open details as visible', requires: [:js] do
269
+ @session.find(:css, 'details').click
270
+ expect(@session.all(:css, 'details *')).to all(be_visible)
271
+ end
257
272
  end
258
273
 
259
274
  describe '#obscured?', requires: [:css] do
@@ -987,6 +1002,28 @@ Capybara::SpecHelper.spec 'node' do
987
1002
  end
988
1003
 
989
1004
  describe '#reload', requires: [:js] do
1005
+ it 'should reload elements found via ancestor with CSS' do
1006
+ @session.visit('/with_js')
1007
+ node = @session.find(:css, '#reload-me em').ancestor(:css, 'div')
1008
+ node.reload
1009
+ expect(node[:id]).to eq 'reload-me'
1010
+ end
1011
+
1012
+ it 'should reload elements found via ancestor with XPath' do
1013
+ @session.visit('/with_js')
1014
+ node = @session.find(:css, '#reload-me em').ancestor(:xpath, './/div')
1015
+ node.reload
1016
+ expect(node[:id]).to eq 'reload-me'
1017
+ end
1018
+
1019
+ it 'should reload elements found via sibling' do
1020
+ @session.visit('/with_js')
1021
+ node = @session.find(:css, '#the-list li', text: 'Item 1').sibling(:css, 'li')
1022
+ expect(node.text).to eq 'Item 2'
1023
+ node.reload
1024
+ expect(node.text).to eq 'Item 2'
1025
+ end
1026
+
990
1027
  context 'without automatic reload' do
991
1028
  before { Capybara.automatic_reload = false }
992
1029
 
@@ -181,6 +181,16 @@ banana
181
181
  &#x20;&#x1680;&#x2000;&#x2001;&#x2002; &#x2003;&#x2004;&nbsp;&#x2005; &#x2006;&#x2007;&#x2008;&#x2009;&#x200A;&#x202F;&#x205F;&#x3000;
182
182
  </div>
183
183
 
184
+ <details>
185
+ <summary>
186
+ <h6>Something</h6>
187
+ </summary>
188
+ <ul>
189
+ <li>Random</li>
190
+ <li>Things</li>
191
+ </ul>
192
+ </details>
193
+
184
194
  <template id="template">
185
195
  <input />
186
196
  </template>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Capybara
4
- VERSION = '3.26.0'
4
+ VERSION = '3.27.0'
5
5
  end
@@ -5,7 +5,7 @@ require 'spec_helper'
5
5
  RSpec.describe Capybara do
6
6
  describe '.string' do
7
7
  let :string do
8
- Capybara.string <<-STRING
8
+ described_class.string <<-STRING
9
9
  <html>
10
10
  <head>
11
11
  <title>simple_node</title>
@@ -52,7 +52,7 @@ RSpec.describe Capybara do
52
52
  end
53
53
 
54
54
  it 'allows using custom matchers' do
55
- Capybara.add_selector :lifeform do
55
+ described_class.add_selector :lifeform do
56
56
  xpath { |name| ".//option[contains(.,'#{name}')]" }
57
57
  end
58
58
  expect(string).to have_selector(:id, 'page')
@@ -62,7 +62,7 @@ RSpec.describe Capybara do
62
62
  end
63
63
 
64
64
  it 'allows custom matcher using css' do
65
- Capybara.add_selector :section do
65
+ described_class.add_selector :section do
66
66
  css { |css_class| "section .#{css_class}" }
67
67
  end
68
68
  expect(string).to have_selector(:section, 'subsection')
@@ -112,13 +112,13 @@ RSpec.describe Capybara do
112
112
 
113
113
  it 'drops illegal fragments when using gumbo' do
114
114
  skip 'libxml is less strict than Gumbo' unless Nokogiri.respond_to?(:HTML5)
115
- expect(Capybara.string('<td>1</td>')).not_to have_css('td')
115
+ expect(described_class.string('<td>1</td>')).not_to have_css('td')
116
116
  end
117
117
 
118
118
  it 'can disable use of gumbo' do
119
119
  skip "Test doesn't make sense unlesss nokogumbo is loaded" unless Nokogiri.respond_to?(:HTML5)
120
- Capybara.allow_gumbo = false
121
- expect(Capybara.string('<td>1</td>')).to have_css('td')
120
+ described_class.allow_gumbo = false
121
+ expect(described_class.string('<td>1</td>')).to have_css('td')
122
122
  end
123
123
 
124
124
  describe '#title' do
@@ -4,20 +4,20 @@ require 'spec_helper'
4
4
 
5
5
  RSpec.describe Capybara do
6
6
  describe 'default_max_wait_time' do
7
- before { @previous_default_time = Capybara.default_max_wait_time }
7
+ before { @previous_default_time = described_class.default_max_wait_time }
8
8
 
9
- after { Capybara.default_max_wait_time = @previous_default_time } # rubocop:disable RSpec/InstanceVariable
9
+ after { described_class.default_max_wait_time = @previous_default_time } # rubocop:disable RSpec/InstanceVariable
10
10
 
11
11
  it 'should be changeable' do
12
- expect(Capybara.default_max_wait_time).not_to eq(5)
13
- Capybara.default_max_wait_time = 5
14
- expect(Capybara.default_max_wait_time).to eq(5)
12
+ expect(described_class.default_max_wait_time).not_to eq(5)
13
+ described_class.default_max_wait_time = 5
14
+ expect(described_class.default_max_wait_time).to eq(5)
15
15
  end
16
16
  end
17
17
 
18
18
  describe '.register_driver' do
19
19
  it 'should add a new driver' do
20
- Capybara.register_driver :schmoo do |app|
20
+ described_class.register_driver :schmoo do |app|
21
21
  Capybara::RackTest::Driver.new(app)
22
22
  end
23
23
  session = Capybara::Session.new(:schmoo, TestApp)
@@ -28,85 +28,85 @@ RSpec.describe Capybara do
28
28
 
29
29
  describe '.register_server' do
30
30
  it 'should add a new server' do
31
- Capybara.register_server :blob do |_app, _port, _host|
31
+ described_class.register_server :blob do |_app, _port, _host|
32
32
  # do nothing
33
33
  end
34
34
 
35
- expect(Capybara.servers).to have_key(:blob)
35
+ expect(described_class.servers).to have_key(:blob)
36
36
  end
37
37
  end
38
38
 
39
39
  describe '.server' do
40
40
  after do
41
- Capybara.server = :default
41
+ described_class.server = :default
42
42
  end
43
43
 
44
44
  it 'should default to a proc that calls run_default_server' do
45
45
  mock_app = Object.new
46
- allow(Capybara).to receive(:run_default_server).and_return(true)
47
- Capybara.server.call(mock_app, 8000)
48
- expect(Capybara).to have_received(:run_default_server).with(mock_app, 8000)
46
+ allow(described_class).to receive(:run_default_server).and_return(true)
47
+ described_class.server.call(mock_app, 8000)
48
+ expect(described_class).to have_received(:run_default_server).with(mock_app, 8000)
49
49
  end
50
50
 
51
51
  it 'should return a custom server proc' do
52
52
  server = ->(_app, _port) {}
53
- Capybara.register_server :custom, &server
54
- Capybara.server = :custom
55
- expect(Capybara.server).to eq(server)
53
+ described_class.register_server :custom, &server
54
+ described_class.server = :custom
55
+ expect(described_class.server).to eq(server)
56
56
  end
57
57
 
58
58
  it 'should have :webrick registered' do
59
- expect(Capybara.servers[:webrick]).not_to be_nil
59
+ expect(described_class.servers[:webrick]).not_to be_nil
60
60
  end
61
61
 
62
62
  it 'should have :puma registered' do
63
- expect(Capybara.servers[:puma]).not_to be_nil
63
+ expect(described_class.servers[:puma]).not_to be_nil
64
64
  end
65
65
  end
66
66
 
67
67
  describe 'server=' do
68
68
  after do
69
- Capybara.server = :default
69
+ described_class.server = :default
70
70
  end
71
71
 
72
72
  it 'accepts a proc' do
73
73
  server = ->(_app, _port) {}
74
- Capybara.server = server
75
- expect(Capybara.server).to eq server
74
+ described_class.server = server
75
+ expect(described_class.server).to eq server
76
76
  end
77
77
  end
78
78
 
79
79
  describe 'app_host' do
80
80
  after do
81
- Capybara.app_host = nil
81
+ described_class.app_host = nil
82
82
  end
83
83
 
84
84
  it 'should warn if not a valid URL' do
85
- expect { Capybara.app_host = 'www.example.com' }.to raise_error(ArgumentError, /Capybara\.app_host should be set to a url/)
85
+ expect { described_class.app_host = 'www.example.com' }.to raise_error(ArgumentError, /Capybara\.app_host should be set to a url/)
86
86
  end
87
87
 
88
88
  it 'should not warn if a valid URL' do
89
- expect { Capybara.app_host = 'http://www.example.com' }.not_to raise_error
89
+ expect { described_class.app_host = 'http://www.example.com' }.not_to raise_error
90
90
  end
91
91
 
92
92
  it 'should not warn if nil' do
93
- expect { Capybara.app_host = nil }.not_to raise_error
93
+ expect { described_class.app_host = nil }.not_to raise_error
94
94
  end
95
95
  end
96
96
 
97
97
  describe 'default_host' do
98
98
  around do |test|
99
- old_default = Capybara.default_host
99
+ old_default = described_class.default_host
100
100
  test.run
101
- Capybara.default_host = old_default
101
+ described_class.default_host = old_default
102
102
  end
103
103
 
104
104
  it 'should raise if not a valid URL' do
105
- expect { Capybara.default_host = 'www.example.com' }.to raise_error(ArgumentError, /Capybara\.default_host should be set to a url/)
105
+ expect { described_class.default_host = 'www.example.com' }.to raise_error(ArgumentError, /Capybara\.default_host should be set to a url/)
106
106
  end
107
107
 
108
108
  it 'should not warn if a valid URL' do
109
- expect { Capybara.default_host = 'http://www.example.com' }.not_to raise_error
109
+ expect { described_class.default_host = 'http://www.example.com' }.not_to raise_error
110
110
  end
111
111
  end
112
112
  end