capybara 3.2.1 → 3.3.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +22 -1
  3. data/README.md +1 -1
  4. data/lib/capybara.rb +2 -0
  5. data/lib/capybara/driver/base.rb +5 -1
  6. data/lib/capybara/driver/node.rb +4 -0
  7. data/lib/capybara/helpers.rb +25 -0
  8. data/lib/capybara/minitest.rb +7 -1
  9. data/lib/capybara/minitest/spec.rb +8 -1
  10. data/lib/capybara/node/base.rb +3 -3
  11. data/lib/capybara/node/element.rb +52 -0
  12. data/lib/capybara/node/matchers.rb +33 -0
  13. data/lib/capybara/queries/selector_query.rb +4 -4
  14. data/lib/capybara/queries/style_query.rb +41 -0
  15. data/lib/capybara/rack_test/browser.rb +7 -1
  16. data/lib/capybara/rack_test/node.rb +4 -0
  17. data/lib/capybara/rspec/compound.rb +67 -65
  18. data/lib/capybara/rspec/features.rb +2 -4
  19. data/lib/capybara/rspec/matchers.rb +30 -10
  20. data/lib/capybara/selector.rb +9 -0
  21. data/lib/capybara/selector/css.rb +74 -1
  22. data/lib/capybara/selector/filters/base.rb +2 -1
  23. data/lib/capybara/selector/filters/expression_filter.rb +2 -1
  24. data/lib/capybara/selector/selector.rb +1 -1
  25. data/lib/capybara/selenium/driver.rb +34 -43
  26. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +35 -0
  27. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +31 -0
  28. data/lib/capybara/selenium/node.rb +17 -20
  29. data/lib/capybara/selenium/nodes/marionette_node.rb +31 -0
  30. data/lib/capybara/server.rb +8 -29
  31. data/lib/capybara/server/checker.rb +38 -0
  32. data/lib/capybara/spec/public/test.js +5 -0
  33. data/lib/capybara/spec/session/all_spec.rb +4 -0
  34. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  35. data/lib/capybara/spec/session/click_button_spec.rb +10 -0
  36. data/lib/capybara/spec/session/click_link_spec.rb +11 -0
  37. data/lib/capybara/spec/session/fill_in_spec.rb +2 -0
  38. data/lib/capybara/spec/session/find_link_spec.rb +18 -0
  39. data/lib/capybara/spec/session/find_spec.rb +1 -0
  40. data/lib/capybara/spec/session/first_spec.rb +1 -0
  41. data/lib/capybara/spec/session/has_css_spec.rb +0 -6
  42. data/lib/capybara/spec/session/has_style_spec.rb +25 -0
  43. data/lib/capybara/spec/session/node_spec.rb +34 -0
  44. data/lib/capybara/spec/session/save_page_spec.rb +4 -1
  45. data/lib/capybara/spec/session/save_screenshot_spec.rb +3 -1
  46. data/lib/capybara/spec/session/text_spec.rb +1 -0
  47. data/lib/capybara/spec/session/title_spec.rb +1 -0
  48. data/lib/capybara/spec/session/window/current_window_spec.rb +1 -0
  49. data/lib/capybara/spec/session/window/open_new_window_spec.rb +1 -0
  50. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -0
  51. data/lib/capybara/spec/session/window/window_spec.rb +20 -0
  52. data/lib/capybara/spec/session/window/windows_spec.rb +1 -0
  53. data/lib/capybara/spec/session/window/within_window_spec.rb +1 -0
  54. data/lib/capybara/spec/session/within_spec.rb +1 -0
  55. data/lib/capybara/spec/spec_helper.rb +3 -1
  56. data/lib/capybara/spec/test_app.rb +18 -0
  57. data/lib/capybara/spec/views/form.erb +8 -0
  58. data/lib/capybara/spec/views/tables.erb +1 -1
  59. data/lib/capybara/spec/views/with_html.erb +9 -2
  60. data/lib/capybara/spec/views/with_js.erb +4 -0
  61. data/lib/capybara/spec/views/with_namespace.erb +20 -0
  62. data/lib/capybara/version.rb +1 -1
  63. data/lib/capybara/window.rb +11 -0
  64. data/spec/css_splitter_spec.rb +38 -0
  65. data/spec/dsl_spec.rb +1 -1
  66. data/spec/minitest_spec.rb +7 -1
  67. data/spec/minitest_spec_spec.rb +8 -1
  68. data/spec/rack_test_spec.rb +10 -0
  69. data/spec/rspec/shared_spec_matchers.rb +2 -0
  70. data/spec/selenium_spec_chrome.rb +28 -0
  71. data/spec/selenium_spec_chrome_remote.rb +2 -2
  72. data/spec/selenium_spec_marionette.rb +21 -1
  73. data/spec/server_spec.rb +0 -1
  74. data/spec/shared_selenium_session.rb +16 -1
  75. metadata +18 -19
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selenium/nodes/marionette_node'
4
+
5
+ module Capybara::Selenium::Driver::MarionetteDriver
6
+ def resize_window_to(handle, width, height)
7
+ within_given_window(handle) do
8
+ # Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
9
+ if window_size(handle) == [width, height]
10
+ {}
11
+ else
12
+ super
13
+ end
14
+ end
15
+ end
16
+
17
+ def reset!
18
+ # Use instance variable directly so we avoid starting the browser just to reset the session
19
+ return unless @browser
20
+
21
+ switch_to_window(window_handles.first)
22
+ window_handles.slice(1..-1).each { |win| close_window(win) }
23
+ super
24
+ end
25
+
26
+ private
27
+
28
+ def build_node(native_node)
29
+ ::Capybara::Selenium::MarionetteNode.new(self, native_node)
30
+ end
31
+ end
@@ -28,6 +28,12 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
28
28
  end
