capybara 3.10.1 → 3.11.0

Sign up to get free protection for your applications and to get access to all the features.
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)