capybara 3.2.1 → 3.3.0

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