capybara 3.32.2 → 3.35.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +97 -15
  3. data/README.md +9 -4
  4. data/lib/capybara.rb +18 -8
  5. data/lib/capybara/config.rb +4 -6
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/driver/base.rb +4 -0
  8. data/lib/capybara/helpers.rb +25 -1
  9. data/lib/capybara/minitest.rb +2 -3
  10. data/lib/capybara/minitest/spec.rb +14 -11
  11. data/lib/capybara/node/actions.rb +16 -21
  12. data/lib/capybara/node/base.rb +6 -6
  13. data/lib/capybara/node/element.rb +1 -5
  14. data/lib/capybara/node/finders.rb +7 -6
  15. data/lib/capybara/node/matchers.rb +12 -12
  16. data/lib/capybara/node/simple.rb +5 -1
  17. data/lib/capybara/queries/ancestor_query.rb +1 -1
  18. data/lib/capybara/queries/current_path_query.rb +14 -4
  19. data/lib/capybara/queries/selector_query.rb +40 -18
  20. data/lib/capybara/queries/sibling_query.rb +1 -1
  21. data/lib/capybara/queries/style_query.rb +1 -1
  22. data/lib/capybara/queries/text_query.rb +7 -1
  23. data/lib/capybara/rack_test/browser.rb +7 -3
  24. data/lib/capybara/rack_test/driver.rb +1 -0
  25. data/lib/capybara/rack_test/form.rb +1 -1
  26. data/lib/capybara/rack_test/node.rb +1 -1
  27. data/lib/capybara/registration_container.rb +44 -0
  28. data/lib/capybara/registrations/drivers.rb +18 -12
  29. data/lib/capybara/registrations/patches/puma_ssl.rb +3 -1
  30. data/lib/capybara/registrations/servers.rb +2 -1
  31. data/lib/capybara/result.rb +6 -10
  32. data/lib/capybara/rspec.rb +2 -0
  33. data/lib/capybara/rspec/matcher_proxies.rb +1 -1
  34. data/lib/capybara/rspec/matchers.rb +7 -6
  35. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  36. data/lib/capybara/rspec/matchers/match_style.rb +5 -0
  37. data/lib/capybara/selector.rb +12 -3
  38. data/lib/capybara/selector/builders/css_builder.rb +1 -1
  39. data/lib/capybara/selector/builders/xpath_builder.rb +3 -1
  40. data/lib/capybara/selector/definition.rb +11 -9
  41. data/lib/capybara/selector/definition/button.rb +26 -14
  42. data/lib/capybara/selector/definition/css.rb +1 -1
  43. data/lib/capybara/selector/definition/datalist_input.rb +1 -1
  44. data/lib/capybara/selector/definition/element.rb +2 -1
  45. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  46. data/lib/capybara/selector/definition/label.rb +1 -1
  47. data/lib/capybara/selector/definition/link.rb +8 -0
  48. data/lib/capybara/selector/definition/select.rb +1 -1
  49. data/lib/capybara/selector/definition/table.rb +1 -1
  50. data/lib/capybara/selector/definition/table_row.rb +2 -2
  51. data/lib/capybara/selector/filter_set.rb +2 -2
  52. data/lib/capybara/selector/selector.rb +9 -1
  53. data/lib/capybara/selenium/atoms/src/isDisplayed.js +1 -1
  54. data/lib/capybara/selenium/driver.rb +48 -4
  55. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +9 -11
  56. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +9 -11
  57. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +1 -1
  58. data/lib/capybara/selenium/extensions/find.rb +4 -4
  59. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  60. data/lib/capybara/selenium/logger_suppressor.rb +8 -2
  61. data/lib/capybara/selenium/node.rb +9 -5
  62. data/lib/capybara/selenium/nodes/chrome_node.rb +23 -5
  63. data/lib/capybara/selenium/nodes/firefox_node.rb +7 -2
  64. data/lib/capybara/selenium/nodes/safari_node.rb +1 -1
  65. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  66. data/lib/capybara/selenium/patches/logs.rb +7 -9
  67. data/lib/capybara/server/animation_disabler.rb +8 -3
  68. data/lib/capybara/server/middleware.rb +4 -2
  69. data/lib/capybara/session.rb +23 -14
  70. data/lib/capybara/session/config.rb +3 -1
  71. data/lib/capybara/session/matchers.rb +11 -11
  72. data/lib/capybara/spec/public/test.js +13 -1
  73. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  74. data/lib/capybara/spec/session/check_spec.rb +6 -0
  75. data/lib/capybara/spec/session/click_button_spec.rb +11 -0
  76. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  77. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  78. data/lib/capybara/spec/session/has_button_spec.rb +51 -0
  79. data/lib/capybara/spec/session/has_css_spec.rb +2 -1
  80. data/lib/capybara/spec/session/has_current_path_spec.rb +15 -2
  81. data/lib/capybara/spec/session/has_field_spec.rb +16 -0
  82. data/lib/capybara/spec/session/has_select_spec.rb +4 -4
  83. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  84. data/lib/capybara/spec/session/has_text_spec.rb +0 -11
  85. data/lib/capybara/spec/session/html_spec.rb +1 -1
  86. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  87. data/lib/capybara/spec/session/node_spec.rb +29 -9
  88. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  89. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  90. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  91. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  92. data/lib/capybara/spec/session/window/window_spec.rb +1 -1
  93. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  94. data/lib/capybara/spec/spec_helper.rb +12 -12
  95. data/lib/capybara/spec/test_app.rb +23 -21
  96. data/lib/capybara/spec/views/form.erb +25 -2
  97. data/lib/capybara/spec/views/with_animation.erb +8 -0
  98. data/lib/capybara/spec/views/with_dragula.erb +3 -1
  99. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  100. data/lib/capybara/spec/views/with_js.erb +3 -0
  101. data/lib/capybara/spec/views/with_sortable_js.erb +1 -1
  102. data/lib/capybara/version.rb +1 -1
  103. data/lib/capybara/window.rb +3 -7
  104. data/spec/basic_node_spec.rb +9 -8
  105. data/spec/capybara_spec.rb +1 -1
  106. data/spec/dsl_spec.rb +14 -1
  107. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  108. data/spec/minitest_spec.rb +3 -2
  109. data/spec/rack_test_spec.rb +16 -5
  110. data/spec/rspec/features_spec.rb +3 -1
  111. data/spec/rspec/scenarios_spec.rb +4 -0
  112. data/spec/rspec/shared_spec_matchers.rb +61 -49
  113. data/spec/rspec_spec.rb +4 -0
  114. data/spec/selector_spec.rb +17 -2
  115. data/spec/selenium_spec_chrome.rb +39 -20
  116. data/spec/selenium_spec_chrome_remote.rb +5 -1
  117. data/spec/selenium_spec_firefox.rb +15 -13
  118. data/spec/server_spec.rb +60 -49
  119. data/spec/shared_selenium_session.rb +83 -1
  120. data/spec/spec_helper.rb +1 -1
  121. metadata +49 -14
  122. data/lib/capybara/spec/session/source_spec.rb +0 -0
