capybara 3.11.1 → 3.12.0

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