capybara 3.11.1 → 3.12.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +18 -1
  3. data/README.md +4 -4
  4. data/lib/capybara.rb +7 -0
  5. data/lib/capybara/node/element.rb +7 -1
  6. data/lib/capybara/node/matchers.rb +0 -18
  7. data/lib/capybara/queries/base_query.rb +1 -1
  8. data/lib/capybara/queries/selector_query.rb +24 -39
  9. data/lib/capybara/result.rb +4 -4
  10. data/lib/capybara/selector.rb +7 -15
  11. data/lib/capybara/selector/builders/css_builder.rb +59 -27
  12. data/lib/capybara/selector/builders/xpath_builder.rb +50 -35
  13. data/lib/capybara/selector/css.rb +7 -7
  14. data/lib/capybara/selector/filter.rb +1 -0
  15. data/lib/capybara/selector/filter_set.rb +17 -15
  16. data/lib/capybara/selector/filters/locator_filter.rb +19 -0
  17. data/lib/capybara/selector/regexp_disassembler.rb +104 -61
  18. data/lib/capybara/selector/selector.rb +14 -5
  19. data/lib/capybara/selenium/driver.rb +14 -9
  20. data/lib/capybara/selenium/driver_specializations/{marionette_driver.rb → firefox_driver.rb} +3 -3
  21. data/lib/capybara/selenium/nodes/{marionette_node.rb → firefox_node.rb} +1 -1
  22. data/lib/capybara/spec/session/all_spec.rb +8 -1
  23. data/lib/capybara/spec/session/assert_selector_spec.rb +0 -10
  24. data/lib/capybara/spec/session/click_button_spec.rb +5 -3
  25. data/lib/capybara/spec/session/click_link_spec.rb +5 -5
  26. data/lib/capybara/spec/session/find_spec.rb +1 -1
  27. data/lib/capybara/spec/session/first_spec.rb +1 -1
  28. data/lib/capybara/spec/session/has_css_spec.rb +7 -0
  29. data/lib/capybara/spec/session/has_xpath_spec.rb +17 -0
  30. data/lib/capybara/spec/session/node_spec.rb +10 -3
  31. data/lib/capybara/spec/session/window/window_spec.rb +2 -2
  32. data/lib/capybara/spec/spec_helper.rb +1 -2
  33. data/lib/capybara/spec/views/obscured.erb +3 -0
  34. data/lib/capybara/version.rb +1 -1
  35. data/spec/css_builder_spec.rb +99 -0
  36. data/spec/result_spec.rb +6 -0
  37. data/spec/selector_spec.rb +26 -1
  38. data/spec/selenium_spec_chrome.rb +18 -16
  39. data/spec/selenium_spec_chrome_remote.rb +0 -2
  40. data/spec/{selenium_spec_marionette.rb → selenium_spec_firefox.rb} +31 -25
  41. data/spec/selenium_spec_firefox_remote.rb +4 -6
  42. data/spec/shared_selenium_session.rb +2 -2
  43. data/spec/spec_helper.rb +5 -5
  44. data/spec/xpath_builder_spec.rb +91 -0
  45. metadata +10 -7
@@ -6,49 +6,64 @@ module Capybara
6
6
  class Selector
7
7
  # @api private
8
8
  class XPathBuilder
9
- class << self
10
- def attribute_conditions(attributes)
11
- attributes.map do |attribute, value|
12
- case value
13
- when XPath::Expression
14
- XPath.attr(attribute)[value]
15
- when Regexp
16
- XPath.attr(attribute)[regexp_to_xpath_conditions(value)]
17
- when true
18
- XPath.attr(attribute)
19
- when false, nil
20
- !XPath.attr(attribute)
21
- else
22
- XPath.attr(attribute) == value.to_s
23
- end
24
- end.reduce(:&)
25
- end
9
+ def initialize(expression)
10
+ @expression = expression || ''
11
+ end
12
+
13
+ attr_reader :expression
26
14
 
27
- def class_conditions(classes)
28
- case classes
29
- when XPath::Expression, Regexp
30
- attribute_conditions(class: classes)
15
+ def add_attribute_conditions(**conditions)
16
+ @expression = conditions.inject(expression) do |xp, (name, value)|
17
+ conditions = name == :class ? class_conditions(value) : attribute_conditions(name => value)
18
+ if xp.is_a? XPath::Expression
19
+ xp[conditions]
31
20
  else
32
- Array(classes).map do |klass|
33
- if klass.start_with?('!') && !klass.start_with?('!!!')
34
- !XPath.attr(:class).contains_word(klass.slice(1..-1))
35
- else
36
- XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
37
- end
38
- end.reduce(:&)
21
+ "(#{xp})[#{conditions}]"
39
22
  end
