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
@@ -9,10 +9,8 @@ RSpec.shared_context "Capybara Features", capybara_feature: true do
9
9
  end
10
10
 
11
11
  # ensure shared_context is included if default shared_context_metadata_behavior is changed
12
- if RSpec::Core::Version::STRING.to_f >= 3.5
13
- RSpec.configure do |config|
14
- config.include_context "Capybara Features", capybara_feature: true
15
- end
12
+ RSpec.configure do |config|
13
+ config.include_context "Capybara Features", capybara_feature: true if config.respond_to?(:include_context)
16
14
  end
17
15
 
18
16
  RSpec.configure do |config|
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'capybara/rspec/compound'
4
+
3
5
  module Capybara
4
6
  module RSpecMatchers
5
7
  class Matcher
6
- if defined?(::RSpec::Expectations::Version)
7
- require 'capybara/rspec/compound'
8
- include ::Capybara::RSpecMatchers::Compound
9
- end
8
+ include ::Capybara::RSpecMatchers::Compound if defined?(::Capybara::RSpecMatchers::Compound)
10
9
 
11
10
  attr_reader :failure_message, :failure_message_when_negated
12
11
 
@@ -199,10 +198,7 @@ module Capybara
199
198
  end
200
199
 
201
200
  class NegatedMatcher
202
- if defined?(::RSpec::Expectations::Version)
203
- require 'capybara/rspec/compound'
204
- include ::Capybara::RSpecMatchers::Compound
205
- end
201
+ include ::Capybara::RSpecMatchers::Compound if defined?(::Capybara::RSpecMatchers::Compound)
206
202
 
207
203
  def initialize(matcher)
208
204
  @matcher = matcher
@@ -229,6 +225,24 @@ module Capybara
229
225
  end
230
226
  end
231
227
 
228
+ class HaveStyle < Matcher
229
+ def initialize(*args)
230
+ @args = args
231
+ end
232
+
233
+ def matches?(actual)
234
+ wrap_matches?(actual) { |el| el.assert_style(*@args) }
235
+ end
236
+
237
+ def does_not_match?(_actual)
238
+ raise ArgumentError, "The have_style matcher does not support use with not_to/should_not"
239
+ end
240
+
241
+ def description
242
+ "have style"
243
+ end
244
+ end
245
+
232
246
  class BecomeClosed
233
247
  def initialize(options)
234
248
  @options = options
@@ -237,9 +251,9 @@ module Capybara
237
251
  def matches?(window)
238
252
  @window = window
239
253
  @wait_time = Capybara::Queries::BaseQuery.wait(@options, window.session.config.default_max_wait_time)
240
- start_time = Capybara::Helpers.monotonic_time
254
+ timer = Capybara::Helpers.timer(expire_in: @wait_time)
241
255
  while window.exists?
242
- return false if (Capybara::Helpers.monotonic_time - start_time) > @wait_time
256
+ return false if timer.expired?
243
257
  sleep 0.05
244
258
  end
245
259
  true
@@ -359,6 +373,12 @@ module Capybara
359
373
  HaveSelector.new(:table, locator, options, &optional_filter_block)
360
374
  end
361
375
 
376
+ # RSpec matcher for element style
377
+ # See {Capybara::Node::Matchers#has_style?}
378
+ def have_style(styles, **options)
379
+ HaveStyle.new(styles, options)
380
+ end
381
+
362
382
  %w[selector css xpath text title current_path link button field checked_field unchecked_field select table].each do |matcher_type|
363
383
  define_method "have_no_#{matcher_type}" do |*args, &optional_filter_block|
364
384
  NegatedMatcher.new(send("have_#{matcher_type}", *args, &optional_filter_block))
@@ -112,6 +112,15 @@ Capybara.add_selector(:link) do
112
112
  href.is_a?(Regexp) ? node[:href].match(href) : true
113
113
  end
114
114
 
115
+ expression_filter(:download, valid_values: [true, false, String]) do |expr, download|
116
+ mod = case download
117
+ when true then XPath.attr(:download)
118
+ when false then !XPath.attr(:download)
119
+ when String then XPath.attr(:download) == download
120
+ end
121
+ expr[mod]
122
+ end
123
+
115
124
  describe do |**options|
