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
@@ -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