capybara 3.10.1 → 3.11.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +13 -0
  3. data/README.md +2 -3
  4. data/lib/capybara.rb +16 -6
  5. data/lib/capybara/minitest.rb +8 -9
  6. data/lib/capybara/node/actions.rb +31 -28
  7. data/lib/capybara/node/base.rb +2 -1
  8. data/lib/capybara/node/document_matchers.rb +6 -2
  9. data/lib/capybara/node/element.rb +10 -10
  10. data/lib/capybara/node/finders.rb +13 -14
  11. data/lib/capybara/node/matchers.rb +1 -3
  12. data/lib/capybara/node/simple.rb +10 -2
  13. data/lib/capybara/queries/base_query.rb +7 -3
  14. data/lib/capybara/queries/selector_query.rb +60 -34
  15. data/lib/capybara/queries/style_query.rb +5 -1
  16. data/lib/capybara/queries/text_query.rb +2 -2
  17. data/lib/capybara/queries/title_query.rb +1 -1
  18. data/lib/capybara/rack_test/node.rb +16 -2
  19. data/lib/capybara/result.rb +9 -4
  20. data/lib/capybara/rspec/features.rb +4 -4
  21. data/lib/capybara/rspec/matcher_proxies.rb +3 -1
  22. data/lib/capybara/rspec/matchers.rb +25 -287
  23. data/lib/capybara/rspec/matchers/base.rb +98 -0
  24. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  25. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  26. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  27. data/lib/capybara/rspec/matchers/have_selector.rb +69 -0
  28. data/lib/capybara/rspec/matchers/have_style.rb +23 -0
  29. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  30. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  31. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  32. data/lib/capybara/selector.rb +48 -20
  33. data/lib/capybara/selector/builders/xpath_builder.rb +3 -3
  34. data/lib/capybara/selector/css.rb +5 -5
  35. data/lib/capybara/selector/filters/base.rb +11 -3
  36. data/lib/capybara/selector/filters/expression_filter.rb +3 -3
  37. data/lib/capybara/selector/filters/node_filter.rb +16 -2
  38. data/lib/capybara/selector/regexp_disassembler.rb +116 -17
  39. data/lib/capybara/selector/selector.rb +52 -26
  40. data/lib/capybara/selenium/driver.rb +6 -2
  41. data/lib/capybara/selenium/node.rb +15 -14
  42. data/lib/capybara/selenium/nodes/marionette_node.rb +19 -5
  43. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -3
  44. data/lib/capybara/server.rb +6 -1
  45. data/lib/capybara/server/animation_disabler.rb +1 -1
  46. data/lib/capybara/session.rb +4 -2
  47. data/lib/capybara/session/matchers.rb +7 -3
  48. data/lib/capybara/spec/public/test.js +5 -5
  49. data/lib/capybara/spec/session/all_spec.rb +5 -0
  50. data/lib/capybara/spec/session/has_css_spec.rb +4 -4
  51. data/lib/capybara/spec/session/has_field_spec.rb +17 -0
  52. data/lib/capybara/spec/session/node_spec.rb +45 -4
  53. data/lib/capybara/spec/spec_helper.rb +6 -1
  54. data/lib/capybara/spec/views/frame_child.erb +1 -1
  55. data/lib/capybara/spec/views/obscured.erb +44 -0
  56. data/lib/capybara/spec/views/with_html.erb +1 -1
  57. data/lib/capybara/version.rb +1 -1
  58. data/spec/rack_test_spec.rb +15 -0
  59. data/spec/regexp_dissassembler_spec.rb +88 -8
  60. data/spec/selector_spec.rb +3 -0
  61. data/spec/selenium_spec_chrome.rb +9 -15
  62. data/spec/selenium_spec_chrome_remote.rb +3 -2
  63. data/spec/selenium_spec_firefox_remote.rb +6 -2
  64. metadata +54 -3
  65. data/lib/capybara/rspec/compound.rb +0 -86
@@ -44,9 +44,9 @@ module Capybara
44
44
  def regexp_to_xpath_conditions(regexp)