@@ -6,13 +6,15 @@ module Capybara
6
6
  class TextQuery < BaseQuery
7
7
  def initialize(type = nil, expected_text, session_options:, **options) # rubocop:disable Style/OptionalArguments
8
8
  @type = type.nil? ? default_type : type
9
+ raise ArgumentError, "#{@type} is not a valid type for a text query" unless valid_types.include?(@type)
10
+
9
11
  @options = options
10
12
  super(@options)
11
13
  self.session_options = session_options
12
14
 
13
15
  if expected_text.nil? && !exact?
14
16
  warn 'Checking for expected text of nil is confusing and/or pointless since it will always match. '\
15
- 'Please specify a string or regexp instead.'
17
+ "Please specify a string or regexp instead. #{Capybara::Helpers.filter_backtrace(caller)}"
16
18
  end
17
19
 
18
20
  @expected_text = expected_text.is_a?(Regexp) ? expected_text : expected_text.to_s
@@ -89,6 +91,10 @@ module Capybara
89
91
  COUNT_KEYS + %i[wait exact normalize_ws]
90
92
  end
91
93
 
94
+ def valid_types
95
+ %i[all visible]
96
+ end
97
+
92
98
  def check_visible_text?
93
99
  @type == :visible
94
100
  end
@@ -8,6 +8,7 @@ class Capybara::RackTest::Browser
8
8
 
9
9
  def initialize(driver)
10
10
  @driver = driver
