capybara 3.30.0 → 3.35.3

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 (146) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +153 -13
  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/dsl.rb +10 -2
  9. data/lib/capybara/helpers.rb +25 -1
  10. data/lib/capybara/minitest.rb +232 -144
  11. data/lib/capybara/minitest/spec.rb +156 -97
  12. data/lib/capybara/node/actions.rb +16 -21
  13. data/lib/capybara/node/base.rb +6 -6
  14. data/lib/capybara/node/element.rb +14 -13
  15. data/lib/capybara/node/finders.rb +12 -7
  16. data/lib/capybara/node/matchers.rb +36 -27
  17. data/lib/capybara/node/simple.rb +6 -2
  18. data/lib/capybara/queries/ancestor_query.rb +1 -1
  19. data/lib/capybara/queries/base_query.rb +2 -1
  20. data/lib/capybara/queries/current_path_query.rb +14 -4
  21. data/lib/capybara/queries/selector_query.rb +40 -18
  22. data/lib/capybara/queries/sibling_query.rb +1 -1
  23. data/lib/capybara/queries/style_query.rb +1 -1
  24. data/lib/capybara/queries/text_query.rb +7 -1
  25. data/lib/capybara/rack_test/browser.rb +9 -3
  26. data/lib/capybara/rack_test/driver.rb +1 -0
  27. data/lib/capybara/rack_test/form.rb +1 -1
  28. data/lib/capybara/rack_test/node.rb +35 -10
  29. data/lib/capybara/registration_container.rb +44 -0
  30. data/lib/capybara/registrations/drivers.rb +18 -12
  31. data/lib/capybara/registrations/patches/puma_ssl.rb +3 -1
  32. data/lib/capybara/registrations/servers.rb +3 -2
  33. data/lib/capybara/result.rb +35 -15
  34. data/lib/capybara/rspec.rb +2 -0
  35. data/lib/capybara/rspec/matcher_proxies.rb +5 -5
  36. data/lib/capybara/rspec/matchers.rb +33 -32
  37. data/lib/capybara/rspec/matchers/base.rb +12 -6
  38. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  39. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  40. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  41. data/lib/capybara/rspec/matchers/have_selector.rb +15 -7
  42. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  43. data/lib/capybara/rspec/matchers/have_text.rb +3 -3
  44. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  45. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  46. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  47. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  48. data/lib/capybara/selector.rb +14 -3
  49. data/lib/capybara/selector/builders/css_builder.rb +1 -1
  50. data/lib/capybara/selector/builders/xpath_builder.rb +3 -1
  51. data/lib/capybara/selector/definition.rb +11 -9
  52. data/lib/capybara/selector/definition/button.rb +26 -14
  53. data/lib/capybara/selector/definition/css.rb +1 -1
  54. data/lib/capybara/selector/definition/datalist_input.rb +1 -1
  55. data/lib/capybara/selector/definition/element.rb +2 -1
  56. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  57. data/lib/capybara/selector/definition/label.rb +2 -2
  58. data/lib/capybara/selector/definition/link.rb +8 -0
  59. data/lib/capybara/selector/definition/select.rb +32 -13
  60. data/lib/capybara/selector/definition/table.rb +1 -1
  61. data/lib/capybara/selector/definition/table_row.rb +2 -2
  62. data/lib/capybara/selector/filter_set.rb +2 -2
  63. data/lib/capybara/selector/selector.rb +9 -1
  64. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  65. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  66. data/lib/capybara/selenium/atoms/src/isDisplayed.js +1 -1
  67. data/lib/capybara/selenium/driver.rb +52 -7
  68. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +10 -12
  69. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +9 -11
  70. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +3 -3
  71. data/lib/capybara/selenium/extensions/find.rb +4 -4
  72. data/lib/capybara/selenium/extensions/html5_drag.rb +24 -8
  73. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  74. data/lib/capybara/selenium/logger_suppressor.rb +8 -2
  75. data/lib/capybara/selenium/node.rb +96 -16
  76. data/lib/capybara/selenium/nodes/chrome_node.rb +27 -16
  77. data/lib/capybara/selenium/nodes/edge_node.rb +1 -1
  78. data/lib/capybara/selenium/nodes/firefox_node.rb +9 -4
  79. data/lib/capybara/selenium/nodes/safari_node.rb +1 -1
  80. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  81. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  82. data/lib/capybara/selenium/patches/logs.rb +7 -9
  83. data/lib/capybara/server/animation_disabler.rb +8 -3
  84. data/lib/capybara/server/middleware.rb +4 -2
  85. data/lib/capybara/session.rb +53 -29
  86. data/lib/capybara/session/config.rb +3 -1
  87. data/lib/capybara/session/matchers.rb +11 -11
  88. data/lib/capybara/spec/public/test.js +64 -7
  89. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  90. data/lib/capybara/spec/session/all_spec.rb +45 -5
  91. data/lib/capybara/spec/session/assert_text_spec.rb +5 -5
  92. data/lib/capybara/spec/session/check_spec.rb +6 -0
  93. data/lib/capybara/spec/session/click_button_spec.rb +11 -0
  94. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  95. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  96. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  97. data/lib/capybara/spec/session/find_spec.rb +11 -8
  98. data/lib/capybara/spec/session/has_button_spec.rb +51 -0
  99. data/lib/capybara/spec/session/has_css_spec.rb +14 -10
  100. data/lib/capybara/spec/session/has_current_path_spec.rb +15 -2
  101. data/lib/capybara/spec/session/has_field_spec.rb +16 -0
  102. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  103. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  104. data/lib/capybara/spec/session/has_text_spec.rb +5 -12
  105. data/lib/capybara/spec/session/html_spec.rb +1 -1
  106. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  107. data/lib/capybara/spec/session/node_spec.rb +169 -33
  108. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  109. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  110. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  111. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  112. data/lib/capybara/spec/session/window/window_spec.rb +8 -8
  113. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  114. data/lib/capybara/spec/spec_helper.rb +13 -14
  115. data/lib/capybara/spec/test_app.rb +23 -21
  116. data/lib/capybara/spec/views/form.erb +36 -3
  117. data/lib/capybara/spec/views/with_animation.erb +8 -0
  118. data/lib/capybara/spec/views/with_dragula.erb +3 -1
  119. data/lib/capybara/spec/views/with_html.erb +2 -2
  120. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  121. data/lib/capybara/spec/views/with_js.erb +3 -0
  122. data/lib/capybara/spec/views/with_sortable_js.erb +1 -1
  123. data/lib/capybara/version.rb +1 -1
  124. data/lib/capybara/window.rb +3 -7
  125. data/spec/basic_node_spec.rb +9 -8
  126. data/spec/capybara_spec.rb +1 -1
  127. data/spec/dsl_spec.rb +14 -1
  128. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  129. data/spec/minitest_spec.rb +3 -2
  130. data/spec/rack_test_spec.rb +28 -6
  131. data/spec/regexp_dissassembler_spec.rb +0 -4
  132. data/spec/result_spec.rb +40 -29
  133. data/spec/rspec/features_spec.rb +3 -1
  134. data/spec/rspec/scenarios_spec.rb +4 -0
  135. data/spec/rspec/shared_spec_matchers.rb +63 -51
  136. data/spec/rspec_spec.rb +4 -0
  137. data/spec/selector_spec.rb +17 -2
  138. data/spec/selenium_spec_chrome.rb +45 -21
  139. data/spec/selenium_spec_chrome_remote.rb +7 -1
  140. data/spec/selenium_spec_firefox.rb +15 -13
  141. data/spec/server_spec.rb +60 -49
  142. data/spec/shared_selenium_node.rb +18 -0
  143. data/spec/shared_selenium_session.rb +98 -7
  144. data/spec/spec_helper.rb +1 -1
  145. metadata +50 -14
  146. data/lib/capybara/spec/session/source_spec.rb +0 -0