45
45
  condition = XPath.current
46
46
  condition = condition.uppercase if regexp.casefold?
47
- Selector::RegexpDisassembler.new(regexp).substrings.map do |str|
48
- condition.contains(str)
49
- end.reduce(:&)
47
+ Selector::RegexpDisassembler.new(regexp).alternated_substrings.map do |strs|
48
+ strs.map { |str| condition.contains(str) }.reduce(:&)
49
+ end.reduce(:|)
50
50
  end
51
51
  end
52
52
  end
@@ -21,11 +21,11 @@ module Capybara
21
21
  end
22
22
 
23
23
  S = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}'
24
- H = /[0-9a-fA-F]/
25
- UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/
26
- NONASCII = /[#{S}]/
27
- ESCAPE = /#{UNICODE}|\\[ -~#{S}]/
28
- NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/
24
+ H = /[0-9a-fA-F]/.freeze
25
+ UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/.freeze
26
+ NONASCII = /[#{S}]/.freeze
27
+ ESCAPE = /#{UNICODE}|\\[ -~#{S}]/.freeze
28
+ NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/.freeze
29
29
 
30
30
  class Splitter
31
31
  def split(css)
@@ -28,6 +28,10 @@ module Capybara
28
28
  !@matcher.nil?
29
29
  end
30
30
 
31
+ def boolean?
32
+ !!@options[:boolean]
33
+ end
34
+
31
35
  def handles_option?(option_name)
32
36
  if matcher?
33
37
  option_name =~ @matcher
@@ -38,17 +42,21 @@ module Capybara
38
42
 
39
43
  private
40
44
 
41
- def apply(subject, name, value, skip_value)
45
+ def apply(subject, name, value, skip_value, ctx)
42
46
  return skip_value if skip?(value)
43
47
  raise ArgumentError, "Invalid value #{value.inspect} passed to #{self.class.name.split('::').last} #{name}#{" : #{@name}" if @name.is_a?(Regexp)}" unless valid_value?(value)
44
48
 
45
49
  if @block.arity == 2
46
- @block.call(subject, value)
50
+ filter_context(ctx).instance_exec(subject, value, &@block)
47
51
  else
48
- @block.call(subject, name, value)
52
+ filter_context(ctx).instance_exec(subject, name, value, &@block)
49
53
  end
50
54
  end
51
55
 
56
+ def filter_context(context)
57
+ context || @block.binding.receiver
58
+ end
59
+
52
60
  def valid_value?(value)
53
61
  return true unless @options.key?(:valid_values)
54
62
 
@@ -6,8 +6,8 @@ module Capybara
6
6
  class Selector
7
7
  module Filters
8
8
  class ExpressionFilter < Base
9
- def apply_filter(expr, name, value)
10
- apply(expr, name, value, expr)
9
+ def apply_filter(expr, name, value, selector)
10
+ apply(expr, name, value, expr, selector)
11
11
  end
12
12
  end
13
13
 
@@ -15,7 +15,7 @@ module Capybara
15
15
  def initialize(name); super(name, nil, nil); end
16
16
  def default?; false; end
17
17
  def matcher?; false; end
18
- def apply_filter(expr, _name, _value); expr; end
18
+ def apply_filter(expr, _name, _value, _ctx); expr; end
19
19
  end
20
20
  end
21
21
  end
@@ -6,8 +6,22 @@ module Capybara
6
6
  class Selector
7
7
  module Filters
8
8
  class NodeFilter < Base
9
- def matches?(node, name, value)
10
- apply(node, name, value, true)
9
+ def initialize(name, matcher, block, **options)
10
+ super
11
+ @block = if boolean?
12
+ proc do |node, value|
13
+ error_cnt = errors.size
14
+ block.call(node, value).tap do |res|
15
+ add_error("Expected #{name} #{value} but it wasn't") if !res && error_cnt == errors.size
16
+ end
17
+ end
18
+ else
19
+ block
20
+ end
21
+ end
22
+
23
+ def matches?(node, name, value, context = nil)
24
+ apply(node, name, value, true, context)
11
25
  rescue Capybara::ElementNotFound
12
26
  false
13
27
  end
@@ -10,56 +10,155 @@ module Capybara
10
10
  @regexp = regexp
11
11
  end
12
12
 
13
+ def alternated_substrings
14
+ @alternated_substrings ||= begin
15
+ or_strings = process(alternation: true)
16
+ remove_or_covered(or_strings)
17
+ or_strings.any?(&:empty?) ? [] : or_strings
18
+ end
19
+ end
20
+
13
21
  def substrings
14
22
  @substrings ||= begin
15
- strs = extract_strings(Regexp::Parser.parse(@regexp), [+''])
16
- strs.map!(&:upcase) if @regexp.casefold?
17
- strs.reject(&:empty?).uniq
23
+ strs = process(alternation: false).first
24
+ remove_and_covered(strs)
18
25
  end
19
26
  end
20
27
 
21
28
  private
22
29
 
30
+ def remove_and_covered(strings)
31
+ # delete_if is documented to modify the array after every block iteration - this doesn't appear to be true
32
+ # uniq the strings to prevent identical strings from removing each other
33
+ strings.uniq!
34
+
35
+ # If we have "ab" and "abcd" required - only need to check for "abcd"
36
+ strings.delete_if do |sub_string|
37
+ strings.any? do |cover_string|
38
+ next if sub_string.equal? cover_string
39
+
40
+ cover_string.include?(sub_string)
41
+ end
42
+ end
43
+ end
44
+
45
+ def remove_or_covered(or_series)
46
+ # If we are going to match `("a" and "b") or ("ade" and "bce")` it only makes sense to match ("a" and "b")
47
+
48
+ # Ensure minimum sets of strings are being or'd
49
+ or_series.each { |strs| remove_and_covered(strs) }
50
+
51
+ # Remove any of the alternated string series that fully contain any other string series
52
+ or_series.delete_if do |and_strs|
53
+ or_series.any? do |and_strs2|
54
+ next if and_strs.equal? and_strs2
55
+
56
+ remove_and_covered(and_strs + and_strs2) == and_strs
57
+ end
58
+ end
59
+ end
60
+
61
+ def process(alternation:)
62
+ strs = extract_strings(Regexp::Parser.parse(@regexp), alternation: alternation)
63
+ strs = collapse(combine(strs).map(&:flatten))
64
+ strs.each { |str| str.map!(&:upcase) } if @regexp.casefold?
65
+ strs
66
+ end
67
+
23
68
  def min_repeat(exp)
24
69
  exp.quantifier&.min || 1
25
70
  end
26
71
 
72
+ def max_repeat(exp)
73
+ exp.quantifier&.max || 1
74
+ end
75
+
27
76
  def fixed_repeat?(exp)
28
- min_repeat(exp) == (exp.quantifier&.max || 1)
77
+ min_repeat(exp) == max_repeat(exp)
29
78
  end
30
79
 
31
80
  def optional?(exp)
32
81
  min_repeat(exp).zero?
33
82
  end
34
83
 
35
- def extract_strings(expression, strings)
36
- expression.each do |exp|
37
- if optional?(exp)
38
- strings.push(+'')
84
+ def combine(strs)
85
+ suffixes = [[]]
86
+ strs.reverse_each do |str|
87
+ if str.is_a? Set
88
+ prefixes = str.each_with_object([]) { |s, memo| memo.concat combine(s) }
89
+
90
+ result = []
91
+ prefixes.product(suffixes) { |pair| result << pair.flatten(1) }
92
+ suffixes = result
93
+ else
94
+ suffixes.each do |arr|
95
+ arr.unshift str
96
+ end
97
+ end
98
+ end
99
+ suffixes
100
+ end
101
+
102
+ def collapse(strs)
103
+ strs.map do |substrings|
104
+ substrings.slice_before(&:nil?).map(&:join).reject(&:empty?).uniq
105
+ end
106
+ end
107
+
108
+ def extract_strings(expression, alternation: false)
109
+ strings = []
110
+ expression.each do |exp| # rubocop:disable Metrics/BlockLength
111
+ if optional?(exp) && !alternation
112
+ strings.push(nil)
113
+ next
114
+ end
115
+
116
+ if %i[meta].include?(exp.type) && !exp.terminal? && alternation
117
+ strings.push(alternative_strings(exp))
39
118
  next
40
119
  end
41
120
 
42
121
  if %i[meta set].include?(exp.type)
43
- strings.push(+'')
122
+ strings.push(nil)
44
123
  next
45
124
  end
46
125
 
47
126
  if exp.terminal?
48
- case exp.type
49
- when :literal
50
- strings.last << (exp.text * min_repeat(exp))
51
- when :escape
52
- strings.last << (exp.char * min_repeat(exp))
127
+ text = case exp.type
128
+ when :literal then exp.text
129
+ when :escape then exp.char
130
+ else
131
+ strings.push(nil)
132
+ next
133
+ end
134
+
135
+ if optional?(exp)
136
+ strings.push(Set.new([[''], [text]]))
137
+ strings.push(nil) unless max_repeat(exp) == 1
138
+ next
53
139
  else
54
- strings.push(+'')
140
+ strings.push(text * min_repeat(exp))
55
141
  end
142
+ elsif optional?(exp)
143
+ strings.push(Set.new([[''], extract_strings(exp, alternation: true)]))
144
+ strings.push(nil) unless max_repeat(exp) == 1
145
+ next
56
146
  else
57
- min_repeat(exp).times { extract_strings(exp, strings) }
147
+ min_repeat(exp).times { strings.concat extract_strings(exp, alternation: alternation) }
58
148
  end
59
- strings.push(+'') unless fixed_repeat?(exp)
149
+ strings.push(nil) unless fixed_repeat?(exp)
60
150
  end
61
151
  strings
62
152
  end
153
+
154
+ def alternative_strings(expression)
155
+ alternatives = expression.alternatives.map { |sub_exp| extract_strings(sub_exp, alternation: true) }
156
+ if alternatives.all?(&:any?)
157
+ Set.new(alternatives)
158
+ else
159
+ nil
160
+ end
161
+ end
63
162
  end
64
163
  end
65
164
  end
@@ -19,18 +19,18 @@ module Capybara
19
19
  # * Locator: A CSS selector
20
20
  #
21
21
  # * **:id** - Select element by id
22
- # * Locator: The id of the element to match
22
+ # * Locator: (String, Regexp, XPath::Expression) The id of the element to match
23
23
  #
24
24
  # * **:field** - Select field elements (input [not of type submit, image, or hidden], textarea, select)
25
25
  # * Locator: Matches against the id, Capybara.test_id attribute, name, or placeholder
26
26
  # * Filters:
27
- # * :id (String) — Matches the id attribute
27
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
28
28
  # * :name (String) — Matches the name attribute
29
29
  # * :placeholder (String) — Matches the placeholder attribute
30
30
  # * :type (String) — Matches the type attribute of the field or element type for 'textarea' and 'select'
31
31
  # * :readonly (Boolean)
32
32
  # * :with (String) — Matches the current value of the field
33
- # * :class (String, Array<String>) — Matches the class(es) provided
33
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
34
34
  # * :checked (Boolean) — Match checked fields?
35
35
  # * :unchecked (Boolean) — Match unchecked fields?
36
36
  # * :disabled (Boolean) — Match disabled field?
@@ -39,25 +39,25 @@ module Capybara
39
39
  # * **:fieldset** - Select fieldset elements
40
40
  # * Locator: Matches id or contents of wrapped legend
41
41
  # * Filters:
42
- # * :id (String) — Matches id attribute
42
+ # * :id (String, Regexp, XPath::Expression) — Matches id attribute
43
43
  # * :legend (String) — Matches contents of wrapped legend
44
- # * :class (String, Array<String>) — Matches the class(es) provided
44
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
45
45
  #
46
46
  # * **:link** - Find links ( <a> elements with an href attribute )
47
47
  # * Locator: Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element
48
48
  # * Filters:
49
- # * :id (String) — Matches the id attribute
49
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
50
50
  # * :title (String) — Matches the title attribute
51
51
  # * :alt (String) — Matches the alt attribute of a contained img element
52
- # * :class (String) — Matches the class(es) provided
52
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
53
53
  # * :href (String, Regexp, nil) — Matches the normalized href of the link, if nil will find <a> elements with no href attribute
54
54
  #
55
55
  # * **:button** - Find buttons ( input [of type submit, reset, image, button] or button elements )
56
56
  # * Locator: Matches the id, Capybara.test_id attribute, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
57
57
  # * Filters:
58
- # * :id (String) — Matches the id attribute
58
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
59
59
  # * :title (String) — Matches the title attribute
60
- # * :class (String) — Matches the class(es) provided
60
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
61
61
  # * :value (String) — Matches the value of an input button
62
62
  # * :type
63
63
  #
@@ -67,21 +67,21 @@ module Capybara
67
67
  # * **:fillable_field** - Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
68
68
  # * Locator: Matches against the id, Capybara.test_id attribute, name, or placeholder
69
69
  # * Filters:
70
- # * :id (String) — Matches the id attribute
70
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
71
71
  # * :name (String) — Matches the name attribute
72
72
  # * :placeholder (String) — Matches the placeholder attribute
73
73
  # * :with (String) — Matches the current value of the field
74
74
  # * :type (String) — Matches the type attribute of the field or element type for 'textarea'
75
- # * :class (String, Array<String>) — Matches the class(es) provided
75
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
76
76
  # * :disabled (Boolean) — Match disabled field?
77
77
  # * :multiple (Boolean) — Match fields that accept multiple values
78
78
  #
79
79
  # * **:radio_button** - Find radio buttons
80
80
  # * Locator: Match id, Capybara.test_id attribute, name, or associated label text
81
81
  # * Filters:
82
- # * :id (String) — Matches the id attribute
82
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
83
83
  # * :name (String) — Matches the name attribute
84
- # * :class (String, Array<String>) — Matches the class(es) provided
84
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
85
85
  # * :checked (Boolean) — Match checked fields?
86
86
  # * :unchecked (Boolean) — Match unchecked fields?
87
87
  # * :disabled (Boolean) — Match disabled field?
@@ -90,9 +90,9 @@ module Capybara
90
90
  # * **:checkbox** - Find checkboxes
91
91
  # * Locator: Match id, Capybara.test_id attribute, name, or associated label text
92
92
  # * Filters:
93
- # * *:id (String) — Matches the id attribute
93
+ # * *:id (String, Regexp, XPath::Expression) — Matches the id attribute
94
94
  # * *:name (String) — Matches the name attribute
95
- # * *:class (String, Array<String>) — Matches the class(es) provided
95
+ # * *:class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
96
96
  # * *:checked (Boolean) — Match checked fields?
97
97
  # * *:unchecked (Boolean) — Match unchecked fields?
98
98
  # * *:disabled (Boolean) — Match disabled field?
@@ -101,10 +101,10 @@ module Capybara
101
101
  # * **:select** - Find select elements
102
102
  # * Locator: Match id, Capybara.test_id attribute, name, placeholder, or associated label text
103
103
  # * Filters:
104
- # * :id (String) — Matches the id attribute
104
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
105
105
  # * :name (String) — Matches the name attribute
106
106
  # * :placeholder (String) — Matches the placeholder attribute
107
- # * :class (String, Array<String>) — Matches the class(es) provided
107
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
108
108
  # * :disabled (Boolean) — Match disabled field?
109
109
  # * :multiple (Boolean) — Match fields that accept multiple values
110
110
  # * :options (Array<String>) — Exact match options
@@ -131,9 +131,9 @@ module Capybara
131
131
  # * **:file_field** - Find file input elements
132
132
  # * Locator: Match id, Capybara.test_id attribute, name, or associated label text
133
133
  # * Filters:
134
- # * :id (String) — Matches the id attribute
134
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
135
135
  # * :name (String) — Matches the name attribute
136
- # * :class (String, Array<String>) — Matches the class(es) provided
136
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
137
137
  # * :disabled (Boolean) — Match disabled field?
138
138
  # * :multiple (Boolean) — Match field that accepts multiple values
139
139
  #
@@ -145,16 +145,16 @@ module Capybara
145
145
  # * **:table** - Find table elements
146
146
  # * Locator: id or caption text of table
147
147
  # * Filters:
148
- # * :id (String) — Match id attribute of table
148
+ # * :id (String, Regexp, XPath::Expression) — Match id attribute of table
149
149
  # * :caption (String) — Match text of associated caption
150
- # * :class (String, Array<String>) — Matches the class(es) provided
150
+ # * :class ((String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
151
151
  #
152
152
  # * **:frame** - Find frame/iframe elements
153
153
  # * Locator: Match id or name
154
154
  # * Filters:
155
- # * :id (String) — Match id attribute
155
+ # * :id (String, Regexp, XPath::Expression) — Match id attribute
156
156
  # * :name (String) — Match name attribute
157
- # * :class (String, Array<String>) — Matches the class(es) provided
157
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
158
158
  #
159
159
  # * **:element**
160
160
  # * Locator: Type of element ('div', 'a', etc) - if not specified defaults to '*'
@@ -191,6 +191,7 @@ module Capybara
191
191
  @format = nil
192
192
  @expression = nil
193
193
  @expression_filters = {}
194
+ @locator_filter = nil
194
195
  @default_visibility = nil
195
196
  @config = {
196
197
  enable_aria_label: false,
@@ -347,6 +348,11 @@ module Capybara
347
348
 
348
349
  def_delegators :@filter_set, :node_filter, :expression_filter, :filter
349
350
 
351
+ def locator_filter(&block)
352
+ @locator_filter = block if block
353
+ @locator_filter
354
+ end
355
+
350
356
  def filter_set(name, filters_to_use = nil)
351
357
  @filter_set.import(name, filters_to_use)
352
358
  end
@@ -389,6 +395,10 @@ module Capybara
389
395
  vis.nil? ? fallback : vis
390
396
  end
391
397
 
398
+ def add_error(error_msg)
399
+ errors << error_msg
400
+ end
401
+
392
402
  # @api private
393
403
  def builder
394
404
  case format
@@ -401,8 +411,20 @@ module Capybara
401
411
  end
402
412
  end
403
413
 
414
+ # @api private
415
+ def with_filter_errors(errors)
416
+ Thread.current["capybara_#{object_id}_errors"] = errors
417
+ yield
418
+ ensure
419
+ Thread.current["capybara_#{object_id}_errors"] = nil
420
+ end
421
+
404
422
  private
405
423
 
424
+ def errors
425
+ Thread.current["capybara_#{object_id}_errors"] || []
426
+ end
427
+
406
428
  def enable_aria_label
407
429
  @config[:enable_aria_label]
408
430
  end
@@ -430,15 +452,19 @@ module Capybara
430
452
  def describe_all_expression_filters(**opts)
431
453
  expression_filters.map do |ef_name, ef|
432
454
  if ef.matcher?
433
- opts.keys.map do |key|
434
- " with #{ef_name}[#{key} => #{opts[key]}]" if ef.handles_option?(key) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(key)
435
- end.join
455
+ handled_custom_keys(ef, opts.keys).map { |key| " with #{ef_name}[#{key} => #{opts[key]}]" }.join
436
456
  elsif opts.key?(ef_name)
437
457
  " with #{ef_name} #{opts[ef_name]}"
438
458
  end
439
459
  end.join
440
460
  end
441
461
 
462
+ def handled_custom_keys(filter, keys)
463
+ keys.select do |key|
464
+ filter.handles_option?(key) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(key)
465
+ end
466
+ end
467
+
442
468
  def find_by_attr(attribute, value)
443
469
  finder_name = "find_by_#{attribute}_attr"
444
470
  if respond_to?(finder_name, true)