11
+ @current_fragment = nil
11
12
  end
12
13
 
13
14
  def app
@@ -31,7 +32,7 @@ class Capybara::RackTest::Browser
31
32
  def submit(method, path, attributes)
32
33
  path = request_path if path.nil? || path.empty?
33
34
  uri = build_uri(path)
34
- uri.query = '' if method&.to_s&.downcase == 'get'
35
+ uri.query = '' if method.to_s.casecmp('get').zero?
35
36
  process_and_follow_redirects(method, uri.to_s, attributes, 'HTTP_REFERER' => current_url)
36
37
  end
37
38
 
@@ -42,6 +43,7 @@ class Capybara::RackTest::Browser
42
43
  end
43
44
 
44
45
  def process_and_follow_redirects(method, path, attributes = {}, env = {})
46
+ @current_fragment = build_uri(path).fragment
45
47
  process(method, path, attributes, env)
46
48
 
47
49
  return unless driver.follow_redirects?
@@ -65,7 +67,7 @@ class Capybara::RackTest::Browser
65
67
  method = method.downcase
66
68
  new_uri = build_uri(path)
67
69
  @current_scheme, @current_host, @current_port = new_uri.select(:scheme, :host, :port)
68
-
70
+ @current_fragment = new_uri.fragment || @current_fragment
69
71
  reset_cache!
70
72
  send(method, new_uri.to_s, attributes, env.merge(options[:headers] || {}))
71
73
  end
@@ -83,7 +85,9 @@ class Capybara::RackTest::Browser
83
85
  end
84
86
 
85
87
  def current_url
86
- last_request.url
88
+ uri = build_uri(last_request.url)
89
+ uri.fragment = @current_fragment if @current_fragment
90
+ uri.to_s
87
91
  rescue Rack::Test::Error
88
92
  ''
89
93
  end
@@ -17,6 +17,7 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
17
17
  def initialize(app, **options)
18
18
  raise ArgumentError, 'rack-test requires a rack application, but none was given' unless app
19
19
 
20
+ super()
20
21
  @app = app
21
22
  @options = DEFAULT_OPTIONS.merge(options)
22
23
  end
@@ -6,7 +6,7 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
6
6
  # That check should be based solely on the form element's 'enctype' attribute value,
7
7
  # which should probably be provided to Rack::Test in its non-GET request methods.
8
8
  class NilUploadedFile < Rack::Test::UploadedFile
9
- def initialize
9
+ def initialize # rubocop:disable Lint/MissingSuper
10
10
  @empty_file = Tempfile.new('nil_uploaded_file')
11
11
  @empty_file.close
12
12
  end
@@ -15,7 +15,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
15
15
  end
16
16
 
17
17
  def visible_text
18
- displayed_text.gsub(/\ +/, ' ')
18
+ displayed_text.squeeze(' ')
19
19
  .gsub(/[\ \n]*\n[\ \n]*/, "\n")
20
20
  .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
21
21
  .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ # @api private
5
+ class RegistrationContainer
6
+ def names
7
+ @registered.keys
8
+ end
9
+
10
+ def [](name)
11
+ @registered[name]
12
+ end
13
+
14
+ def []=(name, value)
15
+ Capybara::Helpers.warn 'DEPRECATED: Directly setting drivers/servers is deprecated, please use Capybara.register_driver/register_server instead'
16
+ @registered[name] = value
17
+ end
18
+
19
+ def method_missing(method_name, *args, **options, &block)
20
+ if @registered.respond_to?(method_name)
21
+ Capybara::Helpers.warn "DEPRECATED: Calling '#{method_name}' on the drivers/servers container is deprecated without replacement"
22
+ # RUBY 2.6 will send an empty hash rather than nothing with **options so fix that
23
+ return @registered.public_send(method_name, *args, &block) if options.empty?
24
+
25
+ return @registered.public_send(method_name, *args, **options, &block)
26
+ end
27
+ super
28
+ end
29
+
30
+ def respond_to_missing?(method_name, include_all)
31
+ @registered.respond_to?(method_name) || super
32
+ end
33
+
34
+ private
35
+
36
+ def initialize
37
+ @registered = {}
38
+ end
39
+
40
+ def register(name, block)
41
+ @registered[name] = block
42
+ end
43
+ end
44
+ end
@@ -9,28 +9,34 @@ Capybara.register_driver :selenium do |app|
9
9
  end