40
23
  end
24
+ end
25
+
26
+ private
41
27
 
42
- private
28
+ def attribute_conditions(attributes)
29
+ attributes.map do |attribute, value|
30
+ case value
31
+ when XPath::Expression
32
+ XPath.attr(attribute)[value]
33
+ when Regexp
34
+ XPath.attr(attribute)[regexp_to_xpath_conditions(value)]
35
+ when true
36
+ XPath.attr(attribute)
37
+ when false, nil
38
+ !XPath.attr(attribute)
39
+ else
40
+ XPath.attr(attribute) == value.to_s
41
+ end
42
+ end.reduce(:&)
43
+ end
43
44
 
44
- def regexp_to_xpath_conditions(regexp)
45
- condition = XPath.current
46
- condition = condition.uppercase if regexp.casefold?
47
- Selector::RegexpDisassembler.new(regexp).alternated_substrings.map do |strs|
48
- strs.map { |str| condition.contains(str) }.reduce(:&)
49
- end.reduce(:|)
45
+ def class_conditions(classes)
46
+ case classes
47
+ when XPath::Expression, Regexp
48
+ attribute_conditions(class: classes)
49
+ else
50
+ Array(classes).map do |klass|
51
+ if klass.start_with?('!') && !klass.start_with?('!!!')
52
+ !XPath.attr(:class).contains_word(klass.slice(1..-1))
53
+ else
54
+ XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
55
+ end
56
+ end.reduce(:&)
50
57
  end
51
58
  end
59
+
60
+ def regexp_to_xpath_conditions(regexp)
61
+ condition = XPath.current
62
+ condition = condition.uppercase if regexp.casefold?
63
+ Selector::RegexpDisassembler.new(regexp).alternated_substrings.map do |strs|
64
+ strs.map { |str| condition.contains(str) }.reduce(:&)
65
+ end.reduce(:|)
66
+ end
52
67
  end
53
68
  end
54
69
  end
@@ -31,22 +31,22 @@ module Capybara
31
31
  def split(css)
32
32
  selectors = []
33
33
  StringIO.open(css.to_s) do |str|
34
- selector = ''
34
+ selector = +''
35
35
  while (char = str.getc)
36
36
  case char
37
37
  when '['
38
- selector += parse_square(str)
38
+ selector << parse_square(str)
39
39
  when '('
40
- selector += parse_paren(str)
40
+ selector << parse_paren(str)
41
41
  when '"', "'"
42
- selector += parse_string(char, str)
42
+ selector << parse_string(char, str)
43
43
  when '\\'
44
- selector += char + str.getc
44
+ selector << char + str.getc
45
45
  when ','
46
46
  selectors << selector.strip
47
- selector = ''
47
+ selector.clear
48
48
  else
49
- selector += char
49
+ selector << char
50
50
  end
51
51
  end
52
52
  selectors << selector.strip
@@ -2,3 +2,4 @@
2
2
 
3
3
  require 'capybara/selector/filters/node_filter'
4
4
  require 'capybara/selector/filters/expression_filter'
5
+ require 'capybara/selector/filters/locator_filter'
@@ -40,9 +40,9 @@ module Capybara
40
40
  def description(node_filters: true, expression_filters: true, **options)
41
41
  opts = options_with_defaults(options)
42
42
  description = +''
43
- description += undeclared_descriptions.map { |desc| desc.call(opts).to_s }.join
44
- description += expression_filter_descriptions.map { |desc| desc.call(opts).to_s }.join if expression_filters
45
- description += node_filter_descriptions.map { |desc| desc.call(opts).to_s }.join if node_filters
43
+ description << undeclared_descriptions.map { |desc| desc.call(opts).to_s }.join
44
+ description << expression_filter_descriptions.map { |desc| desc.call(opts).to_s }.join if expression_filters
45
+ description << node_filter_descriptions.map { |desc| desc.call(opts).to_s }.join if node_filters
46
46
  description
47
47
  end
48
48
 
@@ -52,15 +52,16 @@ module Capybara
52
52
  end
53
53
 
54
54
  def import(name, filters = nil)
55
- f_set = self.class.all[name]
56
55
  filter_selector = filters.nil? ? ->(*) { true } : ->(filter_name, _) { filters.include? filter_name }
57
56
 