29
29
  end
30
30
 
31
+ def style(styles)
32
+ styles.each_with_object({}) do |style, result|
33
+ result[style] = native.css_value(style)
34
+ end
35
+ end
36
+
31
37
  ##
32
38
  #
33
39
  # Set the value of the form element to the given value.
@@ -91,6 +97,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
91
97
  e.message =~ /Other element would receive the click/
92
98
  scroll_to_center
93
99
  end
100
+
94
101
  raise e
95
102
  end
96
103
 
@@ -133,18 +140,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
133
140
  alias :checked? :selected?
134
141
 
135
142
  def disabled?
136
- return true unless native.enabled?
137
-
138
- # workaround for selenium-webdriver/geckodriver reporting elements as enabled when they are nested in disabling elements
139
- if driver.marionette?
140
- if %w[option optgroup].include? tag_name
141
- find_xpath("parent::*[self::optgroup or self::select]")[0].disabled?
142
- else
143
- !find_xpath("parent::fieldset[@disabled] | ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]").empty?
144
- end
145
- else
146
- false
147
- end
143
+ !native.enabled?
148
144
  end
149
145
 
150
146
  def content_editable?
@@ -167,9 +163,15 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
167
163
  path = find_xpath(XPath.ancestor_or_self).reverse
168
164
 
169
165
  result = []
166
+ default_ns = path.last[:namespaceURI]
170
167
  while (node = path.shift)
171
168
  parent = path.first
172
- selector = node.tag_name
169
+ selector = node[:tagName]
170
+ if node[:namespaceURI] != default_ns
171
+ selector = XPath.child.where((XPath.local_name == selector) & (XPath.namespace_uri == node[:namespaceURI])).to_s
172
+ selector
173
+ end
174
+
173
175
  if parent
174
176
  siblings = parent.find_xpath(selector)
175
177
  selector += "[#{siblings.index(node) + 1}]" unless siblings.size == 1
@@ -265,17 +267,12 @@ private
265
267
  arguments[0].dispatchEvent(new InputEvent('input'));
266
268
  arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
267
269
  }
268
- JS
270
+ JS
269
271
  end
270
272
 
271
273
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
272
274
  path_names = value.to_s.empty? ? [] : value
273
- if driver.marionette?
274
- native.clear
275
- Array(path_names).each { |p| native.send_keys(p) }
276
- else
277
- native.send_keys(Array(path_names).join("\n"))
278
- end
275
+ native.send_keys(Array(path_names).join("\n"))
279
276
  end
280
277
 
281
278
  def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node
4
+ def click(keys = [], **options)
5
+ super
6
+ rescue ::Selenium::WebDriver::Error::ElementNotInteractableError
7
+ if tag_name == "tr"
8
+ warn "You are attempting to click a table row which has issues in geckodriver/marionette - see https://github.com/mozilla/geckodriver/issues/1228. " \
9
+ "Your test should probably be clicking on a table cell like a user would. Clicking the first cell in the row instead."
10
+ return find_css('th:first-child,td:first-child')[0].click
11
+ end
12
+ raise
13
+ end
14
+
15
+ def disabled?
16
+ return true if super
17
+
18
+ # workaround for selenium-webdriver/geckodriver reporting elements as enabled when they are nested in disabling elements
19
+ if %w[option optgroup].include? tag_name
20
+ find_xpath("parent::*[self::optgroup or self::select]")[0].disabled?
21
+ else
22
+ !find_xpath("parent::fieldset[@disabled] | ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]").empty?
23
+ end
24
+ end
25
+
26
+ def set_file(value) # rubocop:disable Naming/AccessorMethodName
27
+ path_names = value.to_s.empty? ? [] : value
28
+ native.clear
29
+ Array(path_names).each { |p| native.send_keys(p) }
30
+ end
31
+ end
@@ -5,6 +5,7 @@ require 'net/http'
5
5
  require 'rack'