@@ -12,22 +12,28 @@ module Capybara
12
12
 
13
13
  attr_reader :failure_message, :failure_message_when_negated
14
14
 
15
- def initialize(*args, &filter_block)
15
+ def initialize(*args, **kw_args, &filter_block)
16
16
  @args = args.dup
17
+ @kw_args = kw_args || {}
17
18
  @filter_block = filter_block
18
19
  end
19
20
 
20
21
  private
21
22
 
22
23
  def session_query_args
23
- if @args.last.is_a? Hash
24
- @args.last[:session_options] = session_options
25
- else
26
- @args.push(session_options: session_options)
27
- end
24
+ # if @args.last.is_a? Hash
25
+ # @args.last[:session_options] = session_options
26
+ # else
27
+ # @args.push(session_options: session_options)
28
+ # end
28
29
  @args
29
30
  end
30
31
 
32
+ def session_query_options
33
+ @kw_args[:session_options] = session_options
34
+ @kw_args
35
+ end
36
+
31
37
  def session_options
32
38
  @context_el ||= nil
33
39
  if @context_el.respond_to? :session_options
@@ -29,7 +29,8 @@ module Capybara
29
29
  private
30
30
 
31
31
  def options
32
- (@args.last.is_a?(Hash) ? @args : @args.push({})).last
32
+ # (@args.last.is_a?(Hash) ? @args : @args.push({})).last
33
+ @kw_args
33
34
  end