58
- expression_filters.merge!(f_set.expression_filters.select(&filter_selector))
59
- node_filters.merge!(f_set.node_filters.select(&filter_selector))
60
-
61
- f_set.undeclared_descriptions.each { |desc| describe(&desc) }
62
- f_set.expression_filter_descriptions.each { |desc| describe(:expression_filters, &desc) }
63
- f_set.node_filter_descriptions.each { |desc| describe(:node_filters, &desc) }
57
+ self.class[name].tap do |f_set|
58
+ expression_filters.merge!(f_set.expression_filters.select(&filter_selector))
59
+ node_filters.merge!(f_set.node_filters.select(&filter_selector))
60
+ f_set.undeclared_descriptions.each { |desc| describe(&desc) }
61
+ f_set.expression_filter_descriptions.each { |desc| describe(:expression_filters, &desc) }
62
+ f_set.node_filter_descriptions.each { |desc| describe(:node_filters, &desc) }
63
+ end
64
+ self
64
65
  end
65
66
 
66
67
  class << self
@@ -68,6 +69,10 @@ module Capybara
68
69
  @filter_sets ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
69
70
  end
70
71
 
72
+ def [](name)
73
+ all.fetch(name.to_sym) { |set_name| raise ArgumentError, "Unknown filter set (:#{set_name})" }
74
+ end
75
+
71
76
  def add(name, &block)
72
77
  all[name.to_sym] = FilterSet.new(name.to_sym, &block)
73
78
  end
@@ -107,11 +112,8 @@ module Capybara
107
112
  types.each { |type| options[type] = true }
108
113
  raise 'ArgumentError', ':default option is not supported for filters with a :matcher option' if matcher && options[:default]
109
114
 
110
- if filter_class <= Filters::ExpressionFilter
111
- @expression_filters[name] = filter_class.new(name, matcher, block, options)
112
- else
113
- @node_filters[name] = filter_class.new(name, matcher, block, options)
114
- end
115
+ filter = filter_class.new(name, matcher, block, options)
116
+ (filter_class <= Filters::ExpressionFilter ? @expression_filters : @node_filters)[name] = filter
115
117
  end
116
118
  end
117
119
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selector/filters/base'
4
+
5
+ module Capybara
6
+ class Selector
7
+ module Filters
8
+ class LocatorFilter < NodeFilter
9
+ def initialize(block, **options)
10
+ super(nil, nil, block, options)
11
+ end
12
+
13
+ def matches?(node, value, context = nil)
14
+ super(node, nil, value, context)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -65,22 +65,6 @@ module Capybara
65
65
  strs
66
66
  end
67
67
 
68
- def min_repeat(exp)
69
- exp.quantifier&.min || 1
70
- end
71
-
72
- def max_repeat(exp)
73
- exp.quantifier&.max || 1
74
- end
75
-
76
- def fixed_repeat?(exp)
77
- min_repeat(exp) == max_repeat(exp)
78
- end
79
-
80
- def optional?(exp)
81
- min_repeat(exp).zero?
82
- end
83
-
84
68
  def combine(strs)
85
69
  suffixes = [[]]
86
70
  strs.reverse_each do |str|
@@ -91,9 +75,7 @@ module Capybara
91
75
  prefixes.product(suffixes) { |pair| result << pair.flatten(1) }
92
76
  suffixes = result
93
77
  else
94
- suffixes.each do |arr|
95
- arr.unshift str
96
- end
78
+ suffixes.each { |arr| arr.unshift str }
97
79
  end
98
80
  end
99
81
  suffixes
@@ -106,59 +88,120 @@ module Capybara
106
88
  end
107
89
 
108
90
  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
91
+ Expression.new(expression).extract_strings(alternation)
92
+ end
93
+
94
+ # @api private
95
+ class Expression
96
+ def initialize(exp)
97
+ @exp = exp
98
+ end
99
+
100
+ def extract_strings(process_alternatives)
101
+ strings = []
102
+ each do |exp|
103
+ next strings.push(nil) if exp.optional? && !process_alternatives
115
104
 
116
- if %i[meta].include?(exp.type) && !exp.terminal? && alternation
117
- strings.push(alternative_strings(exp))
118
- next
105
+ next strings.push(exp.alternative_strings) if exp.alternation? && process_alternatives
106
+
107
+ strings.concat(exp.strings(process_alternatives))
119
108
  end
109
+ strings
110
+ end
111
+
112
+ protected
113
+
114
+ def alternation?
115
+ (type == :meta) && !terminal?
116
+ end
117
+
118
+ def optional?
119
+ min_repeat.zero?
120
+ end
121
+
122
+ def terminal?
123
+ @exp.terminal?
124
+ end
120
125
 