10
10
 
11
11
  Capybara.register_driver :selenium_headless do |app|
12
- Capybara::Selenium::Driver.load_selenium
13
- browser_options = ::Selenium::WebDriver::Firefox::Options.new
14
- browser_options.args << '-headless'
15
- Capybara::Selenium::Driver.new(app, browser: :firefox, options: browser_options)
12
+ version = Capybara::Selenium::Driver.load_selenium
13
+ options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
14
+ browser_options = ::Selenium::WebDriver::Firefox::Options.new.tap do |opts|
15
+ opts.add_argument '-headless'
16
+ end
17
+ Capybara::Selenium::Driver.new(app, **Hash[:browser => :firefox, options_key => browser_options])
16
18
  end
17
19
 
18
20
  Capybara.register_driver :selenium_chrome do |app|
19
- Capybara::Selenium::Driver.load_selenium
21
+ version = Capybara::Selenium::Driver.load_selenium
22
+ options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
20
23
  browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
21
24
  # Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary
22
- opts.args << '--disable-site-isolation-trials'
25
+ opts.add_argument('--disable-site-isolation-trials')
23
26
  end
24
- Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options)
27
+
28
+ Capybara::Selenium::Driver.new(app, **Hash[:browser => :chrome, options_key => browser_options])
25
29
  end
26
30
 
27
31
  Capybara.register_driver :selenium_chrome_headless do |app|
28
- Capybara::Selenium::Driver.load_selenium
32
+ version = Capybara::Selenium::Driver.load_selenium
33
+ options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
29
34
  browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
30
- opts.args << '--headless'
31
- opts.args << '--disable-gpu' if Gem.win_platform?
35
+ opts.add_argument('--headless')
36
+ opts.add_argument('--disable-gpu') if Gem.win_platform?
32
37
  # Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary
33
- opts.args << '--disable-site-isolation-trials'
38
+ opts.add_argument('--disable-site-isolation-trials')
34
39
  end
35
- Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options)
40
+
41
+ Capybara::Selenium::Driver.new(app, **Hash[:browser => :chrome, options_key => browser_options])
36
42
  end
@@ -4,12 +4,14 @@ module Puma
4
4
  module MiniSSL
5
5
  class Socket
6
6
  def read_nonblock(size, *_)
7
+ wait_states = %i[wait_readable wait_writable]
8
+
7
9
  loop do
8
10
  output = engine_read_all
9
11
  return output if output
10
12
 
11
13
  data = @socket.read_nonblock(size, exception: false)
12
- raise IO::EAGAINWaitReadable if %i[wait_readable wait_writable].include? data
14
+ raise IO::EAGAINWaitReadable if wait_states.include? data
13
15
  return nil if data.nil?
14
16
 
15
17
  @engine.inject(data)
@@ -28,10 +28,11 @@ Capybara.register_server :puma do |app, port, host, **options|
28
28
  options = default_options.merge(options)
29
29
 
30
30
  conf = Rack::Handler::Puma.config(app, options)
31
+ conf.clamp
31
32
  events = conf.options[:Silent] ? ::Puma::Events.strings : ::Puma::Events.stdio
32
33
 
33
34
  puma_ver = Gem::Version.new(Puma::Const::PUMA_VERSION)
34
- require_relative 'patches/puma_ssl' if (Gem::Version.new('4.0.0')...Gem::Version.new('4.1.0')).cover? puma_ver
35
+ require_relative 'patches/puma_ssl' if Gem::Requirement.new('>=4.0.0', '< 4.1.0').satisfied_by?(puma_ver)
35
36
 
36
37
  events.log 'Capybara starting Puma...'
37
38
  events.log "* Version #{Puma::Const::PUMA_VERSION} , codename: #{Puma::Const::CODE_NAME}"
@@ -39,7 +39,7 @@ module Capybara
39
39
  alias index find_index
40
40
 
41
41
  def each(&block)
42
- return enum_for(:each) unless block_given?
42
+ return enum_for(:each) unless block
43
43
 
44
44
  @result_cache.each(&block)
45
45
  loop do
@@ -54,10 +54,10 @@ module Capybara
54
54
  idx, length = args
55
55
  max_idx = case idx
56
56
  when Integer