116
125
  desc = +""
117
126
  desc << " with href #{options[:href].inspect}" if options[:href]
@@ -16,12 +16,85 @@ module Capybara
16
16
  c =~ %r{[ -/:-~]} ? "\\#{c}" : format("\\%06x", c.ord)
17
17
  end
18
18
 
19
+ def self.split(css)
20
+ Splitter.new.split(css)
21
+ end
22
+
19
23
  S = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}'
20
24
  H = /[0-9a-fA-F]/
21
25
  UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/
22
26
  NONASCII = /[#{S}]/
23
27
  ESCAPE = /#{UNICODE}|\\[ -~#{S}]/
24
- NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/
28
+ NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/
29
+
30
+ class Splitter
31
+ def split(css)
32
+ selectors = []
33
+ StringIO.open(css) do |str|
34
+ selector = ""
35
+ while (c = str.getc)
36
+ case c
37
+ when '['
38
+ selector += parse_square(str)
39
+ when '('
40
+ selector += parse_paren(str)
41
+ when '"', "'"
42
+ selector += parse_string(c, str)
43
+ when '\\'
44
+ selector += c + str.getc
45
+ when ','
46
+ selectors << selector.strip
47
+ selector = ""
48
+ else
49
+ selector += c
50
+ end
51
+ end
52
+ selectors << selector.strip
53
+ end
54
+ selectors
55
+ end
56
+
57
+ private
58
+
59
+ def parse_square(strio)
60
+ parse_block('[', ']', strio)
61
+ end
62
+
63
+ def parse_paren(strio)
64
+ parse_block('(', ')', strio)
65
+ end
66
+
67
+ def parse_block(start, final, strio)
68
+ block = start
69
+ while (c = strio.getc)
70
+ case c
71
+ when final
72
+ return block + c
73
+ when '\\'
74
+ block += c + strio.getc
75
+ when '"', "'"
76
+ block += parse_string(c, strio)
77
+ else
78
+ block += c
79
+ end
80
+ end
81
+ raise ArgumentError, "Invalid CSS Selector - Block end '#{final}' not found"
82
+ end
83
+
84
+ def parse_string(quote, strio)
85
+ string = quote
86
+ while (c = strio.getc)
87
+ string += c
88
+ case c
89
+ when quote
90
+ return string
91
+ when '\\'
92
+ string += strio.getc
93
+ end
94
+ end
95
+ raise ArgumentError, 'Invalid CSS Selector - string end not found'
96
+ end
97
+ end
25
98
  end
26
99
  end
27
100
  end
@@ -49,7 +49,8 @@ module Capybara
49
49
  end
50
50
 
51
51
  def valid_value?(value)
52
- !@options.key?(:valid_values) || Array(@options[:valid_values]).include?(value)
52
+ return true unless @options.key?(:valid_values)
53
+ Array(@options[:valid_values]).any? { |valid| valid === value } # rubocop:disable Style/CaseEquality
53
54
  end
54
55
  end
55
56
  end
@@ -12,8 +12,9 @@ module Capybara
12
12
  end
13
13
 
14
14
  class IdentityExpressionFilter < ExpressionFilter
15
- def initialize; end
15
+ def initialize(name); super(name, nil, nil); end
16
16
  def default?; false; end
17
+ def matcher?; false; end
17
18
  def apply_filter(expr, _name, _value); expr; end
18
19
  end
19
20
  end
@@ -223,7 +223,7 @@ module Capybara
223
223
  def xpath(*allowed_filters, &block)
224
224
  if block
225
225
  @format, @expression = :xpath, block
226
- allowed_filters.flatten.each { |ef| expression_filters[ef] = Filters::IdentityExpressionFilter.new }
226
+ allowed_filters.flatten.each { |ef| expression_filters[ef] = Filters::IdentityExpressionFilter.new(ef) }
227
227
  end
228
228
  format == :xpath ? @expression : nil
229
229
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "uri"
4
- require "English"
3
+ require 'uri'
4
+ require 'English'
5
5
 
6
6
  class Capybara::Selenium::Driver < Capybara::Driver::Base
7
7
  DEFAULT_OPTIONS = {
@@ -10,18 +10,11 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
10
10
  clear_session_storage: false
11
11
  }.freeze
12
12
  SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage].freeze