121
- if %i[meta set].include?(exp.type)
122
- strings.push(nil)
123
- next
126
+ def strings(process_alternatives)
127
+ if indeterminate?
128
+ [nil]
129
+ elsif terminal?
130
+ terminal_strings
131
+ elsif optional?
132
+ optional_strings
133
+ else
134
+ repeated_strings(process_alternatives)
124
135
  end
136
+ end
125
137
 
126
- if exp.terminal?
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
139
- else
140
- strings.push(text * min_repeat(exp))
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
138
+ def terminal_strings
139
+ text = case @exp.type
140
+ when :literal then @exp.text
141
+ when :escape then @exp.char
146
142
  else
147
- min_repeat(exp).times { strings.concat extract_strings(exp, alternation: alternation) }
143
+ return [nil]
148
144
  end
149
- strings.push(nil) unless fixed_repeat?(exp)
145
+
146
+ optional? ? options_set(text) : repeat_set(text)
147
+ end
148
+
149
+ def optional_strings
150
+ options_set(extract_strings(true))
151
+ end
152
+
153
+ def repeated_strings(process_alternatives)
154
+ repeat_set extract_strings(process_alternatives)
155
+ end
156
+
157
+ def alternative_strings
158
+ alts = alternatives.map { |sub_exp| sub_exp.extract_strings(alternation: true) }
159
+ alts.all?(&:any?) ? Set.new(alts) : nil
160
+ end
161
+
162
+ private
163
+
164
+ def indeterminate?
165
+ %i[meta set].include?(type)
166
+ end
167
+
168
+ def min_repeat
169
+ @exp.quantifier&.min || 1
170
+ end
171
+
172
+ def max_repeat
173
+ @exp.quantifier&.max || 1
174
+ end
175
+
176
+ def fixed_repeat?
177
+ min_repeat == max_repeat
178
+ end
179
+
180
+ def type
181
+ @exp.type
182
+ end
183
+
184
+ def repeat_set(str)
185
+ strs = Array(str * min_repeat)
186
+ strs.push(nil) unless fixed_repeat?
187
+ strs
188
+ end
189
+
190
+ def options_set(strs)
191
+ strs = [Set.new([[''], Array(strs)])]
192
+ strs.push(nil) unless max_repeat == 1
193
+ strs
194
+ end
195
+
196
+ def alternatives
197
+ @exp.alternatives.map { |exp| Expression.new(exp) }
150
198
  end
151
- strings
152
- end
153
199
 
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
200
+ def each
201
+ @exp.each { |exp| yield Expression.new(exp) }
160
202
  end
161
203
  end
204
+ private_constant :Expression
162
205
  end
163
206
  end
164
207
  end
@@ -169,17 +169,25 @@ module Capybara
169
169
  @selectors ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
170
170
  end
171
171
 
172
+ def [](name)
173
+ all.fetch(name.to_sym) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
174
+ end
175
+
172
176
  def add(name, &block)
173
177
  all[name.to_sym] = Capybara::Selector.new(name.to_sym, &block)
174
178
  end
175
179
 
176
180
  def update(name, &block)
177
- all[name.to_sym].instance_eval(&block)
181
+ self[name].instance_eval(&block)
178
182
  end
179
183
 
180
184
  def remove(name)
181
185
  all.delete(name.to_sym)
182
186
  end
187
+
188
+ def for(locator)
189
+ all.values.find { |sel| sel.match?(locator) }
190
+ end
183
191
  end
184
192
 
185
193
  def initialize(name, &block)
@@ -348,8 +356,9 @@ module Capybara
348
356
 
349
357
  def_delegators :@filter_set, :node_filter, :expression_filter, :filter
350
358
 
351
- def locator_filter(&block)
352
- @locator_filter = block if block
359
+ def locator_filter(*types, **options, &block)
360
+ types.each { |type| options[type] = true }
361
+ @locator_filter = Filters::LocatorFilter.new(block, options) if block
353
362
  @locator_filter
354
363
  end
355
364
 
@@ -400,7 +409,7 @@ module Capybara
400
409
  end
401
410
 
402
411
  # @api private
403
- def builder
412
+ def builder(expr = nil)
404
413
  case format
405
414
  when :css
406
415
  Capybara::Selector::CSSBuilder
@@ -408,7 +417,7 @@ module Capybara
408
417
  Capybara::Selector::XPathBuilder
409
418
  else
410
419
  raise NotImplementedError, "No builder exists for selector of type #{format}"
411
- end
420
+ end.new(expr)
412
421
  end
413
422
 
414
423
  # @api private