57
- if !idx.negative?
58
- length.nil? ? idx : idx + length - 1
59
- else
57
+ if idx.negative?
60
58
  nil
59
+ else
60
+ length.nil? ? idx : idx + length - 1
61
61
  end
62
62
  when Range
63
63
  # idx.max is broken with beginless ranges
@@ -91,13 +91,9 @@ module Capybara
91
91
  return load_up_to(count + 1) <=> count
92
92
  end
93
93
 
94
- if min && (min = Integer(min))
95
- return -1 if load_up_to(min) < min
96
- end
94
+ return -1 if min && (min = Integer(min)) && (load_up_to(min) < min)
97
95
 
98
- if max && (max = Integer(max))
99
- return 1 if load_up_to(max + 1) > max
100
- end
96
+ return 1 if max && (max = Integer(max)) && (load_up_to(max + 1) > max)
101
97
 
102
98
  if between
103
99
  min, max = (between.begin && between.min) || 1, between.end
@@ -9,6 +9,8 @@ require 'capybara/rspec/matcher_proxies'
9
9
  RSpec.configure do |config|
10
10
  config.include Capybara::DSL, type: :feature
11
11
  config.include Capybara::RSpecMatchers, type: :feature
12
+ config.include Capybara::DSL, type: :system
13
+ config.include Capybara::RSpecMatchers, type: :system
12
14
  config.include Capybara::RSpecMatchers, type: :view
13
15
 
14
16
  # The before and after blocks must run instantaneously, because Capybara
@@ -11,7 +11,7 @@ module Capybara
11
11
  end
12
12
 
13
13
  def within(*args, **kwargs, &block)
14
- if block_given?
14
+ if block
15
15
  within_element(*args, **kwargs, &block)
16
16
  else
17
17
  be_within(*args)
@@ -94,7 +94,7 @@ module Capybara
94
94
  # @see Capybara::Node::Matchers#has_button?
95
95
 
96
96
  # @!method have_field(locator = nil, **options, &optional_filter_block)
97
- # RSpec matcher for links.
97
+ # RSpec matcher for form fields.
98
98
  #
99
99
  # @see Capybara::Node::Matchers#has_field?
100
100
 
@@ -139,22 +139,23 @@ module Capybara
139
139
  # RSpec matcher for the current path.
140
140
  #
141
141
  # @see Capybara::SessionMatchers#assert_current_path
142
- def have_current_path(path, **options)
143
- Matchers::HaveCurrentPath.new(path, **options)
142
+ def have_current_path(path, **options, &optional_filter_block)
143
+ Matchers::HaveCurrentPath.new(path, **options, &optional_filter_block)
144
144
  end
145
145
 
146
146
  # RSpec matcher for element style.
147
147
  #
148
148
  # @see Capybara::Node::Matchers#matches_style?
149
- def match_style(styles, **options)
149
+ def match_style(styles = nil, **options)
150
+ styles, options = options, {} if styles.nil?
150
151
  Matchers::MatchStyle.new(styles, **options)
151
152
  end
152
153
 
153
154
  ##
154
155
  # @deprecated
155
156
  #
156
- def have_style(styles, **options)
157
- warn 'DEPRECATED: have_style is deprecated, please use match_style'
157
+ def have_style(styles = nil, **options)
158
+ Capybara::Helpers.warn "DEPRECATED: have_style is deprecated, please use match_style : #{Capybara::Helpers.filter_backtrace(caller)}"
158
159
  match_style(styles, **options)
159
160
  end
160
161
 
@@ -7,11 +7,11 @@ module Capybara
7
7
  module Matchers
8
8
  class HaveCurrentPath < WrappedElementMatcher
9
9
  def element_matches?(el)
10
- el.assert_current_path(current_path, **@kw_args)
10
+ el.assert_current_path(current_path, **@kw_args, &@filter_block)
11
11
  end
12
12
 
13
13
  def element_does_not_match?(el)
14
- el.assert_no_current_path(current_path, **@kw_args)
14
+ el.assert_no_current_path(current_path, **@kw_args, &@filter_block)
15
15
  end
16
16
 
17
17
  def description
@@ -6,6 +6,11 @@ module Capybara
6
6
  module RSpecMatchers
7
7
  module Matchers
8
8
  class MatchStyle < WrappedElementMatcher