34
35
  end
35
36
  end
@@ -7,11 +7,11 @@ module Capybara
7
7
  module Matchers
8
8
  class HaveAncestor < CountableWrappedElementMatcher
9
9
  def element_matches?(el)
10
- el.assert_ancestor(*@args, &@filter_block)
10
+ el.assert_ancestor(*@args, **session_query_options, &@filter_block)
11
11
  end
12
12
 
13
13
  def element_does_not_match?(el)
14
- el.assert_no_ancestor(*@args, &@filter_block)
14
+ el.assert_no_ancestor(*@args, **session_query_options, &@filter_block)
15
15
  end
16
16
 
17
17
  def description
@@ -19,7 +19,8 @@ module Capybara
19
19
  end
20
20
 
21
21
  def query
22
- @query ||= Capybara::Queries::AncestorQuery.new(*session_query_args, &@filter_block)
22
+ # @query ||= Capybara::Queries::AncestorQuery.new(*session_query_args, &@filter_block)
23
+ @query ||= Capybara::Queries::AncestorQuery.new(*session_query_args, **session_query_options, &@filter_block)
23
24
  end
24
25
  end
25
26
  end
@@ -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(*@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(*@args)
14
+ el.assert_no_current_path(current_path, **@kw_args, &@filter_block)
15
15
  end
16
16
 
17
17
  def description
@@ -6,12 +6,20 @@ module Capybara
6
6
  module RSpecMatchers
7
7
  module Matchers
8
8
  class HaveSelector < CountableWrappedElementMatcher
9
+ def initialize(*args, **kw_args, &filter_block)
10
+ super
11
+ if (RUBY_VERSION >= '2.7') && (@args.size < 2) && @kw_args.keys.any?(String) # rubocop:disable Style/GuardClause
12
+ @args.push(@kw_args)
13
+ @kw_args = {}
14
+ end
15
+ end
16
+
9
17
  def element_matches?(el)
10
- el.assert_selector(*@args, &@filter_block)
18
+ el.assert_selector(*@args, **session_query_options, &@filter_block)
11
19
  end
12
20
 
13
21
  def element_does_not_match?(el)
14
- el.assert_no_selector(*@args, &@filter_block)
22
+ el.assert_no_selector(*@args, **session_query_options, &@filter_block)
15
23
  end
16
24
 
17
25
  def description
@@ -19,13 +27,13 @@ module Capybara
19
27
  end
20
28
 
21
29
  def query
22
- @query ||= Capybara::Queries::SelectorQuery.new(*session_query_args, &@filter_block)
30
+ @query ||= Capybara::Queries::SelectorQuery.new(*session_query_args, **session_query_options, &@filter_block)
23
31
  end
24
32
  end
25
33
 
26
34
  class HaveAllSelectors < WrappedElementMatcher
27
35
  def element_matches?(el)
28
- el.assert_all_of_selectors(*@args, &@filter_block)
36
+ el.assert_all_of_selectors(*@args, **session_query_options, &@filter_block)
29
37
  end
30
38
 
31
39
  def does_not_match?(_actual)
@@ -39,7 +47,7 @@ module Capybara
39
47
 
40
48
  class HaveNoSelectors < WrappedElementMatcher
41
49
  def element_matches?(el)
42
- el.assert_none_of_selectors(*@args, &@filter_block)
50
+ el.assert_none_of_selectors(*@args, **session_query_options, &@filter_block)
43
51
  end
44
52
 
45
53
  def does_not_match?(_actual)
@@ -53,11 +61,11 @@ module Capybara
53
61
 
54
62
  class HaveAnySelectors < WrappedElementMatcher
55
63
  def element_matches?(el)
56
- el.assert_any_of_selectors(*@args, &@filter_block)
64
+ el.assert_any_of_selectors(*@args, **session_query_options, &@filter_block)
57
65
  end
58
66
 
59
67
  def does_not_match?(_actual)
60
- el.assert_none_of_selectors(*@args, &@filter_block)
68
+ el.assert_none_of_selectors(*@args, **session_query_options, &@filter_block)
61
69
  end
62
70
 
63
71
  def description
@@ -7,11 +7,11 @@ module Capybara
7
7
  module Matchers
8
8
  class HaveSibling < CountableWrappedElementMatcher
9
9
  def element_matches?(el)
10
- el.assert_sibling(*@args, &@filter_block)
10
+ el.assert_sibling(*@args, **session_query_options, &@filter_block)
11
11
  end
12
12
 
13
13
  def element_does_not_match?(el)
14
- el.assert_no_sibling(*@args, &@filter_block)
14
+ el.assert_no_sibling(*@args, **session_query_options, &@filter_block)
15
15
  end
16
16
 
17
17
  def description
@@ -19,7 +19,7 @@ module Capybara
19
19
  end
20
20
 
21
21
  def query
22
- @query ||= Capybara::Queries::SiblingQuery.new(*session_query_args, &@filter_block)
22
+ @query ||= Capybara::Queries::SiblingQuery.new(*session_query_args, **session_query_options, &@filter_block)
23
23
  end
24
24
  end
25
25
  end
@@ -7,15 +7,15 @@ module Capybara
7
7
  module Matchers
8
8
  class HaveText < CountableWrappedElementMatcher
9
9
  def element_matches?(el)
10
- el.assert_text(*@args)
10
+ el.assert_text(*@args, **@kw_args)
11
11
  end
12
12
 
13
13
  def element_does_not_match?(el)
14
- el.assert_no_text(*@args)
14
+ el.assert_no_text(*@args, **@kw_args)
15
15
  end
16
16
 
17
17
  def description
18
- "text #{format(text)}"
18
+ "have text #{format(text)}"
19
19
  end
20
20
 
21
21
  def format(content)
@@ -7,11 +7,11 @@ module Capybara
7
7
  module Matchers
8
8
  class HaveTitle < WrappedElementMatcher
9
9
  def element_matches?(el)
10
- el.assert_title(*@args)
10
+ el.assert_title(*@args, **@kw_args)
11
11
  end
12
12
 
13
13
  def element_does_not_match?(el)
14
- el.assert_no_title(*@args)
14
+ el.assert_no_title(*@args, **@kw_args)
15
15
  end
16
16
 
17
17
  def description
@@ -7,11 +7,11 @@ module Capybara
7
7
  module Matchers
8
8
  class MatchSelector < HaveSelector
9
9
  def element_matches?(el)
10
- el.assert_matches_selector(*@args, &@filter_block)
10
+ el.assert_matches_selector(*@args, **session_query_options, &@filter_block)
11
11
  end
12
12
 
13
13
  def element_does_not_match?(el)
14
- el.assert_not_matches_selector(*@args, &@filter_block)
14
+ el.assert_not_matches_selector(*@args, **session_query_options, &@filter_block)
15
15
  end
16
16
 
17
17
  def description
@@ -19,7 +19,7 @@ module Capybara
19
19
  end
20
20
 
21
21
  def query
22
- @query ||= Capybara::Queries::MatchQuery.new(*session_query_args, &@filter_block)
22
+ @query ||= Capybara::Queries::MatchQuery.new(*session_query_args, **session_query_options, &@filter_block)
23
23
  end
24
24
  end
25
25
  end
@@ -6,8 +6,13 @@ 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
- el.assert_matches_style(*@args)
15
+ el.assert_matches_style(*@args, **@kw_args)
11
16
  end
12
17
 
13
18
  def does_not_match?(_actual)
@@ -28,7 +33,7 @@ module Capybara
28
33
  ##
29
34
  # @deprecated
30
35
  class HaveStyle < MatchStyle
31
- def initialize(*args, &filter_block)
36
+ def initialize(*args, **kw_args, &filter_block)
32
37
  warn 'HaveStyle matcher is deprecated, please use the MatchStyle matcher instead'
33
38
  super
34
39
  end
@@ -31,7 +31,8 @@ module Capybara
31
31
  private
32
32
 
33
33
  def options
34
- (@args.last.is_a?(Hash) ? @args : @args.push({})).last
34
+ # (@args.last.is_a?(Hash) ? @args : @args.push({})).last
35
+ @kw_args
35
36
  end
36
37
  end
37
38
  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
@@ -108,6 +110,8 @@ require 'capybara/selector/definition'
108
110
  # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
109
111
  # * :multiple (Boolean) - Match fields that accept multiple values
110
112
  # * :options (Array<String>) - Exact match options
113
+ # * :enabled_options (Array<String>) - Exact match enabled options
114
+ # * :disabled_options (Array<String>) - Exact match disabled options
111
115
  # * :with_options (Array<String>) - Partial match options
112
116
  # * :selected (String, Array<String>) - Match the selection(s)
113
117
  # * :with_selected (String, Array<String>) - Partial match the selection(s)
@@ -167,7 +171,7 @@ require 'capybara/selector/definition'
167
171
  # * Filters:
168
172
  # * :\<any> (String, Regexp) - Match on any specified element attribute
169
173
  #
170
- class Capybara::Selector; end
174
+ class Capybara::Selector; end # rubocop:disable Lint/EmptyClass
171
175
 
172
176
  Capybara::Selector::FilterSet.add(:_field) do
173
177
  node_filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }
@@ -176,6 +180,12 @@ Capybara::Selector::FilterSet.add(:_field) do
176
180
  node_filter(:valid, :boolean) { |node, value| node.evaluate_script('this.validity.valid') == value }
177
181
  node_filter(:name) { |node, value| !value.is_a?(Regexp) || value.match?(node[:name]) }
178
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
179
189
 
180
190
  expression_filter(:name) do |xpath, val|
181
191
  builder(xpath).add_attribute_conditions(name: val)
@@ -196,7 +206,7 @@ Capybara::Selector::FilterSet.add(:_field) do
196
206
  desc
197
207
  end
198
208
 
199
- 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, **|
200
210
  desc, states = +'', []
201
211
  states << 'checked' if checked || (unchecked == false)
202
212
  states << 'not checked' if unchecked || (checked == false)
@@ -204,6 +214,7 @@ Capybara::Selector::FilterSet.add(:_field) do
204
214
  desc << " that is #{states.join(' and ')}" unless states.empty?
205
215
  desc << ' that is valid' if valid == true
206
216
  desc << ' that is invalid' if valid == false
217
+ desc << " with validation message #{validation_message.to_s.inspect}" if validation_message
207
218
  desc
208
219
  end
209
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)
@@ -4,30 +4,30 @@ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
4
4
  xpath(:value, :title, :type, :name) do |locator, **options|