6
6
  require 'capybara/server/middleware'
7
7
  require 'capybara/server/animation_disabler'
8
+ require 'capybara/server/checker'
8
9
 
9
10
  module Capybara
10
11
  class Server
@@ -23,10 +24,10 @@ module Capybara
23
24
  @server_thread = nil # suppress warnings
24
25
  @host = deprecated_options[1] || host
25
26
  @reportable_errors = deprecated_options[2] || reportable_errors
26
- @using_ssl = false
27
27
  @port = deprecated_options[0] || port
28
28
  @port ||= Capybara::Server.ports[port_key]
29
29
  @port ||= find_available_port(host)
30
+ @checker = Checker.new(@host, @port)
30
31
  end
31
32
 
32
33
  def reset_error!
@@ -38,22 +39,12 @@ module Capybara
38
39
  end
39
40
 
40
41
  def using_ssl?
41
- @using_ssl
42
+ @checker.ssl?
42
43
  end
43
44
 
44
45
  def responsive?
45
46
  return false if @server_thread&.join(0)
46
-
47
- begin
48
- res = if !using_ssl?
49
- http_connect
50
- else
51
- https_connect
52
- end
53
- rescue EOFError, Net::ReadTimeout
54
- res = https_connect
55
- @using_ssl = true
56
- end
47
+ res = @checker.request { |http| http.get('/__identify__') }
57
48
 
58
49
  if res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPRedirection)
59
50
  return res.body == app.object_id.to_s
@@ -63,11 +54,9 @@ module Capybara
63
54
  end
64
55
 
65
56
  def wait_for_pending_requests
66
- start_time = Capybara::Helpers.monotonic_time
57
+ timer = Capybara::Helpers.timer(expire_in: 60)
67
58
  while pending_requests?
68
- if (Capybara::Helpers.monotonic_time - start_time) > 60
69
- raise "Requests did not finish in 60 seconds"
70
- end
59
+ raise "Requests did not finish in 60 seconds" if timer.expired?
71
60
  sleep 0.01
72
61
  end
73
62
  end
@@ -80,11 +69,9 @@ module Capybara
80
69
  Capybara.server.call(middleware, port, host)
81
70
  end
82
71
 
83
- start_time = Capybara::Helpers.monotonic_time
72
+ timer = Capybara::Helpers.timer(expire_in: 60)
84
73
  until responsive?
85
- if (Capybara::Helpers.monotonic_time - start_time) > 60
86
- raise "Rack application timed out during boot"
87
- end
74
+ raise "Rack application timed out during boot" if timer.expired?
88
75
  @server_thread.join(0.1)
89
76
  end
90
77
  end
@@ -94,14 +81,6 @@ module Capybara
94
81
 
95
82
  private
96
83
 
97
- def http_connect
98
- Net::HTTP.start(host, port, read_timeout: 2) { |http| http.get('/__identify__') }
99
- end
100
-
101
- def https_connect
102
- Net::HTTP.start(host, port, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) { |http| http.get('/__identify__') }
103
- end
104
-
105
84
  def middleware
106
85
  @middleware ||= Middleware.new(app, @reportable_errors, @extra_middleware)
107
86
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ class Server
5
+ class Checker
6
+ def initialize(host, port)
7
+ @host, @port = host, port
8
+ @ssl = false
9
+ end
10
+
11
+ def request(&block)
12
+ ssl? ? https_request(&block) : http_request(&block)
13
+ rescue EOFError, Net::ReadTimeout
14
+ res = https_request(&block)
15
+ @ssl = true
16
+ res
17
+ end
18
+
19
+ def ssl?
20
+ @ssl
21
+ end
22
+
23
+ private
24
+
25
+ def http_request(&block)
26
+ Net::HTTP.start(@host, @port, read_timeout: 2, &block)
27
+ end
28
+
29
+ def https_request(&block)
30
+ Net::HTTP.start(@host, @port, ssl_options, &block)
31
+ end
32
+
33
+ def ssl_options
34
+ { use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -63,6 +63,11 @@ $(function() {
63
63
  $('title').text('changed title')
64
64
  }, 400)
65
65
  });
