capybara 3.11.0 → 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 +24 -0
  3. data/README.md +4 -4
  4. data/lib/capybara/node/element.rb +7 -1
  5. data/lib/capybara/node/matchers.rb +0 -18
  6. data/lib/capybara/queries/base_query.rb +1 -1
  7. data/lib/capybara/queries/selector_query.rb +24 -39
  8. data/lib/capybara/result.rb +4 -4
  9. data/lib/capybara/selector/builders/css_builder.rb +59 -27
  10. data/lib/capybara/selector/builders/xpath_builder.rb +50 -35
  11. data/lib/capybara/selector/css.rb +7 -7
  12. data/lib/capybara/selector/filter.rb +1 -0
  13. data/lib/capybara/selector/filter_set.rb +17 -15
  14. data/lib/capybara/selector/filters/locator_filter.rb +19 -0
  15. data/lib/capybara/selector/regexp_disassembler.rb +104 -61
  16. data/lib/capybara/selector/selector.rb +14 -5
  17. data/lib/capybara/selector.rb +10 -20
  18. data/lib/capybara/selenium/driver.rb +14 -9
  19. data/lib/capybara/selenium/driver_specializations/{marionette_driver.rb → firefox_driver.rb} +3 -3
  20. data/lib/capybara/selenium/nodes/{marionette_node.rb → firefox_node.rb} +1 -1
  21. data/lib/capybara/spec/session/all_spec.rb +8 -1
  22. data/lib/capybara/spec/session/assert_selector_spec.rb +0 -10
  23. data/lib/capybara/spec/session/click_button_spec.rb +5 -3
  24. data/lib/capybara/spec/session/click_link_spec.rb +5 -5
  25. data/lib/capybara/spec/session/find_spec.rb +1 -1
  26. data/lib/capybara/spec/session/first_spec.rb +1 -1
  27. data/lib/capybara/spec/session/has_css_spec.rb +7 -0
  28. data/lib/capybara/spec/session/has_xpath_spec.rb +17 -0
  29. data/lib/capybara/spec/session/node_spec.rb +10 -3
  30. data/lib/capybara/spec/session/window/window_spec.rb +2 -2
  31. data/lib/capybara/spec/spec_helper.rb +1 -2
  32. data/lib/capybara/spec/views/obscured.erb +3 -0
  33. data/lib/capybara/version.rb +1 -1
  34. data/lib/capybara.rb +7 -0
  35. data/spec/css_builder_spec.rb +99 -0
  36. data/spec/result_spec.rb +6 -0
  37. data/spec/selector_spec.rb +41 -0
  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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c349d493d1d6639b40265451f5b357aa7b5d29d279e1095284071fba9bc8a59
4
- data.tar.gz: 23350dd400ffd3e11fdda081f12331ab7518b94061fb104355e30cfbee0d9935
3
+ metadata.gz: 8b3b4d3a612269d9ee165e7ed94e1e62f91c6fd85bcc5c5ae614c25d0c22bd32
4
+ data.tar.gz: a2bdfa0c2398789bd15d791e3b2d0d03d9c3371033e4f4e4d21e302db845c93f
5
5
  SHA512:
6
- metadata.gz: caa9124c0465dcc82b15109c4cbd841dbc8e214ff87fd928a712abd6896155c6c8b3be00d0ec09cb1fbbea9da8206773b799766da5fd08ac9bc0a9d36d81dd85
7
- data.tar.gz: 18b0c515e7be809183bc26d6d2ce38692561e30760b96a26992d44b96027484ae942f7c25e48b803323ef0534ca616fd20c503ec2cfefbf1da47416eb30b2443
6
+ metadata.gz: f4815632f5bc14c932e9b5181d68584ac3088f11241e8ee3c19f3ee0a27190f8ad1544b88a532f901eaf79f9ae3c2f721dfca1d2d6d20d098a40438042b9f64a
7
+ data.tar.gz: eb2765462cdbdda76a4ca5d100b93688029d1ca727c00579c5e9e5d77f26fc4a0fe64bbe3616bb3f89b34e730c337e242b319b2ea606b5287a780f8219ee5b08
data/History.md CHANGED
@@ -1,3 +1,27 @@
1
+ # Version 3.12.0
2
+ Release date: 2018-11-28
3
+
4
+ ### Added
5
+
6
+ * Support Ruby 2.6 endless range in Result#[] and query `:between` option
7
+ * Pre-registered headless firefox driver :selenium_headless [Andrew Havens]
8
+ * Selenium driver now defaults to clearing `sessionStorage` and `localStorage`. To disable pass `clear_local_storage: false` and/or `clear_session_storage: false` when creating Capybara::Selenium::Driver instance in your driver registration
9
+
10
+ ### Fixed
11
+
12
+ * Raise error if only :x or :y are passed as an offset to click methods
13
+
14
+ ### Removed
15
+
16
+ * Support for RSpec < 3.5
17
+
18
+ # Version 3.11.1
19
+ Release date: 2018-11-16
20
+
21
+ ### Fixed
22
+
23
+ * Fixed :link_or_button XPath generation when it has had an expression filter added
24
+
1
25
  # Version 3.11.0
2
26
  Release date: 2018-11-14
3
27
 