5
5
  input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
6
6
  btn_xpath = XPath.descendant(:button)
7
+ aria_btn_xpath = XPath.descendant[XPath.attr(:role).equals('button')]
7
8
  image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
8
9
 
9
10
  unless locator.nil?
10
11
  locator = locator.to_s
11
- locator_matchers = XPath.attr(:id).equals(locator) |
12
- XPath.attr(:name).equals(locator) |
13
- XPath.attr(:value).is(locator) |
14
- XPath.attr(:title).is(locator)
15
- locator_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
16
- locator_matchers |= XPath.attr(test_id) == locator if test_id
12
+ locator_matchers = combine_locators(locator, config: self)
13
+ btn_matchers = locator_matchers |
14
+ XPath.string.n.is(locator) |
15
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]
17
16
 
18
- input_btn_xpath = input_btn_xpath[locator_matchers]
19
-
20
- btn_xpath = btn_xpath[locator_matchers |
21
- XPath.string.n.is(locator) |
22
- XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
17
+ input_btn_xpath = input_btn_xpath[locator_matchers] + locate_label(locator).descendant(input_btn_xpath)
18
+ btn_xpath = btn_xpath[btn_matchers] + locate_label(locator).descendant(btn_xpath)
19
+ aria_btn_xpath = aria_btn_xpath[btn_matchers]
23
20
 
24
21
  alt_matches = XPath.attr(:alt).is(locator)
25
22
  alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
26
- image_btn_xpath = image_btn_xpath[alt_matches]
23
+ image_btn_xpath = image_btn_xpath[alt_matches] + locate_label(locator).descendant(image_btn_xpath)
27
24
  end
28
25
 
29
- %i[value title type].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
30
- memo[find_by_attr(ef, options[ef])]
26
+ btn_xpaths = [input_btn_xpath, btn_xpath, image_btn_xpath]
27
+ btn_xpaths << aria_btn_xpath if enable_aria_role
28
+
29
+ %i[value title type].inject(btn_xpaths.inject(&:union)) do |memo, ef|
30
+ memo.where(find_by_attr(ef, options[ef]))
31
31
  end
32
32
  end
33
33
 
@@ -48,4 +48,16 @@ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
48
48
  describe_node_filters do |disabled: nil, **|
49
49
  ' that is disabled' if disabled == true
50
50
  end
51
+
52
+ def combine_locators(locator, config:)
53
+ [
54
+ XPath.attr(:id).equals(locator),
55
+ XPath.attr(:name).equals(locator),
56
+ XPath.attr(:value).is(locator),
57
+ XPath.attr(:title).is(locator),
58
+ (XPath.attr(:id) == XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)),
59
+ (XPath.attr(:'aria-label').is(locator) if config.enable_aria_label),
60
+ (XPath.attr(config.test_id) == locator if config.test_id)
61
+ ].compact.inject(&:|)
62
+ end
51
63
  end