66
+ $('#change-size').click(function() {
67
+ setTimeout(function() {
68
+ document.getElementById('change').style.fontSize = '50px';
69
+ }, 500)
70
+ });
66
71
  $('#click-test').on({
67
72
  click: function(e) {
68
73
  var desc = "";
@@ -59,6 +59,7 @@ Capybara::SpecHelper.spec "#all" do
59
59
 
60
60
  context "with css as default selector" do
61
61
  before { Capybara.default_selector = :css }
62
+
62
63
  it "should find the first element using the given locator" do
63
64
  expect(@session.all('h1').first.text).to eq('This is a test')
64
65
  expect(@session.all("input[id='test_field']").first.value).to eq('monkey')
@@ -102,6 +103,7 @@ Capybara::SpecHelper.spec "#all" do
102
103
  expect { @session.all(:css, 'h1, p', count: 5) }.to raise_error(Capybara::ExpectationNotMet)
103
104
  end
104
105
  end
106
+
105
107
  context ':minimum' do
106
108
  it 'should succeed when the number of elements founds matches the expectation' do
107
109
  expect { @session.all(:css, 'h1, p', minimum: 0) }.not_to raise_error
@@ -110,6 +112,7 @@ Capybara::SpecHelper.spec "#all" do
110
112
  expect { @session.all(:css, 'h1, p', minimum: 5) }.to raise_error(Capybara::ExpectationNotMet)
111
113
  end
112
114
  end
115
+
113
116
  context ':maximum' do
114
117
  it 'should succeed when the number of elements founds matches the expectation' do
115
118
  expect { @session.all(:css, 'h1, p', maximum: 4) }.not_to raise_error
@@ -118,6 +121,7 @@ Capybara::SpecHelper.spec "#all" do
118
121
  expect { @session.all(:css, 'h1, p', maximum: 0) }.to raise_error(Capybara::ExpectationNotMet)
119
122
  end
120
123
  end
124
+
121
125
  context ':between' do
122
126
  it 'should succeed when the number of elements founds matches the expectation' do
123
127
  expect { @session.all(:css, 'h1, p', between: 2..7) }.not_to raise_error
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara::SpecHelper.spec '#assert_style', requires: [:css] do
4
+ it "should not raise if the elements style contains the given properties" do
5
+ @session.visit('/with_html')
6
+ expect do
7
+ @session.find(:css, '#first').assert_style(display: 'block')
8
+ end.not_to raise_error
9
+ end
10
+
11
+ it "should raise error if the elements style doesn't contain the given properties" do
12
+ @session.visit('/with_html')
13
+ expect do
14
+ @session.find(:css, '#first').assert_style(display: 'inline')
15
+ end.to raise_error(Capybara::ExpectationNotMet, 'Expected node to have styles {"display"=>"inline"}. Actual styles were {"display"=>"block"}')
16
+ end
17
+
18
+ it "should wait for style", requires: %i[css js] do
19
+ @session.visit('/with_js')
20
+ el = @session.find(:css, '#change')
21
+ @session.click_link("Change size")
22
+ expect do
23
+ el.assert_style({ 'font-size': '50px' }, wait: 3)
24
+ end.not_to raise_error
25
+ end
26
+ end
@@ -396,6 +396,16 @@ Capybara::SpecHelper.spec '#click_button' do
396
396
  expect(@session.current_url).to match(%r{/landed$})
397
397
  end
398
398
 
399
+ it "should follow temporary redirects that maintain method" do
400
+ @session.click_button('Go 307')
401
+ expect(@session).to have_content('You post landed: TWTW')
402
+ end
403
+
404
+ it "should follow permanent redirects that maintain method" do
405
+ @session.click_button('Go 308')
406
+ expect(@session).to have_content('You post landed: TWTW')
407
+ end
408
+
399
409
  it "should post pack to the same URL when no action given" do
400
410
  @session.visit('/postback')
401
411
  @session.click_button('With no action')
@@ -203,4 +203,15 @@ Capybara::SpecHelper.spec '#click_link' do
203
203
  el = @session.find(:link, 'Normal Anchor')
204
204
  expect(@session.click_link('Normal Anchor')).to eq el
205
205
  end
206
+
207
+ it "can download a file", requires: [:download] do
208
+ # This requires the driver used for the test to be configured
209
+ # to download documents with the mime type "text/csv"
210
+ download_file = File.join(Capybara.save_path, 'download.csv')
211
+ expect(File).not_to exist(download_file)
212
+ @session.click_link('Download Me')
213
+ sleep 2
214
+ expect(File).to exist(download_file)
215
+ FileUtils.rm_rf download_file
216
+ end
206
217
  end
@@ -188,7 +188,9 @@ Capybara::SpecHelper.spec "#fill_in" do
188
188
 
189
189
  context "with ignore_hidden_fields" do
190
190
  before { Capybara.ignore_hidden_elements = true }
191
+
191
192
  after { Capybara.ignore_hidden_elements = false }
193
+
192
194
  it "should not find a hidden field" do
193
195
  msg = "Unable to find visible field \"Super Secret\" that is not disabled"
194
196
  expect do
@@ -49,4 +49,22 @@ Capybara::SpecHelper.spec '#find_link' do
49
49
  expect(@session.find_link(href: '#anchor').text).to eq "Normal Anchor"
50
50
  end
51
51
  end
52
+
53
+ context "download filter" do
54
+ it "finds a download link" do
55
+ expect(@session.find_link("Download Me", download: true).text).to eq "Download Me"
56
+ end
57
+
58
+ it "doesn't find a download link if download is false" do
59
+ expect { @session.find_link("Download Me", download: false) }.to raise_error Capybara::ElementNotFound
60
+ end
61
+
62
+ it "finds a renaming download link" do
63
+ expect(@session.find_link(download: "other.csv").text).to eq "Download Other"
64
+ end
65
+
66
+ it "raises if passed an invalid value" do
67
+ expect { @session.find_link(download: 37) }.to raise_error ArgumentError
68
+ end
69
+ end
52
70
  end
@@ -206,6 +206,7 @@ Capybara::SpecHelper.spec '#find' do
206
206
 
207
207
  context "with css as default selector" do
208
208
  before { Capybara.default_selector = :css }
209
+
209
210
  it "should find the first element using the given locator" do
210
211
  expect(@session.find('h1').text).to eq('This is a test')
211
212
  expect(@session.find("input[id='test_field']").value).to eq('monkey')
@@ -51,6 +51,7 @@ Capybara::SpecHelper.spec '#first' do
51
51
 
52
52
  context "with css as default selector" do
53
53
  before { Capybara.default_selector = :css }
54
+
54
55
  it "should find the first element using the given locator" do
55
56
  expect(@session.first('h1').text).to eq('This is a test')
56
57
  expect(@session.first("input[id='test_field']").value).to eq('monkey')
@@ -115,12 +115,6 @@ Capybara::SpecHelper.spec '#has_css?' do
115
115
  end
116
116
 
117
117
  it "should allow escapes in the CSS selector" do
118
- if (defined?(TestClass) && @session.is_a?(TestClass)) || @session.driver.is_a?(Capybara::RackTest::Driver)
119
- # Nokogiri doesn't unescape CSS selectors when converting from CSS to XPath
120
- # See: https://github.com/teamcapybara/capybara/issues/1866
121
- # Also: https://github.com/sparklemotion/nokogiri/pull/1646
122
- pending "Current Nokogiri doesn't handle escapes in CSS attribute selectors correctly"
123
- end
124
118
  expect(@session).to have_css('p[data-random="abc\\\\def"]')
125
119
  expect(@session).to have_css("p[data-random='#{Capybara::Selector::CSS.escape('abc\def')}']")
126
120
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara::SpecHelper.spec '#has_style?', requires: [:css] do
4
+ before do
5
+ @session.visit('/with_html')
6
+ end
7
+
8
+ it "should be true if the element has the given style" do
9
+ expect(@session.find(:css, '#first')).to have_style(display: 'block')
10
+ expect(@session.find(:css, '#first').has_style?(display: 'block')).to be true
11
+ expect(@session.find(:css, '#second')).to have_style('display' => 'inline')
12
+ expect(@session.find(:css, '#second').has_style?('display' => 'inline')).to be true
13
+ end
14
+
15
+ it "should be false if the element does not have the given style" do
16
+ expect(@session.find(:css, '#first').has_style?('display' => 'inline')).to be false
17
+ expect(@session.find(:css, '#second').has_style?(display: 'block')).to be false
18
+ end
19
+
20
+ it "allows Regexp for value matching" do
21
+ expect(@session.find(:css, '#first')).to have_style(display: /^bl/)
22
+ expect(@session.find(:css, '#first').has_style?('display' => /^bl/)).to be true
23
+ expect(@session.find(:css, '#first').has_style?(display: /^in/)).to be false
24
+ end
25
+ end