9
+ def initialize(styles = nil, **kw_args, &filter_block)
10
+ styles, kw_args = kw_args, {} if styles.nil?
11
+ super(styles, **kw_args, &filter_block)
12
+ end
13
+
9
14
  def element_matches?(el)
10
15
  el.assert_matches_style(*@args, **@kw_args)
11
16
  end
@@ -7,7 +7,7 @@ require 'capybara/selector/definition'
7
7
  #
8
8
  # All Selectors below support the listed selector specific filters in addition to the following system-wide filters
9
9
  # * :id (String, Regexp, XPath::Expression) - Matches the id attribute
10
- # * :class (String, Array<String>, Regexp, XPath::Expression) - Matches the class(es) provided
10
+ # * :class (String, Array<String | Regexp>, Regexp, XPath::Expression) - Matches the class(es) provided
11
11
  # * :style (String, Regexp, Hash<String, String>) - Match on elements style
12
12
  # * :above (Element) - Match elements above the passed element on the page
13
13
  # * :below (Element) - Match elements below the passed element on the page
@@ -40,6 +40,7 @@ require 'capybara/selector/definition'
40
40
  # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
41
41
  # * :multiple (Boolean) - Match fields that accept multiple values
42
42
  # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
43
+ # * :validation_message (String, Regexp) - Matches the elements current validationMessage
43
44
  #
44
45
  # * **:fieldset** - Select fieldset elements
45
46
  # * Locator: Matches id, {Capybara.configure test_id}, or contents of wrapped legend
@@ -79,6 +80,7 @@ require 'capybara/selector/definition'
79
80
  # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
80
81
  # * :multiple (Boolean) - Match fields that accept multiple values
81
82
  # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
83
+ # * :validation_message (String, Regexp) - Matches the elements current validationMessage
82
84
  #
83
85
  # * **:radio_button** - Find radio buttons
84
86
  # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
@@ -169,7 +171,7 @@ require 'capybara/selector/definition'
169
171
  # * Filters:
170
172
  # * :\<any> (String, Regexp) - Match on any specified element attribute
171
173
  #
172
- class Capybara::Selector; end
174
+ class Capybara::Selector; end # rubocop:disable Lint/EmptyClass
173
175
 
174
176
  Capybara::Selector::FilterSet.add(:_field) do
175
177
  node_filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }
@@ -178,6 +180,12 @@ Capybara::Selector::FilterSet.add(:_field) do
178
180
  node_filter(:valid, :boolean) { |node, value| node.evaluate_script('this.validity.valid') == value }
179
181
  node_filter(:name) { |node, value| !value.is_a?(Regexp) || value.match?(node[:name]) }
180
182
  node_filter(:placeholder) { |node, value| !value.is_a?(Regexp) || value.match?(node[:placeholder]) }
183
+ node_filter(:validation_message) do |node, msg|
184
+ vm = node[:validationMessage]
185
+ (msg.is_a?(Regexp) ? msg.match?(vm) : vm == msg.to_s).tap do |res|
186
+ add_error("Expected validation message to be #{msg.inspect} but was #{vm}") unless res
187
+ end
188
+ end
181
189
 
182
190
  expression_filter(:name) do |xpath, val|
183
191
  builder(xpath).add_attribute_conditions(name: val)
@@ -198,7 +206,7 @@ Capybara::Selector::FilterSet.add(:_field) do
198
206
  desc
199
207
  end
200
208
 
201
- describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, valid: nil, **|
209
+ describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, valid: nil, validation_message: nil, **|
202
210
  desc, states = +'', []
203
211
  states << 'checked' if checked || (unchecked == false)
204
212
  states << 'not checked' if unchecked || (checked == false)
@@ -206,6 +214,7 @@ Capybara::Selector::FilterSet.add(:_field) do
206
214
  desc << " that is #{states.join(' and ')}" unless states.empty?
207
215
  desc << ' that is valid' if valid == true
208
216
  desc << ' that is invalid' if valid == false
217
+ desc << " with validation message #{validation_message.to_s.inspect}" if validation_message
209
218
  desc
210
219
  end
211
220
  end
@@ -74,7 +74,7 @@ module Capybara
74
74
  end.join
75
75
  end
76
76
  else