13
-
14
13
  attr_reader :app, :options
15
14
 
16
15
  def self.load_selenium
17
16
  require 'selenium-webdriver'
18
- # Fix for selenium-webdriver 3.4.0 which misnamed these
19
- unless defined?(::Selenium::WebDriver::Error::ElementNotInteractableError)
20
- ::Selenium::WebDriver::Error.const_set('ElementNotInteractableError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
21
- end
22
- unless defined?(::Selenium::WebDriver::Error::ElementClickInterceptedError)
23
- ::Selenium::WebDriver::Error.const_set('ElementClickInterceptedError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
24
- end
17
+ warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade." if Gem::Version.new(Selenium::WebDriver::VERSION) < Gem::Version.new('3.5.0')
25
18
  rescue LoadError => e
26
19
  raise e if e.message !~ /selenium-webdriver/
27
20
  raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
@@ -37,10 +30,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
37
30
  @processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
38
31
  @browser = Selenium::WebDriver.for(options[:browser], @processed_options)
39
32
 
40
- @w3c = ((defined?(Selenium::WebDriver::Remote::W3CCapabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3CCapabilities)) ||
41
- (defined?(Selenium::WebDriver::Remote::W3C::Capabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3C::Capabilities)))
42
- main = Process.pid
33
+ extend ChromeDriver if chrome?
34
+ extend MarionetteDriver if marionette?
43
35
 
36
+ main = Process.pid
44
37
  at_exit do
45
38
  # Store the exit status of the test run since it goes away after calling the at_exit proc...
46
39
  @exit_status = $ERROR_INFO.status if $ERROR_INFO.is_a?(SystemExit)
@@ -59,6 +52,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
59
52
  @exit_status = nil
60
53
  @frame_handles = {}
61
54
  @options = DEFAULT_OPTIONS.merge(options)
55
+ @node_class = ::Capybara::Selenium::Node
62
56
  end
63
57
 
64
58
  def visit(path)
@@ -90,11 +84,11 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
90
84
  end
91
85
 
92
86
  def find_xpath(selector)
93
- browser.find_elements(:xpath, selector).map { |node| Capybara::Selenium::Node.new(self, node) }
87
+ browser.find_elements(:xpath, selector).map(&method(:build_node))
94
88
  end
95
89
 
96
90
  def find_css(selector)
97
- browser.find_elements(:css, selector).map { |node| Capybara::Selenium::Node.new(self, node) }
91
+ browser.find_elements(:css, selector).map(&method(:build_node))
98
92
  end
99
93
 
100
94
  def wait?; true; end
@@ -123,13 +117,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
123
117
  # Use instance variable directly so we avoid starting the browser just to reset the session
124
118
  return unless @browser
125
119
 
126
- if firefox? || chrome?
127
- switch_to_window(window_handles.first)
128
- window_handles.slice(1..-1).each { |win| close_window(win) }
129
- end
130
-
131
120
  navigated = false
132
- start_time = Capybara::Helpers.monotonic_time
121
+ timer = Capybara::Helpers.timer(expire_in: 10)
133
122
  begin
134
123
  unless navigated
135
124
  # Only trigger a navigation if we haven't done it already, otherwise it
@@ -152,7 +141,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
152
141
 
153
142
  # Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
154
143
  until find_xpath("/html/body/*").empty?
155
- raise Capybara::ExpectationNotMet, 'Timed out waiting for Selenium session reset' if (Capybara::Helpers.monotonic_time - start_time) >= 10
144
+ raise Capybara::ExpectationNotMet, 'Timed out waiting for Selenium session reset' if timer.expired?
156
145
  sleep 0.05
157
146
  end
158
147
  rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
@@ -212,12 +201,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
212
201
 
213
202
  def resize_window_to(handle, width, height)
214
203
  within_given_window(handle) do
215
- # Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
216
- if marionette? && (window_size(handle) == [width, height])
217
- {}
218
- else
219
- browser.manage.window.resize_to(width, height)
220
- end
204
+ browser.manage.window.resize_to(width, height)
221
205
  end
222
206
  end
223
207
 
@@ -228,6 +212,12 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
228
212
  sleep 0.1 # work around for https://code.google.com/p/selenium/issues/detail?id=7405
229
213
  end
230
214
 
215
+ def fullscreen_window(handle)
216
+ within_given_window(handle) do
217
+ browser.manage.window.full_screen
218
+ end
219
+ end
220
+
231
221
  def close_window(handle)
232
222
  raise ArgumentError, "Not allowed to close the primary window" if handle == window_handles.first
233
223
  within_given_window(handle) do
@@ -299,38 +289,36 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
299
289
  Selenium::WebDriver::Error::NoSuchWindowError
300
290
  end
301
291
 
302
- # @api private
292
+ private
293
+
294
+ def w3c?
295
+ browser && browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3C::Capabilities)
296
+ end
297
+
303
298
  def marionette?
304
- firefox? && browser && @w3c
299
+ firefox? && w3c?
305
300
  end
306
301
 
307
- # @api private
308
302
  def firefox?
309
303
  browser_name == :firefox
310
304
  end
311
305
 
312
- # @api private
313
306
  def chrome?
314
307
  browser_name == :chrome
315
308
  end
316
309
 
317
- # @api private
318
310
  def edge?
319
311
  browser_name == :edge
320
312
  end
321
313
 
322
- # @api private
323
314
  def ie?
324
315
  browser_name == :ie
325
316
  end
326
317
 
327
- # @api private
328
318
  def browser_name
329
319
  browser.browser
330
320
  end
331
321
 
332
- private
333
-
334
322
  def native_args(args)
335
323
  args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
336
324
  end
@@ -354,11 +342,7 @@ private
354
342
  end
355
343
 
356
344
  def modal_error
357
- if defined?(Selenium::WebDriver::Error::NoSuchAlertError)
358
- Selenium::WebDriver::Error::NoSuchAlertError
359
- else
360
- Selenium::WebDriver::Error::NoAlertPresentError
361
- end
345
+ Selenium::WebDriver::Error::NoSuchAlertError
362
346
  end
363
347
 
364
348
  def within_given_window(handle)
@@ -406,9 +390,16 @@ private
406
390
  when Hash
407
391
  arg.each { |k, v| arg[k] = unwrap_script_result(v) }
408
392
  when Selenium::WebDriver::Element
409
- Capybara::Selenium::Node.new(self, arg)
393
+ build_node(arg)
410
394
  else
411
395
  arg
412
396
  end
413
397
  end
398
+
399
+ def build_node(native_node)
400
+ ::Capybara::Selenium::Node.new(self, native_node)
401
+ end
414
402
  end
403
+
404
+ require 'capybara/selenium/driver_specializations/chrome_driver'
405
+ require 'capybara/selenium/driver_specializations/marionette_driver'
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Selenium::Driver::ChromeDriver
4
+ def fullscreen_window(handle)
5
+ within_given_window(handle) do
6
+ begin
7
+ super
8
+ rescue NoMethodError => e
9
+ raise unless e.message =~ /full_screen_window/
10
+ bridge = browser.send(:bridge)
11
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
12
+ result['value']
13
+ end
14
+ end
15
+ end
16
+
17
+ def resize_window_to(handle, width, height)
18
+ super
19
+ rescue Selenium::WebDriver::Error::UnknownError => e
20
+ raise unless e.message =~ /failed to change window state/
21
+ # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
22
+ # and raises unnecessary error. Wait a bit and try again.
23
+ sleep 0.5
24
+ super
25
+ end
26
+
27
+ def reset!
28
+ # Use instance variable directly so we avoid starting the browser just to reset the session
29
+ return unless @browser
30
+
31
+ switch_to_window(window_handles.first)
32
+ window_handles.slice(1..-1).each { |win| close_window(win) }
33
+ super
34
+ end
35
+ end