data/README.md CHANGED
@@ -6,8 +6,7 @@
6
6
  [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jnicklas/capybara?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
7
7
  [![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=capybara&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=capybara&package-manager=bundler&version-scheme=semver)
8
8
 
9
- **Note** You are viewing the README for the 3.11.0 version of Capybara.
10
-
9
+ **Note** You are viewing the README for the 3.12.x version of Capybara.
11
10
 
12
11
  Capybara helps you test web applications by simulating how a real user would
13
12
  interact with your app. It is agnostic about the driver running your tests and
@@ -139,7 +138,7 @@ There are also explicit tags for each registered driver set up for you (`@seleni
139
138
 
140
139
  ## <a name="using-capybara-with-rspec"></a>Using Capybara with RSpec
141
140
 
142
- Load RSpec 3.x support by adding the following line (typically to your
141
+ Load RSpec 3.5+ support by adding the following line (typically to your
143
142
  `spec_helper.rb` file):
144
143
 
145
144
  ```ruby
@@ -394,6 +393,7 @@ and add it to your Gemfile if you're using bundler.
394
393
  Capybara pre-registers a number of named drives that use Selenium - they are:
395
394
 
396
395
  * :selenium => Selenium driving Firefox
396
+ * :selenium_headless => Selenium driving Firefox in a headless configuration
397
397
  * :selenium_chrome => Selenium driving Chrome
398
398
  * :selenium_chrome_headless => Selenium driving Chrome in a headless configuration
399
399
 
@@ -404,7 +404,7 @@ to the browsers. See the section on adding and configuring drivers.
404
404
 
405
405
  **Note**: drivers which run the server in a different thread may not share the
406
406
  same transaction as your tests, causing data not to be shared between your test
407
- and test server, see "Transactions and database setup" below.
407
+ and test server, see [Transactions and database setup](#transactions-and-database-setup) below.
408
408
 
409
409
  ### <a name="capybara-webkit"></a>Capybara-webkit
410
410
 
@@ -153,7 +153,9 @@ module Capybara
153
153
  # @option offset [Integer] y Y coordinate to offset the click location from the top left corner of the element
154
154
  # @return [Capybara::Node::Element] The element
155
155
  def click(*keys, wait: nil, **offset)
156
- synchronize(wait) { base.click(keys, offset) }
156
+ raise ArgumentError, 'You must specify both x: and y: for a click offset' if nil ^ offset[:x] ^ offset[:y]
157
+
158
+ synchronize(wait) { base.click(Array(keys), offset) }
157
159
  self
158
160
  end
159
161
 
@@ -164,6 +166,8 @@ module Capybara
164
166
  # @macro click_modifiers
165
167
  # @return [Capybara::Node::Element] The element
166
168
  def right_click(*keys, wait: nil, **offset)
169
+ raise ArgumentError, 'You must specify both x: and y: for a click offset' if nil ^ offset[:x] ^ offset[:y]
170
+
167
171
  synchronize(wait) { base.right_click(keys, offset) }
168
172
  self
169
173
  end
@@ -175,6 +179,8 @@ module Capybara
175
179
  # @macro click_modifiers
176
180
  # @return [Capybara::Node::Element] The element
177
181
  def double_click(*keys, wait: nil, **offset)
182
+ raise ArgumentError, 'You must specify both x: and y: for a click offset' if nil ^ offset[:x] ^ offset[:y]
183
+
178
184
  synchronize(wait) { base.double_click(keys, offset) }
179
185
  self
180
186
  end
@@ -546,24 +546,6 @@ module Capybara
546
546
  end
547
547
  end
548
548
 
549
- # Deprecated
550
- # TODO: remove
551
- def refute_selector(*args, &optional_filter_block)
552
- warn '`refute_selector` was never meant to be in this scope unless ' \
553
- 'using minitest. Either replace with `assert_no_selector` ' \
554
- "or require 'capybara/minitest'."
555
- assert_no_selector(*args, &optional_filter_block)
556
- end
557
-
558
- # Deprecated
559
- # TODO: remove
560
- def refute_matches_selector(*args, &optional_filter_block)
561
- warn '`refute_matches_selector` was never meant to be in this scope unless ' \
562
- 'using minitest. Either replace with `assert_not_matches_selector` ' \
563
- "or require 'capybara/minitest'."
564
- assert_not_matches_selector(*args, &optional_filter_block)
565
- end
566
-
567
549
  ##
568
550
  #
569
551
  # Checks if the current node matches given selector
@@ -79,7 +79,7 @@ module Capybara
79
79
  if count
80
80
  message << " #{occurrences count}"
81
81
  elsif between
82
- message << " between #{between.first} and #{between.last} times"
82
+ message << " between #{between.first} and #{between.end ? between.last : 'infinite'} times"
83
83
  elsif maximum
84
84
  message << " at most #{occurrences maximum}"
85
85
  elsif minimum
@@ -62,13 +62,11 @@ module Capybara
62
62
 
63
63
  def matches_filters?(node, node_filter_errors = [])
64
64
  return true if (@resolved_node&.== node) && options[:allow_self]
65
- return false unless matches_locator_filter?(node)
66
65
 
67
- applied_filters << :system
68
- return false unless matches_system_filters?(node)
69
-
70
- applied_filters << :node
71
- matches_node_filters?(node, node_filter_errors) && matches_filter_block?(node)
66
+ matches_locator_filter?(node) &&
67
+ matches_system_filters?(node) &&
68
+ matches_node_filters?(node, node_filter_errors) &&
69
+ matches_filter_block?(node)
72
70
  rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
73
71
  false
74
72
  end
@@ -93,11 +91,11 @@ module Capybara
93
91
  exact = exact? if exact.nil?
94
92
  expr = apply_expression_filters(@expression)
95
93
  expr = exact ? expr.to_xpath(:exact) : expr.to_s if expr.respond_to?(:to_xpath)
96
- filtered_xpath(expr)
94
+ filtered_expression(expr)
97
95
  end
98
96
 
99
97
  def css
100
- filtered_css(apply_expression_filters(@expression))
98
+ filtered_expression(apply_expression_filters(@expression))
101
99
  end
102
100
 
103
101
  # @api private
@@ -136,12 +134,10 @@ module Capybara
136
134
  end
137
135
 
138
136
  def find_selector(locator)
139
- selector = if locator.is_a?(Symbol)
140
- Selector.all.fetch(locator) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
141
- else
142
- Selector.all.values.find { |sel| sel.match?(locator) }
143
- end
144
- selector || Selector.all[session_options.default_selector]
137
+ case locator
138
+ when Symbol then Selector[locator]
139
+ else Selector.for(locator)
140
+ end || Selector[session_options.default_selector]
145
141
  end
146
142
 
147
143
  def find_nodes_by_selector_format(node, exact)
@@ -165,6 +161,8 @@ module Capybara
165
161
  end
166
162
 
167
163
  def matches_node_filters?(node, errors)
164
+ applied_filters << :node
165
+
168
166
  unapplied_options = options.keys - valid_keys
169
167
  @selector.with_filter_errors(errors) do
170
168
  node_filters.all? do |filter_name, filter|
@@ -196,7 +194,7 @@ module Capybara
196
194
  end
197
195
 
198
196
  def filter_set(name)
199
- ::Capybara::Selector::FilterSet.all[name]
197
+ ::Capybara::Selector::FilterSet[name]
200
198
  end
201
199
 
202
200
  def node_filters
@@ -235,18 +233,11 @@ module Capybara
235
233
  raise ArgumentError, "invalid keys #{invalid_names}, should be one of #{valid_names}"
236
234
  end
237
235
 
238
- def filtered_xpath(expr)
239
- expr = "(#{expr})[#{conditions_from_id}]" if use_default_id_filter?
240
- expr = "(#{expr})[#{conditions_from_classes}]" if use_default_class_filter?
241
- expr
242
- end
243
-
244
- def filtered_css(expr)
245
- ::Capybara::Selector::CSS.split(expr).map do |sel|
246
- sel += conditions_from_id if use_default_id_filter?
247
- sel += conditions_from_classes if use_default_class_filter?
248
- sel
249
- end.join(', ')
236
+ def filtered_expression(expr)
237
+ conditions = {}
238
+ conditions[:id] = options[:id] if use_default_id_filter?
239
+ conditions[:class] = options[:class] if use_default_class_filter?
240
+ builder(expr).add_attribute_conditions(conditions)
250
241
  end
251
242
 
252
243
  def use_default_id_filter?
@@ -257,14 +248,6 @@ module Capybara
257
248
  options.key?(:class) && !custom_keys.include?(:class)
258
249
  end
259
250
 
260
- def conditions_from_classes
261
- builder.class_conditions(options[:class])
262
- end
263
-
264
- def conditions_from_id
265
- builder.attribute_conditions(id: options[:id])
266
- end
267
-
268
251
  def apply_expression_filters(expression)
269
252
  unapplied_options = options.keys - valid_keys
270
253
  expression_filters.inject(expression) do |expr, (name, ef)|
@@ -307,12 +290,14 @@ module Capybara
307
290
  end
308
291
 
309
292
  def matches_locator_filter?(node)
310
- return true if @selector.locator_filter.nil?
293
+ return true unless @selector.locator_filter
311
294
 
312
- @selector.locator_filter.call(node, @locator)
295
+ @selector.locator_filter.matches?(node, @locator, @selector)
313
296
  end
314
297
 
315
298
  def matches_system_filters?(node)
299
+ applied_filters << :system
300
+
316
301
  matches_id_filter?(node) &&
317
302
  matches_class_filter?(node) &&
318
303
  matches_text_filter?(node) &&
@@ -374,8 +359,8 @@ module Capybara
374
359
  @selector.default_visibility(session_options.ignore_hidden_elements, options)
375
360
  end
376
361
 
377
- def builder
378
- selector.builder
362
+ def builder(expr)
363
+ selector.builder(expr)
379
364
  end
380
365
  end
381
366
  end
@@ -59,7 +59,7 @@ module Capybara
59
59
  nil
60
60
  end
61
61
  when Range
62
- idx.max
62
+ idx.end && idx.max # endless range will have end == nil
63
63
  end
64
64
 
65
65
  if max_idx.nil?
@@ -92,9 +92,9 @@ module Capybara
92
92
  end
93
93
 
94
94
  if between
95
- min, max = between.minmax
96
- size = load_up_to(max + 1)
97
- return between.include?(size) ? 0 : size <=> min
95
+ min, max = between.min, (between.end && between.max)
96
+ size = load_up_to(max ? max + 1 : min)
97
+ return size <=> min unless between.include?(size)
98
98
  end
99
99
 
100
100
  0
@@ -6,41 +6,73 @@ module Capybara
6
6
  class Selector
7
7
  # @api private
8
8
  class CSSBuilder
9
- class << self
10
- def attribute_conditions(attributes)
11
- attributes.map do |attribute, value|
12
- case value
13
- when XPath::Expression
14
- raise ArgumentError, "XPath expressions are not supported for the :#{attribute} filter with CSS based selectors"
15
- when Regexp
16
- Selector::RegexpDisassembler.new(value).substrings.map do |str|
17
- "[#{attribute}*='#{str}'#{' i' if value.casefold?}]"
9
+ def initialize(expression)
10
+ @expression = expression || ''
11
+ end
12
+
13
+ attr_reader :expression
14
+
15
+ def add_attribute_conditions(**attributes)
16
+ @expression = attributes.inject(expression) do |css, (name, value)|
17
+ conditions = if name == :class
18
+ class_conditions(value)
19
+ elsif value.is_a? Regexp
20
+ Selector::RegexpDisassembler.new(value).alternated_substrings.map do |strs|
21
+ strs.map do |str|
22
+ "[#{name}*='#{str}'#{' i' if value.casefold?}]"
18
23
  end.join
19
- when true
20
- "[#{attribute}]"
21
- when false
22
- ':not([attribute])'
23
- else
24
- if attribute == :id
25
- "##{::Capybara::Selector::CSS.escape(value)}"
26
- else
27
- "[#{attribute}='#{value}']"
28
- end
29
24
  end
30
- end.join
25
+ else
26
+ [attribute_conditions(name => value)]
27
+ end
28
+
29
+ ::Capybara::Selector::CSS.split(css).map do |sel|
30
+ next sel if conditions.empty?
31
+
32
+ conditions.map { |cond| sel + cond }.join(', ')
33
+ end.join(', ')
31
34
  end
35
+ end
36
+
37
+ private
32
38
 
33
- def class_conditions(classes)
34
- case classes
39
+ def attribute_conditions(attributes)
40
+ attributes.map do |attribute, value|
41
+ case value
35
42
  when XPath::Expression
36
- raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
43
+ raise ArgumentError, "XPath expressions are not supported for the :#{attribute} filter with CSS based selectors"
37
44
  when Regexp
38
- attribute_conditions(class: classes)
45
+ Selector::RegexpDisassembler.new(value).substrings.map do |str|
46
+ "[#{attribute}*='#{str}'#{' i' if value.casefold?}]"
47
+ end.join
48
+ when true
49
+ "[#{attribute}]"
50
+ when false
51
+ ':not([attribute])'
39
52
  else
40
- cls = Array(classes).group_by { |cl| cl.start_with?('!') && !cl.start_with?('!!!') }
41
- (cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
42
- cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join
53
+ if attribute == :id
54
+ "##{::Capybara::Selector::CSS.escape(value)}"
55
+ else
56
+ "[#{attribute}='#{value}']"
57
+ end
58
+ end
59
+ end.join
60
+ end
61
+
62
+ def class_conditions(classes)
63
+ case classes
64
+ when XPath::Expression
65
+ raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
66
+ when Regexp
67
+ Selector::RegexpDisassembler.new(classes).alternated_substrings.map do |strs|
68
+ strs.map do |str|
69
+ "[class*='#{str}'#{' i' if classes.casefold?}]"
70
+ end.join
43
71
  end
72
+ else
73
+ cls = Array(classes).group_by { |cl| cl.start_with?('!') && !cl.start_with?('!!!') }
74
+ [(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
75
+ cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join]
44
76
  end
45
77
  end
46
78
  end
@@ -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