77
- cls = Array(classes).group_by { |cl| cl.match?(/^!(?!!!)/) }
77
+ cls = Array(classes).reject { |c| c.is_a? Regexp }.group_by { |cl| cl.match?(/^!(?!!!)/) }
78
78
  [(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
79
79
  cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join]
80
80
  end
@@ -15,6 +15,8 @@ module Capybara
15
15
  def add_attribute_conditions(**conditions)
16
16
  @expression = conditions.inject(expression) do |xp, (name, value)|
17
17
  conditions = name == :class ? class_conditions(value) : attribute_conditions(name => value)
18
+ return xp if conditions.nil?
19
+
18
20
  if xp.is_a? XPath::Expression
19
21
  xp[conditions]
20
22
  else
@@ -47,7 +49,7 @@ module Capybara
47
49
  when XPath::Expression, Regexp
48
50
  attribute_conditions(class: classes)
49
51
  else
50
- Array(classes).map do |klass|
52
+ Array(classes).reject { |c| c.is_a? Regexp }.map do |klass|
51
53
  if klass.match?(/^!(?!!!)/)
52
54
  !XPath.attr(:class).contains_word(klass.slice(1..-1))
53
55
  else
@@ -10,11 +10,12 @@ module Capybara
10
10
  class Selector
11
11
  class Definition
12
12
  attr_reader :name, :expressions
13
+
13
14
  extend Forwardable
14
15
 
15
16
  def initialize(name, locator_type: nil, raw_locator: false, supports_exact: nil, &block)
16
17
  @name = name
17
- @filter_set = Capybara::Selector::FilterSet.add(name) {}
18
+ @filter_set = Capybara::Selector::FilterSet.add(name)
18
19
  @match = nil
19
20
  @label = nil
20
21
  @failure_message = nil
@@ -82,7 +83,7 @@ module Capybara
82
83
  # Automatic selector detection
83
84
  #
84
85
  # @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
85
- # @yieldparam [String], locator The locator string used to determin if it matches the selector
86
+ # @yieldparam [String], locator The locator string used to determine if it matches the selector
86
87
  # @yieldreturn [Boolean] Whether this selector matches the locator string
87
88
  # @return [#call] The block that will be used to detect selector match
88
89
  #
@@ -177,7 +178,7 @@ module Capybara
177
178
  def_delegator :@filter_set, :describe
178
179
 
179
180
  def describe_expression_filters(&block)
180
- if block_given?
181
+ if block
181
182
  describe(:expression_filters, &block)
182
183
  else
183
184
  describe(:expression_filters) do |**options|
@@ -189,7 +190,7 @@ module Capybara
189
190
  def describe_all_expression_filters(**opts)
190
191
  expression_filters.map do |ef_name, ef|
191
192
  if ef.matcher?
192
- handled_custom_keys(ef, opts.keys).map { |key| " with #{ef_name}[#{key} => #{opts[key]}]" }.join
193
+ handled_custom_options(ef, opts).map { |option, value| " with #{ef_name}[#{option} => #{value}]" }.join
193
194
  elsif opts.key?(ef_name)
194
195
  " with #{ef_name} #{opts[ef_name]}"
195
196
  end
@@ -214,7 +215,7 @@ module Capybara
214
215
  end
215
216
 
216
217
  def default_visibility(fallback = Capybara.ignore_hidden_elements, options = {})
217
- vis = if @default_visibility&.respond_to?(:call)
218
+ vis = if @default_visibility.respond_to?(:call)
218
219
  @default_visibility.call(options)
219
220
  else
220
221
  @default_visibility
@@ -251,14 +252,15 @@ module Capybara
251
252
 
252
253
  private
253
254
 
254
- def handled_custom_keys(filter, keys)
255
- keys.select do |key|
256
- filter.handles_option?(key) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(key)
255
+ def handled_custom_options(filter, options)
256
+ options.select do |option, _|
257
+ filter.handles_option?(option) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(option)
257
258
  end
258
259
  end
259
260
 
260
261
  def parameter_names(block)
261
- block.parameters.select { |(type, _name)| %i[key keyreq].include? type }.map { |(_type, name)| name }
262
+ key_types = %i[key keyreq]
263
+ block.parameters.select { |(type, _name)| key_types.include? type }.map { |(_type, name)| name }
262
264
  end
263
265
 
264
266
  def expression(type, allowed_filters, &block)