capybara 3.26.0 → 3.27.0

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